From 870c781cf759126aea705e0a422ac990ca27536a Mon Sep 17 00:00:00 2001 From: alemi Date: Sat, 23 Nov 2024 04:39:31 +0100 Subject: [PATCH] feat(web): global privacy control, 4 privacies --- web/index.html | 13 ++ web/src/activities/item.rs | 7 +- web/src/app.rs | 5 + web/src/components/mod.rs | 24 ++-- web/src/components/post.rs | 242 ++++++++++++++++++++++++------------- web/src/objects/item.rs | 38 +++--- 6 files changed, 214 insertions(+), 115 deletions(-) diff --git a/web/index.html b/web/index.html index 00b84c7..11617e6 100644 --- a/web/index.html +++ b/web/index.html @@ -317,6 +317,16 @@ color: var(--background); cursor: pointer; } + input[type="range"] { + accent-color: var(--accent); + } + input[type="range"]:hover { + outline: none; + } + input[type="range"]:focus { + outline: none; + accent-color: var(--accent-dim); + } .ml-1-r { margin-left: 1em; } @@ -377,6 +387,9 @@ color: unset; text-shadow: unset; } + span.big-emoji { + font-size: 1.5em; + } div.context { border-left: 1px solid var(--background-dim); padding-left: 1px; diff --git a/web/src/activities/item.rs b/web/src/activities/item.rs index fa7aeec..456794a 100644 --- a/web/src/activities/item.rs +++ b/web/src/activities/item.rs @@ -1,12 +1,15 @@ use leptos::*; use crate::prelude::*; -use apb::{target::Addressed, Activity, ActivityMut, Base, Object}; +use apb::{Activity, ActivityMut, Base, Object}; #[component] pub fn ActivityLine(activity: crate::Object, children: Children) -> impl IntoView { let object_id = activity.object().id().unwrap_or_default(); + let to = activity.to().all_ids(); + let cc = activity.cc().all_ids(); + let privacy = Privacy::from_addressed(&to, &cc); let activity_url = activity.id().map(|x| view! { "↗" }); @@ -31,7 +34,7 @@ pub fn ActivityLine(activity: crate::Object, children: Children) -> impl IntoVie {kind.as_ref().to_string()} {activity_url} - + diff --git a/web/src/app.rs b/web/src/app.rs index 6fe5fad..abbe7a0 100644 --- a/web/src/app.rs +++ b/web/src/app.rs @@ -74,6 +74,8 @@ pub fn App() -> impl IntoView { ); let (config, set_config, _) = use_local_storage::("config"); + let (privacy, set_privacy) = create_signal(Privacy::Private); + let auth = Auth { token, userid }; let username = auth.userid.get_untracked() @@ -85,6 +87,7 @@ pub fn App() -> impl IntoView { provide_context(auth); provide_context(config); provide_context(feeds); + provide_context(privacy); let reply_controls = ReplyControls::default(); provide_context(reply_controls); @@ -136,6 +139,8 @@ pub fn App() -> impl IntoView { />
+ +
{move || if advanced.get() { view! { }} else { view! { diff --git a/web/src/components/mod.rs b/web/src/components/mod.rs index b331057..68e5b5e 100644 --- a/web/src/components/mod.rs +++ b/web/src/components/mod.rs @@ -37,21 +37,17 @@ pub fn DateTime(t: Option>) -> impl IntoView { } } -pub const PRIVACY_PUBLIC : &str = "đŸĒŠ"; -pub const PRIVACY_FOLLOWERS : &str = "🔒"; -pub const PRIVACY_PRIVATE : &str = "📨"; - #[component] -pub fn PrivacyMarker(addressed: Vec) -> impl IntoView { - let privacy = if addressed.iter().any(|x| x == apb::target::PUBLIC) { - PRIVACY_PUBLIC - } else if addressed.iter().any(|x| x.ends_with("/followers")) { - PRIVACY_FOLLOWERS - } else { - PRIVACY_PRIVATE - }; - let audience = format!("[ {} ]", addressed.join(", ")); +pub fn PrivacyMarker<'a>( + privacy: Privacy, + #[prop(optional)] to: &'a [String], + #[prop(optional)] cc: &'a [String], + #[prop(optional)] big: bool, +) -> impl IntoView { + let to_txt = if to.is_empty() { String::new() } else { format!("to: {}", to.join(", ")) }; + let cc_txt = if cc.is_empty() { String::new() } else { format!("cc: {}", cc.join(", ")) }; + let audience = format!("{to_txt}\n{cc_txt}"); view! { - {privacy} + {privacy.icon()} } } diff --git a/web/src/components/post.rs b/web/src/components/post.rs index d3f3f39..6a22d85 100644 --- a/web/src/components/post.rs +++ b/web/src/components/post.rs @@ -35,17 +35,122 @@ struct MentionMatch { domain: String, } +pub type PrivacyControl = ReadSignal; + +#[derive(Debug, Clone, Copy)] +pub enum Privacy { + Broadcast = 4, + Public = 3, + Private = 2, + Direct = 1, +} + +impl Privacy { + pub fn is_public(&self) -> bool { + match self { + Self::Broadcast | Self::Public => true, + _ => false, + } + } + + pub fn from_value(v: &str) -> Self { + match v { + "1" => Self::Direct, + "2" => Self::Private, + "3" => Self::Public, + "4" => Self::Broadcast, + _ => panic!("invalid value for privacy"), + } + } + + pub fn from_addressed(to: &[String], cc: &[String]) -> Self { + if to.iter().any(|x| x == apb::target::PUBLIC) { + return Self::Broadcast; + } + if cc.iter().any(|x| x == apb::target::PUBLIC) { + return Self::Public; + } + if to.iter().any(|x| x.ends_with("/followers")) + || cc.iter().any(|x| x.ends_with("/followers")) { + return Self::Private; + } + + Self::Direct + } + + pub fn icon(&self) -> &'static str { + match self { + Self::Broadcast => "đŸ“ĸ", + Self::Public => "đŸĒŠ", + Self::Private => "🔒", + Self::Direct => "📨", + } + } + + pub fn address(&self, user: &str) -> (Vec, Vec) { + match self { + Self::Broadcast => ( + vec![apb::target::PUBLIC.to_string()], + vec![format!("{URL_BASE}/actors/{user}/followers")], + ), + Self::Public => ( + vec![], + vec![apb::target::PUBLIC.to_string(), format!("{URL_BASE}/actors/{user}/followers")], + ), + Self::Private => ( + vec![], + vec![format!("{URL_BASE}/actors/{user}/followers")], + ), + Self::Direct => ( + vec![], + vec![], + ), + } + } +} + +#[component] +pub fn PrivacySelector(setter: WriteSignal) -> impl IntoView { + let privacy = use_context::().expect("missing privacy context"); + let auth = use_context::().expect("missing auth context"); + view! { + + + + + +
+ + + {move || { + let p = privacy.get(); + let (to, cc) = p.address(&auth.username()); + view! { + + } + }} +
+ } +} + #[component] pub fn PostBox(advanced: WriteSignal) -> impl IntoView { let auth = use_context::().expect("missing auth context"); + let privacy = use_context::().expect("missing privacy context"); let reply = use_context::().expect("missing reply controls"); let (posting, set_posting) = create_signal(false); let (error, set_error) = create_signal(None); let (content, set_content) = create_signal("".to_string()); let summary_ref: NodeRef = create_node_ref(); - let public_ref: NodeRef = create_node_ref(); - let followers_ref: NodeRef = create_node_ref(); - let private_ref: NodeRef = create_node_ref(); // TODO is this too abusive with resources? im even checking if TLD exists... let mentions = create_local_resource( @@ -109,88 +214,63 @@ pub fn PostBox(advanced: WriteSignal) -> impl IntoView { on:input=move |ev| set_content.set(event_target_value(&ev)) > +
} diff --git a/web/src/objects/item.rs b/web/src/objects/item.rs index 12a1363..c01f382 100644 --- a/web/src/objects/item.rs +++ b/web/src/objects/item.rs @@ -11,8 +11,9 @@ pub fn Object(object: crate::Object) -> impl IntoView { let author_id = object.attributed_to().id().ok().unwrap_or_default(); let author = cache::OBJECTS.get_or(&author_id, serde_json::Value::String(author_id.clone()).into()); let sensitive = object.sensitive().unwrap_or_default(); - let addressed = object.addressed(); - let public = addressed.iter().any(|x| x.as_str() == apb::target::PUBLIC); + let to = object.to().all_ids(); + let cc = object.cc().all_ids(); + let privacy = Privacy::from_addressed(&to, &cc); let external_url = object.url().id().ok().unwrap_or_else(|| oid.clone()); let attachments = object.attachment() .flat() @@ -180,7 +181,7 @@ pub fn Object(object: crate::Object) -> impl IntoView { {object.in_reply_to().id().ok().map(|reply| view! { reply })} - + @@ -195,8 +196,8 @@ pub fn Object(object: crate::Object) -> impl IntoView { {audience_badge} - - + + {if privacy.is_public() { Some(view! { }) } else { None }} } @@ -230,6 +231,7 @@ pub fn LikeButton( let (count, set_count) = create_signal(n); let (clicked, set_clicked) = create_signal(!liked); let auth = use_context::().expect("missing auth context"); + let privacy = use_context::().expect("missing privacy context"); view! { impl IntoView { } #[component] -pub fn RepostButton(n: i32, target: String) -> impl IntoView { +pub fn RepostButton(n: i32, target: String, author: String) -> impl IntoView { let (count, set_count) = create_signal(n); let (clicked, set_clicked) = create_signal(true); let auth = use_context::().expect("missing auth context"); + let privacy = use_context::().expect("missing privacy context"); view! { impl IntoView { if !auth.present() { return; } if !clicked.get() { return; } set_clicked.set(false); - let to = apb::Node::links(vec![apb::target::PUBLIC.to_string()]); - let cc = apb::Node::links(vec![format!("{URL_BASE}/actors/{}/followers", auth.username())]); + let (mut to, cc) = privacy.get().address(&auth.username()); + to.push(author.clone()); let payload = serde_json::Value::Object(serde_json::Map::default()) .set_activity_type(Some(apb::ActivityType::Announce)) .set_object(apb::Node::link(target.clone())) - .set_to(to) - .set_cc(cc); + .set_to(apb::Node::links(to)) + .set_cc(apb::Node::links(cc)); spawn_local(async move { match Http::post(&auth.outbox(), &payload, auth).await { Ok(()) => set_count.set(count.get() + 1),