feat: add seen-on-relays viewer per message (#149)

* chore: bump version

* add seen on

* seen on menu
This commit is contained in:
reya
2025-09-14 11:50:14 +07:00
committed by GitHub
parent d38e70ecbf
commit 5127eaadbb
9 changed files with 164 additions and 63 deletions

76
Cargo.lock generated
View File

@@ -190,7 +190,7 @@ dependencies = [
[[package]] [[package]]
name = "assets" name = "assets"
version = "0.2.6" version = "0.2.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gpui", "gpui",
@@ -428,7 +428,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]] [[package]]
name = "auto_update" name = "auto_update"
version = "0.2.6" version = "0.2.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo-packager-updater", "cargo-packager-updater",
@@ -1029,7 +1029,7 @@ dependencies = [
[[package]] [[package]]
name = "client_keys" name = "client_keys"
version = "0.2.6" version = "0.2.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"global", "global",
@@ -1131,7 +1131,7 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
@@ -1166,7 +1166,7 @@ dependencies = [
[[package]] [[package]]
name = "common" name = "common"
version = "0.2.6" version = "0.2.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -1239,7 +1239,7 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]] [[package]]
name = "coop" name = "coop"
version = "0.2.6" version = "0.2.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assets", "assets",
@@ -1572,7 +1572,7 @@ dependencies = [
[[package]] [[package]]
name = "derive_refineable" name = "derive_refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2405,7 +2405,7 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]] [[package]]
name = "global" name = "global"
version = "0.2.6" version = "0.2.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dirs 5.0.1", "dirs 5.0.1",
@@ -2498,7 +2498,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"as-raw-xcb-connection", "as-raw-xcb-connection",
@@ -2592,7 +2592,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_macros" name = "gpui_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -2604,7 +2604,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_tokio" name = "gpui_tokio"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gpui", "gpui",
@@ -2818,7 +2818,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client" name = "http_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -2838,7 +2838,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client_tls" name = "http_client_tls"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32"
dependencies = [ dependencies = [
"rustls", "rustls",
"rustls-platform-verifier", "rustls-platform-verifier",
@@ -2941,7 +2941,7 @@ dependencies = [
[[package]] [[package]]
name = "i18n" name = "i18n"
version = "0.2.6" version = "0.2.7"
dependencies = [ dependencies = [
"rust-i18n", "rust-i18n",
] ]
@@ -3629,7 +3629,7 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen 0.71.1", "bindgen 0.71.1",
@@ -5069,7 +5069,7 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32"
dependencies = [ dependencies = [
"derive_refineable", "derive_refineable",
"workspace-hack", "workspace-hack",
@@ -5106,7 +5106,7 @@ checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]] [[package]]
name = "registry" name = "registry"
version = "0.2.6" version = "0.2.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"common", "common",
@@ -5223,7 +5223,7 @@ dependencies = [
[[package]] [[package]]
name = "reqwest_client" name = "reqwest_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -5758,7 +5758,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
[[package]] [[package]]
name = "semantic_version" name = "semantic_version"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
@@ -5773,18 +5773,28 @@ checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.219" version = "1.0.221"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.219" version = "1.0.221"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" checksum = "d6185cf75117e20e62b1ff867b9518577271e58abe0037c40bb4794969355ab0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -5813,15 +5823,15 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.143" version = "1.0.144"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" checksum = "56177480b00303e689183f110b4e727bb4211d692c62d4fcd16d02be93077d40"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"itoa", "itoa",
"memchr", "memchr",
"ryu", "ryu",
"serde", "serde_core",
] ]
[[package]] [[package]]
@@ -5893,7 +5903,7 @@ dependencies = [
[[package]] [[package]]
name = "settings" name = "settings"
version = "0.2.6" version = "0.2.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"global", "global",
@@ -5960,7 +5970,7 @@ dependencies = [
[[package]] [[package]]
name = "signer_proxy" name = "signer_proxy"
version = "0.2.6" version = "0.2.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"atomic-destructor", "atomic-destructor",
@@ -6199,7 +6209,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "sum_tree" name = "sum_tree"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@@ -6502,7 +6512,7 @@ dependencies = [
[[package]] [[package]]
name = "theme" name = "theme"
version = "0.2.6" version = "0.2.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gpui", "gpui",
@@ -6664,7 +6674,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "title_bar" name = "title_bar"
version = "0.2.6" version = "0.2.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"common", "common",
@@ -7035,7 +7045,7 @@ dependencies = [
[[package]] [[package]]
name = "ui" name = "ui"
version = "0.2.6" version = "0.2.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"common", "common",
@@ -7229,7 +7239,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "util" name = "util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ded646760467d653fd57ee3ef4fc1edcfafba6ae" source = "git+https://github.com/zed-industries/zed#c50b561e1c826e707e0d89bd7d82373c27f2fe32"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-fs", "async-fs",

View File

@@ -4,7 +4,7 @@ members = ["crates/*"]
default-members = ["crates/coop"] default-members = ["crates/coop"]
[workspace.package] [workspace.package]
version = "0.2.6" version = "0.2.7"
edition = "2021" edition = "2021"
publish = false publish = false

4
assets/icons/server.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="square" stroke-linejoin="round" stroke-width="1.5" d="M21.25 12V6.75a2 2 0 0 0-2-2H4.75a2 2 0 0 0-2 2V12m18.5 0H2.75m18.5 0v5.25a2 2 0 0 1-2 2H4.75a2 2 0 0 1-2-2V12"/>
<path fill="currentColor" stroke="currentColor" stroke-width=".5" d="M6.5 14.875a.75.75 0 1 1 0 1.5.75.75 0 0 1 0-1.5Zm0-7.25a.75.75 0 1 1 0 1.5.75.75 0 0 1 0-1.5Z"/>
</svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@@ -14,7 +14,7 @@ product-name = "Coop"
description = "Chat Freely, Stay Private on Nostr" description = "Chat Freely, Stay Private on Nostr"
identifier = "su.reya.coop" identifier = "su.reya.coop"
category = "SocialNetworking" category = "SocialNetworking"
version = "0.2.6" version = "0.2.7"
out-dir = "../../dist" out-dir = "../../dist"
before-packaging-command = "cargo build --release" before-packaging-command = "cargo build --release"
resources = ["Cargo.toml", "src"] resources = ["Cargo.toml", "src"]

View File

@@ -382,6 +382,14 @@ impl ChatSpace {
match message { match message {
RelayMessage::Event { event, .. } => { 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 // Skip events that have already been processed
if !processed_events.insert(event.id) { if !processed_events.insert(event.id) {
continue; continue;

View File

@@ -29,7 +29,7 @@ use ui::dock_area::panel::{Panel, PanelEvent};
use ui::emoji_picker::EmojiPicker; use ui::emoji_picker::EmojiPicker;
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
use ui::popup_menu::PopupMenu; use ui::popup_menu::{PopupMenu, PopupMenuExt};
use ui::text::RenderedText; use ui::text::RenderedText;
use ui::{ use ui::{
h_flex, v_flex, ContextModal, Disableable, Icon, IconName, InteractiveElementExt, Sizable, h_flex, v_flex, ContextModal, Disableable, Icon, IconName, InteractiveElementExt, Sizable,
@@ -40,7 +40,7 @@ mod subject;
#[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[derive(Action, Clone, PartialEq, Eq, Deserialize)]
#[action(namespace = chat, no_json)] #[action(namespace = chat, no_json)]
pub struct ChangeSubject(pub String); pub struct SeenOn(pub EventId);
pub fn init(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Chat> { pub fn init(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Chat> {
cx.new(|cx| Chat::new(room, window, cx)) cx.new(|cx| Chat::new(room, window, cx))
@@ -920,8 +920,7 @@ impl Chat {
} }
fn render_actions(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement { fn render_actions(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement {
let groups = vec![ let reply = Button::new("reply")
Button::new("reply")
.icon(IconName::Reply) .icon(IconName::Reply)
.tooltip(t!("chat.reply_button")) .tooltip(t!("chat.reply_button"))
.small() .small()
@@ -931,8 +930,10 @@ impl Chat {
cx.listener(move |this, _event, _window, cx| { cx.listener(move |this, _event, _window, cx| {
this.reply_to(&id, cx); this.reply_to(&id, cx);
}) })
}), })
Button::new("copy") .into_any_element();
let copy = Button::new("copy")
.icon(IconName::Copy) .icon(IconName::Copy)
.tooltip(t!("chat.copy_message_button")) .tooltip(t!("chat.copy_message_button"))
.small() .small()
@@ -942,8 +943,21 @@ impl Chat {
cx.listener(move |this, _event, _window, cx| { cx.listener(move |this, _event, _window, cx| {
this.copy_message(&id, 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() h_flex()
.p_0p5() .p_0p5()
@@ -957,7 +971,7 @@ impl Chat {
.border_1() .border_1()
.border_color(cx.theme().border) .border_color(cx.theme().border)
.bg(cx.theme().background) .bg(cx.theme().background)
.children(groups) .children(vec![reply, copy, more])
.group_hover("", |this| this.visible()) .group_hover("", |this| this.visible())
} }
@@ -1133,6 +1147,62 @@ impl Chat {
.ok(); .ok();
}) })
} }
fn on_open_seen_on(&mut self, ev: &SeenOn, window: &mut Window, cx: &mut Context<Self>) {
let id = ev.0;
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
let client = nostr_client();
let css = css();
let mut relays: Vec<RelayUrl> = 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 { impl Panel for Chat {
@@ -1179,6 +1249,7 @@ impl Focusable for Chat {
impl Render for Chat { impl Render for Chat {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex() v_flex()
.on_action(cx.listener(Self::on_open_seen_on))
.image_cache(self.image_cache.clone()) .image_cache(self.image_cache.clone())
.size_full() .size_full()
.child( .child(

View File

@@ -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)] #[derive(Debug)]
pub struct CoopSimpleStorage { pub struct CoopSimpleStorage {
pub init_at: Timestamp, pub init_at: Timestamp,
pub last_used_at: Option<Timestamp>,
pub gift_wrap_sub_id: SubscriptionId, pub gift_wrap_sub_id: SubscriptionId,
pub gift_wrap_processing: AtomicBool, pub gift_wrap_processing: AtomicBool,
pub auto_close_opts: Option<SubscribeAutoCloseOptions>, pub auto_close_opts: Option<SubscribeAutoCloseOptions>,
pub seen_on_relays: RwLock<HashMap<EventId, HashSet<RelayUrl>>>,
pub sent_ids: RwLock<HashSet<EventId>>, pub sent_ids: RwLock<HashSet<EventId>>,
pub resent_ids: RwLock<Vec<Output<EventId>>>, pub resent_ids: RwLock<Vec<Output<EventId>>>,
pub resend_queue: RwLock<HashMap<EventId, RelayUrl>>, pub resend_queue: RwLock<HashMap<EventId, RelayUrl>>,
@@ -137,11 +139,13 @@ impl CoopSimpleStorage {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
init_at: Timestamp::now(), init_at: Timestamp::now(),
last_used_at: None,
gift_wrap_sub_id: SubscriptionId::new("inbox"), gift_wrap_sub_id: SubscriptionId::new("inbox"),
gift_wrap_processing: AtomicBool::new(false), gift_wrap_processing: AtomicBool::new(false),
auto_close_opts: Some( auto_close_opts: Some(
SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE), SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE),
), ),
seen_on_relays: RwLock::new(HashMap::new()),
sent_ids: RwLock::new(HashSet::new()), sent_ids: RwLock::new(HashSet::new()),
resent_ids: RwLock::new(Vec::new()), resent_ids: RwLock::new(Vec::new()),
resend_queue: RwLock::new(HashMap::new()), resend_queue: RwLock::new(HashMap::new()),

View File

@@ -52,6 +52,7 @@ pub enum IconName {
Signal, Signal,
Search, Search,
Settings, Settings,
Server,
SortAscending, SortAscending,
SortDescending, SortDescending,
Sun, Sun,
@@ -112,6 +113,7 @@ impl IconName {
Self::Signal => "icons/signal.svg", Self::Signal => "icons/signal.svg",
Self::Search => "icons/search.svg", Self::Search => "icons/search.svg",
Self::Settings => "icons/settings.svg", Self::Settings => "icons/settings.svg",
Self::Server => "icons/server.svg",
Self::SortAscending => "icons/sort-ascending.svg", Self::SortAscending => "icons/sort-ascending.svg",
Self::SortDescending => "icons/sort-descending.svg", Self::SortDescending => "icons/sort-descending.svg",
Self::Sun => "icons/sun.svg", Self::Sun => "icons/sun.svg",

View File

@@ -51,6 +51,8 @@ common:
en: "Recommended:" en: "Recommended:"
resend: resend:
en: "Resend" en: "Resend"
seen_on:
en: "Seen on"
auto_update: auto_update:
updating: updating: