From f6b5a4c876132c7254734a8ed6fca678a6dbefad Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 04:27:41 +0000 Subject: [PATCH] feat: add maximally portable share card functionality - Add share button to status display page - Create share modal with platform-specific sharing options (BlueSky, Twitter/X, SMS, Email, Mastodon) - Implement copy-to-clipboard for share URL - Add dynamic SVG Open Graph image generation endpoint - Update meta tags to use dynamic share card images - Support Web Share API for native sharing on mobile Co-authored-by: nate nowack --- src/api/mod.rs | 1 + src/api/status_read.rs | 93 ++++++++ templates/base.html | 2 +- templates/status.html | 478 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 573 insertions(+), 1 deletion(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index e6475f6..25b47d7 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -23,6 +23,7 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) { .service(status_read::home) .service(status_read::user_status_page) .service(status_read::feed) + .service(status_read::user_og_image) // Status JSON API routes (read) .service(status_read::owner_status_json) .service(status_read::user_status_json) diff --git a/src/api/status_read.rs b/src/api/status_read.rs index b2c9daf..37e6e9d 100644 --- a/src/api/status_read.rs +++ b/src/api/status_read.rs @@ -446,3 +446,96 @@ pub async fn get_following( // Placeholder: follow list disabled here to keep module slim Ok(web::Json(json!({ "follows": [] }))) } + +/// Generate an SVG Open Graph image for a user's status +#[get("/@{handle}/og-image.svg")] +pub async fn user_og_image( + handle: web::Path, + db_pool: web::Data>, +) -> Result { + let handle = handle.into_inner(); + let atproto_handle_resolver = AtprotoHandleResolver::new(AtprotoHandleResolverConfig { + dns_txt_resolver: HickoryDnsTxtResolver::default(), + http_client: Arc::new(DefaultHttpClient::default()), + }); + + let handle_obj = atrium_api::types::string::Handle::new(handle.clone()).ok(); + let did = if let Some(h) = handle_obj { + atproto_handle_resolver.resolve(&h).await.ok() + } else { + None + }; + + let current_status = if let Some(did) = did { + StatusFromDb::my_status(&db_pool, &did) + .await + .unwrap_or(None) + .and_then(|s| { + if let Some(expires_at) = s.expires_at { + if chrono::Utc::now() > expires_at { + return None; + } + } + Some(s) + }) + } else { + None + }; + + // Generate SVG with status + let (emoji, text) = if let Some(status) = current_status { + let emoji = if status.status.starts_with("custom:") { + // For custom emojis, we'll use a placeholder emoji in the SVG + "✨".to_string() + } else { + status.status + }; + (emoji, status.text.unwrap_or_default()) + } else { + ("💭".to_string(), "no status set".to_string()) + }; + + let svg = format!(r#" + + + + + + + + + + + + + + + + {} + + + @{} + + + {} + + + status.zzstoatzz.io +"#, + emoji, + handle, + if text.len() > 60 { + format!("{}...", &text[..60]) + } else { + text + } + ); + + Ok(web::HttpResponse::Ok() + .content_type("image/svg+xml") + .body(svg)) +} diff --git a/templates/base.html b/templates/base.html index 20f19f6..214b273 100644 --- a/templates/base.html +++ b/templates/base.html @@ -17,7 +17,7 @@ - + diff --git a/templates/status.html b/templates/status.html index 86c1220..54f09b1 100644 --- a/templates/status.html +++ b/templates/status.html @@ -4,9 +4,11 @@ {% block og_url %}/@{{ handle }}{% endblock %} {% block og_title %}@{{ handle }}'s status{% endblock %} {% block og_description %}{% if let Some(current) = current_status %}{{ current.status }} {% if current.text.is_some() %}{{ current.text.as_ref().unwrap() }}{% endif %}{% else %}no status currently set{% endif %}{% endblock %} +{% block og_image %}https://status.zzstoatzz.io/@{{ handle }}/og-image.svg{% endblock %} {% block twitter_url %}/@{{ handle }}{% endblock %} {% block twitter_title %}@{{ handle }}'s status{% endblock %} {% block twitter_description %}{% if let Some(current) = current_status %}{{ current.status }} {% if current.text.is_some() %}{{ current.text.as_ref().unwrap() }}{% endif %}{% else %}no status currently set{% endif %}{% endblock %} +{% block twitter_image %}https://status.zzstoatzz.io/@{{ handle }}/og-image.svg{% endblock %} {% block content %} + {% endif %} + {% if is_owner %}
@@ -250,6 +266,88 @@

pick an emoji

{% endif %} + + +