From 5127eaadbb535b0ba98eb948d577a2d0aefc31c3 Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Sun, 14 Sep 2025 11:50:14 +0700 Subject: [PATCH] feat: add seen-on-relays viewer per message (#149) * chore: bump version * add seen on * seen on menu --- Cargo.lock | 76 ++++++++++-------- Cargo.toml | 2 +- assets/icons/server.svg | 4 + crates/coop/Cargo.toml | 2 +- crates/coop/src/chatspace.rs | 8 ++ crates/coop/src/views/chat/mod.rs | 125 +++++++++++++++++++++++------- crates/global/src/lib.rs | 6 +- crates/ui/src/icon.rs | 2 + locales/app.yml | 2 + 9 files changed, 164 insertions(+), 63 deletions(-) create mode 100644 assets/icons/server.svg diff --git a/Cargo.lock b/Cargo.lock index a61413d..816549a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,7 +190,7 @@ dependencies = [ [[package]] name = "assets" -version = "0.2.6" +version = "0.2.7" dependencies = [ "anyhow", "gpui", @@ -428,7 +428,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "auto_update" -version = "0.2.6" +version = "0.2.7" dependencies = [ "anyhow", "cargo-packager-updater", @@ -1029,7 +1029,7 @@ dependencies = [ [[package]] name = "client_keys" -version = "0.2.6" +version = "0.2.7" dependencies = [ "anyhow", "global", @@ -1131,7 +1131,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" +source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1166,7 +1166,7 @@ dependencies = [ [[package]] name = "common" -version = "0.2.6" +version = "0.2.7" dependencies = [ "anyhow", "chrono", @@ -1239,7 +1239,7 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] name = "coop" -version = "0.2.6" +version = "0.2.7" dependencies = [ "anyhow", "assets", @@ -1572,7 +1572,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" +source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32" dependencies = [ "proc-macro2", "quote", @@ -2405,7 +2405,7 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "global" -version = "0.2.6" +version = "0.2.7" dependencies = [ "anyhow", "dirs 5.0.1", @@ -2498,7 +2498,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" +source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2592,7 +2592,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" +source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2604,7 +2604,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" +source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32" dependencies = [ "anyhow", "gpui", @@ -2818,7 +2818,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" +source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32" dependencies = [ "anyhow", "bytes", @@ -2838,7 +2838,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" +source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32" dependencies = [ "rustls", "rustls-platform-verifier", @@ -2941,7 +2941,7 @@ dependencies = [ [[package]] name = "i18n" -version = "0.2.6" +version = "0.2.7" dependencies = [ "rust-i18n", ] @@ -3629,7 +3629,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" +source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32" dependencies = [ "anyhow", "bindgen 0.71.1", @@ -5069,7 +5069,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" +source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32" dependencies = [ "derive_refineable", "workspace-hack", @@ -5106,7 +5106,7 @@ checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "registry" -version = "0.2.6" +version = "0.2.7" dependencies = [ "anyhow", "common", @@ -5223,7 +5223,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" +source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32" dependencies = [ "anyhow", "bytes", @@ -5758,7 +5758,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" +source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32" dependencies = [ "anyhow", "serde", @@ -5773,18 +5773,28 @@ checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.221" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "341877e04a22458705eb4e131a1508483c877dca2792b3781d4e5d8a6019ec43" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.221" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c459bc0a14c840cb403fc14b148620de1e0778c96ecd6e0c8c3cacb6d8d00fe" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.221" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d6185cf75117e20e62b1ff867b9518577271e58abe0037c40bb4794969355ab0" dependencies = [ "proc-macro2", "quote", @@ -5813,15 +5823,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "56177480b00303e689183f110b4e727bb4211d692c62d4fcd16d02be93077d40" dependencies = [ "indexmap", "itoa", "memchr", "ryu", - "serde", + "serde_core", ] [[package]] @@ -5893,7 +5903,7 @@ dependencies = [ [[package]] name = "settings" -version = "0.2.6" +version = "0.2.7" dependencies = [ "anyhow", "global", @@ -5960,7 +5970,7 @@ dependencies = [ [[package]] name = "signer_proxy" -version = "0.2.6" +version = "0.2.7" dependencies = [ "anyhow", "atomic-destructor", @@ -6199,7 +6209,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" +source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32" dependencies = [ "arrayvec", "log", @@ -6502,7 +6512,7 @@ dependencies = [ [[package]] name = "theme" -version = "0.2.6" +version = "0.2.7" dependencies = [ "anyhow", "gpui", @@ -6664,7 +6674,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "title_bar" -version = "0.2.6" +version = "0.2.7" dependencies = [ "anyhow", "common", @@ -7035,7 +7045,7 @@ dependencies = [ [[package]] name = "ui" -version = "0.2.6" +version = "0.2.7" dependencies = [ "anyhow", "common", @@ -7229,7 +7239,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" +source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32" dependencies = [ "anyhow", "async-fs", diff --git a/Cargo.toml b/Cargo.toml index a1b3b24..3987c2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["crates/*"] default-members = ["crates/coop"] [workspace.package] -version = "0.2.6" +version = "0.2.7" edition = "2021" publish = false diff --git a/assets/icons/server.svg b/assets/icons/server.svg new file mode 100644 index 0000000..841911c --- /dev/null +++ b/assets/icons/server.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/coop/Cargo.toml b/crates/coop/Cargo.toml index 51ba207..09d9a45 100644 --- a/crates/coop/Cargo.toml +++ b/crates/coop/Cargo.toml @@ -14,7 +14,7 @@ product-name = "Coop" description = "Chat Freely, Stay Private on Nostr" identifier = "su.reya.coop" category = "SocialNetworking" -version = "0.2.6" +version = "0.2.7" out-dir = "../../dist" before-packaging-command = "cargo build --release" resources = ["Cargo.toml", "src"] diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index a839d2d..be9525a 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -382,6 +382,14 @@ impl ChatSpace { match message { RelayMessage::Event { event, .. } => { + // Keep track of which relays have seen this event + css.seen_on_relays + .write() + .await + .entry(event.id) + .or_insert_with(HashSet::new) + .insert(relay_url); + // Skip events that have already been processed if !processed_events.insert(event.id) { continue; diff --git a/crates/coop/src/views/chat/mod.rs b/crates/coop/src/views/chat/mod.rs index 1c149a5..9df7dca 100644 --- a/crates/coop/src/views/chat/mod.rs +++ b/crates/coop/src/views/chat/mod.rs @@ -29,7 +29,7 @@ use ui::dock_area::panel::{Panel, PanelEvent}; use ui::emoji_picker::EmojiPicker; use ui::input::{InputEvent, InputState, TextInput}; use ui::modal::ModalButtonProps; -use ui::popup_menu::PopupMenu; +use ui::popup_menu::{PopupMenu, PopupMenuExt}; use ui::text::RenderedText; use ui::{ h_flex, v_flex, ContextModal, Disableable, Icon, IconName, InteractiveElementExt, Sizable, @@ -40,7 +40,7 @@ mod subject; #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = chat, no_json)] -pub struct ChangeSubject(pub String); +pub struct SeenOn(pub EventId); pub fn init(room: Entity, window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Chat::new(room, window, cx)) @@ -920,30 +920,44 @@ impl Chat { } fn render_actions(&self, id: &EventId, cx: &Context) -> impl IntoElement { - let groups = vec![ - Button::new("reply") - .icon(IconName::Reply) - .tooltip(t!("chat.reply_button")) - .small() - .ghost() - .on_click({ - let id = id.to_owned(); - cx.listener(move |this, _event, _window, cx| { - this.reply_to(&id, cx); - }) - }), - Button::new("copy") - .icon(IconName::Copy) - .tooltip(t!("chat.copy_message_button")) - .small() - .ghost() - .on_click({ - let id = id.to_owned(); - cx.listener(move |this, _event, _window, cx| { - this.copy_message(&id, cx); - }) - }), - ]; + let reply = Button::new("reply") + .icon(IconName::Reply) + .tooltip(t!("chat.reply_button")) + .small() + .ghost() + .on_click({ + let id = id.to_owned(); + cx.listener(move |this, _event, _window, cx| { + this.reply_to(&id, cx); + }) + }) + .into_any_element(); + + let copy = Button::new("copy") + .icon(IconName::Copy) + .tooltip(t!("chat.copy_message_button")) + .small() + .ghost() + .on_click({ + let id = id.to_owned(); + cx.listener(move |this, _event, _window, cx| { + this.copy_message(&id, cx); + }) + }) + .into_any_element(); + + let more = Button::new("seen-on") + .icon(IconName::Ellipsis) + .small() + .ghost() + .popup_menu({ + let id = id.to_owned(); + move |this, _window, _cx| { + // TODO: add more actions + this.menu(t!("common.seen_on"), Box::new(SeenOn(id))) + } + }) + .into_any_element(); h_flex() .p_0p5() @@ -957,7 +971,7 @@ impl Chat { .border_1() .border_color(cx.theme().border) .bg(cx.theme().background) - .children(groups) + .children(vec![reply, copy, more]) .group_hover("", |this| this.visible()) } @@ -1133,6 +1147,62 @@ impl Chat { .ok(); }) } + + fn on_open_seen_on(&mut self, ev: &SeenOn, window: &mut Window, cx: &mut Context) { + let id = ev.0; + + let task: Task, Error>> = cx.background_spawn(async move { + let client = nostr_client(); + let css = css(); + let mut relays: Vec = vec![]; + + let filter = Filter::new() + .kind(Kind::ApplicationSpecificData) + .event(id) + .limit(1); + + if let Some(event) = client.database().query(filter).await?.first_owned() { + if let Some(Ok(id)) = event.tags.identifier().map(EventId::parse) { + if let Some(urls) = css.seen_on_relays.read().await.get(&id).cloned() { + relays.extend(urls); + } + } + } + + Ok(relays) + }); + + cx.spawn_in(window, async move |_, cx| { + if let Ok(urls) = task.await { + cx.update(|window, cx| { + window.open_modal(cx, move |this, _window, cx| { + this.title(shared_t!("common.seen_on")).child( + v_flex().pb_4().gap_2().children({ + let mut items = Vec::with_capacity(urls.len()); + + for url in urls.clone().into_iter() { + items.push( + h_flex() + .h_8() + .px_2() + .bg(cx.theme().elevated_surface_background) + .rounded(cx.theme().radius) + .font_semibold() + .text_xs() + .child(url.to_string()), + ) + } + + items + }), + ) + }); + }) + .ok(); + } + }) + .detach(); + } } impl Panel for Chat { @@ -1179,6 +1249,7 @@ impl Focusable for Chat { impl Render for Chat { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() + .on_action(cx.listener(Self::on_open_seen_on)) .image_cache(self.image_cache.clone()) .size_full() .child( diff --git a/crates/global/src/lib.rs b/crates/global/src/lib.rs index 1253adb..fa4fa13 100644 --- a/crates/global/src/lib.rs +++ b/crates/global/src/lib.rs @@ -115,13 +115,15 @@ impl Ingester { } } -/// A simple storage to store all runtime states that using across the application. +/// A simple storage to store all states that using across the application. #[derive(Debug)] pub struct CoopSimpleStorage { pub init_at: Timestamp, + pub last_used_at: Option, pub gift_wrap_sub_id: SubscriptionId, pub gift_wrap_processing: AtomicBool, pub auto_close_opts: Option, + pub seen_on_relays: RwLock>>, pub sent_ids: RwLock>, pub resent_ids: RwLock>>, pub resend_queue: RwLock>, @@ -137,11 +139,13 @@ impl CoopSimpleStorage { pub fn new() -> Self { Self { init_at: Timestamp::now(), + last_used_at: None, gift_wrap_sub_id: SubscriptionId::new("inbox"), gift_wrap_processing: AtomicBool::new(false), auto_close_opts: Some( SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE), ), + seen_on_relays: RwLock::new(HashMap::new()), sent_ids: RwLock::new(HashSet::new()), resent_ids: RwLock::new(Vec::new()), resend_queue: RwLock::new(HashMap::new()), diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index 19d0a7a..143fbbb 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -52,6 +52,7 @@ pub enum IconName { Signal, Search, Settings, + Server, SortAscending, SortDescending, Sun, @@ -112,6 +113,7 @@ impl IconName { Self::Signal => "icons/signal.svg", Self::Search => "icons/search.svg", Self::Settings => "icons/settings.svg", + Self::Server => "icons/server.svg", Self::SortAscending => "icons/sort-ascending.svg", Self::SortDescending => "icons/sort-descending.svg", Self::Sun => "icons/sun.svg", diff --git a/locales/app.yml b/locales/app.yml index b743289..eb66b2e 100644 --- a/locales/app.yml +++ b/locales/app.yml @@ -51,6 +51,8 @@ common: en: "Recommended:" resend: en: "Resend" + seen_on: + en: "Seen on" auto_update: updating: