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: