chore: improve chat panel (#121)
* . * . * . * skip sent message * improve sent reports * . * . * .
This commit is contained in:
58
Cargo.lock
generated
58
Cargo.lock
generated
@@ -178,7 +178,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "assets"
|
name = "assets"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -417,7 +417,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "auto_update"
|
name = "auto_update"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo-packager-updater",
|
"cargo-packager-updater",
|
||||||
@@ -1021,7 +1021,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "client_keys"
|
name = "client_keys"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"global",
|
"global",
|
||||||
@@ -1123,7 +1123,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
|
source = "git+https://github.com/zed-industries/zed#308cb9e537eda81b35bfccef00e2ef7be8d070d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
@@ -1158,7 +1158,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "common"
|
name = "common"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -1214,7 +1214,7 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coop"
|
name = "coop"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assets",
|
"assets",
|
||||||
@@ -1544,7 +1544,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#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
|
source = "git+https://github.com/zed-industries/zed#308cb9e537eda81b35bfccef00e2ef7be8d070d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2342,7 +2342,7 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "global"
|
name = "global"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
@@ -2436,7 +2436,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
|
source = "git+https://github.com/zed-industries/zed#308cb9e537eda81b35bfccef00e2ef7be8d070d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
@@ -2529,7 +2529,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#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
|
source = "git+https://github.com/zed-industries/zed#308cb9e537eda81b35bfccef00e2ef7be8d070d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2541,7 +2541,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#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
|
source = "git+https://github.com/zed-industries/zed#308cb9e537eda81b35bfccef00e2ef7be8d070d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -2772,7 +2772,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#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
|
source = "git+https://github.com/zed-industries/zed#308cb9e537eda81b35bfccef00e2ef7be8d070d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2792,7 +2792,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#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
|
source = "git+https://github.com/zed-industries/zed#308cb9e537eda81b35bfccef00e2ef7be8d070d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -2886,7 +2886,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "i18n"
|
name = "i18n"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rust-i18n",
|
"rust-i18n",
|
||||||
]
|
]
|
||||||
@@ -3003,7 +3003,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "identity"
|
name = "identity"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"client_keys",
|
"client_keys",
|
||||||
@@ -3594,7 +3594,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
|
source = "git+https://github.com/zed-industries/zed#308cb9e537eda81b35bfccef00e2ef7be8d070d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen 0.71.1",
|
"bindgen 0.71.1",
|
||||||
@@ -4632,9 +4632,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.95"
|
version = "1.0.96"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -4984,7 +4984,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
|
source = "git+https://github.com/zed-industries/zed#308cb9e537eda81b35bfccef00e2ef7be8d070d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
@@ -5021,7 +5021,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "registry"
|
name = "registry"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -5029,13 +5029,11 @@ dependencies = [
|
|||||||
"fuzzy-matcher",
|
"fuzzy-matcher",
|
||||||
"global",
|
"global",
|
||||||
"gpui",
|
"gpui",
|
||||||
"i18n",
|
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"nostr",
|
"nostr",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"oneshot",
|
"oneshot",
|
||||||
"rust-i18n",
|
|
||||||
"settings",
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
@@ -5142,7 +5140,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#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
|
source = "git+https://github.com/zed-industries/zed#308cb9e537eda81b35bfccef00e2ef7be8d070d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -5678,7 +5676,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#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
|
source = "git+https://github.com/zed-industries/zed#308cb9e537eda81b35bfccef00e2ef7be8d070d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5813,7 +5811,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "settings"
|
name = "settings"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"global",
|
"global",
|
||||||
@@ -6073,7 +6071,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#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
|
source = "git+https://github.com/zed-industries/zed#308cb9e537eda81b35bfccef00e2ef7be8d070d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -6376,7 +6374,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "theme"
|
name = "theme"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -6536,7 +6534,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "title_bar"
|
name = "title_bar"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"common",
|
"common",
|
||||||
@@ -6907,7 +6905,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ui"
|
name = "ui"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"common",
|
"common",
|
||||||
@@ -7107,7 +7105,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#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
|
source = "git+https://github.com/zed-industries/zed#308cb9e537eda81b35bfccef00e2ef7be8d070d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
|
|||||||
3
assets/icons/sent.svg
Normal file
3
assets/icons/sent.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m7.25 10.488 3.675 3.762 6.825-10m-14 10.5v2.3c0 1.12 0 1.68.218 2.108a2 2 0 0 0 .874.874c.428.218.988.218 2.108.218h10.1c1.12 0 1.68 0 2.108-.218a2 2 0 0 0 .874-.874c.218-.428.218-.988.218-2.108v-2.3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 406 B |
@@ -155,12 +155,19 @@ impl ChatSpace {
|
|||||||
if let Some(room) = room.upgrade() {
|
if let Some(room) = room.upgrade() {
|
||||||
this.dock.update(cx, |this, cx| {
|
this.dock.update(cx, |this, cx| {
|
||||||
let panel = chat::init(room, window, cx);
|
let panel = chat::init(room, window, cx);
|
||||||
// Load messages on panel creation
|
|
||||||
|
// Load messages when the panel is created
|
||||||
panel.update(cx, |this, cx| {
|
panel.update(cx, |this, cx| {
|
||||||
this.load_messages(window, cx);
|
this.load_messages(window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.add_panel(panel, DockPlacement::Center, window, cx);
|
// Add the panel to the center dock (tabs)
|
||||||
|
this.add_panel(
|
||||||
|
Arc::new(panel),
|
||||||
|
DockPlacement::Center,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
window.push_notification(t!("common.room_error"), cx);
|
window.push_notification(t!("common.room_error"), cx);
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ fn main() {
|
|||||||
|
|
||||||
match smol::future::or(recv(), timeout()).await {
|
match smol::future::or(recv(), timeout()).await {
|
||||||
Some(event) => {
|
Some(event) => {
|
||||||
let cached = try_unwrap_event(&event, &signal_tx, &mta_tx).await;
|
let cached = unwrap_gift(&event, &signal_tx, &mta_tx).await;
|
||||||
|
|
||||||
// Increment the total messages counter if message is not from cache
|
// Increment the total messages counter if message is not from cache
|
||||||
if !cached {
|
if !cached {
|
||||||
@@ -521,21 +521,21 @@ async fn get_unwrapped(root: EventId) -> Result<Event, Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Unwraps a gift-wrapped event and processes its contents.
|
/// Unwraps a gift-wrapped event and processes its contents.
|
||||||
async fn try_unwrap_event(
|
async fn unwrap_gift(
|
||||||
event: &Event,
|
gift: &Event,
|
||||||
signal_tx: &Sender<NostrSignal>,
|
signal_tx: &Sender<NostrSignal>,
|
||||||
mta_tx: &Sender<PublicKey>,
|
mta_tx: &Sender<PublicKey>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let client = nostr_client();
|
let client = nostr_client();
|
||||||
let mut is_cached = false;
|
let mut is_cached = false;
|
||||||
|
|
||||||
let event = match get_unwrapped(event.id).await {
|
let event = match get_unwrapped(gift.id).await {
|
||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
is_cached = true;
|
is_cached = true;
|
||||||
event
|
event
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
match client.unwrap_gift_wrap(event).await {
|
match client.unwrap_gift_wrap(gift).await {
|
||||||
Ok(unwrap) => {
|
Ok(unwrap) => {
|
||||||
// Sign the unwrapped event with a RANDOM KEYS
|
// Sign the unwrapped event with a RANDOM KEYS
|
||||||
let Ok(unwrapped) = unwrap.rumor.sign_with_keys(&Keys::generate()) else {
|
let Ok(unwrapped) = unwrap.rumor.sign_with_keys(&Keys::generate()) else {
|
||||||
@@ -544,7 +544,7 @@ async fn try_unwrap_event(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Save this event to the database for future use.
|
// Save this event to the database for future use.
|
||||||
if let Err(e) = set_unwrapped(event.id, &unwrapped).await {
|
if let Err(e) = set_unwrapped(gift.id, &unwrapped).await {
|
||||||
log::warn!("Failed to cache unwrapped event: {e}")
|
log::warn!("Failed to cache unwrapped event: {e}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,6 @@ common = { path = "../common" }
|
|||||||
global = { path = "../global" }
|
global = { path = "../global" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
|
|
||||||
rust-i18n.workspace = true
|
|
||||||
i18n.workspace = true
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr.workspace = true
|
nostr.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ use crate::room::Room;
|
|||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod room;
|
pub mod room;
|
||||||
|
|
||||||
i18n::init!();
|
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
Registry::set_global(cx.new(Registry::new), cx);
|
Registry::set_global(cx.new(Registry::new), cx);
|
||||||
}
|
}
|
||||||
@@ -421,8 +419,8 @@ impl Registry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Emit the new message to the room
|
// Emit the new message to the room
|
||||||
cx.defer_in(window, |this, window, cx| {
|
cx.defer_in(window, move |this, _window, cx| {
|
||||||
this.emit_message(event, window, cx);
|
this.emit_message(event, cx);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::iter::IntoIterator;
|
|
||||||
|
|
||||||
use chrono::{Local, TimeZone};
|
use chrono::{Local, TimeZone};
|
||||||
use gpui::SharedString;
|
use gpui::SharedString;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
use crate::room::SendError;
|
|
||||||
|
|
||||||
/// Represents a message in the chat system.
|
|
||||||
///
|
|
||||||
/// Contains information about the message content, author, creation time,
|
|
||||||
/// mentions, replies, and any errors that occurred during sending.
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Message {
|
pub struct RenderedMessage {
|
||||||
/// Unique identifier of the message (EventId from nostr_sdk)
|
|
||||||
pub id: EventId,
|
pub id: EventId,
|
||||||
/// Author's public key
|
/// Author's public key
|
||||||
pub author: PublicKey,
|
pub author: PublicKey,
|
||||||
@@ -23,138 +15,82 @@ pub struct Message {
|
|||||||
pub created_at: Timestamp,
|
pub created_at: Timestamp,
|
||||||
/// List of mentioned public keys in the message
|
/// List of mentioned public keys in the message
|
||||||
pub mentions: Vec<PublicKey>,
|
pub mentions: Vec<PublicKey>,
|
||||||
/// List of EventIds this message is replying to
|
/// List of event of the message this message is a reply to
|
||||||
pub replies_to: Option<Vec<EventId>>,
|
pub replies_to: Vec<EventId>,
|
||||||
/// Any errors that occurred while sending this message
|
|
||||||
pub errors: Option<Vec<SendError>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for Message {}
|
impl From<Event> for RenderedMessage {
|
||||||
|
fn from(inner: Event) -> Self {
|
||||||
|
let mentions = extract_mentions(&inner.content);
|
||||||
|
let replies_to = extract_reply_ids(&inner.tags);
|
||||||
|
|
||||||
impl PartialEq for Message {
|
Self {
|
||||||
|
id: inner.id,
|
||||||
|
author: inner.pubkey,
|
||||||
|
content: inner.content.into(),
|
||||||
|
created_at: inner.created_at,
|
||||||
|
mentions,
|
||||||
|
replies_to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UnsignedEvent> for RenderedMessage {
|
||||||
|
fn from(inner: UnsignedEvent) -> Self {
|
||||||
|
let mentions = extract_mentions(&inner.content);
|
||||||
|
let replies_to = extract_reply_ids(&inner.tags);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
// Event ID must be known
|
||||||
|
id: inner.id.unwrap(),
|
||||||
|
author: inner.pubkey,
|
||||||
|
content: inner.content.into(),
|
||||||
|
created_at: inner.created_at,
|
||||||
|
mentions,
|
||||||
|
replies_to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Box<Event>> for RenderedMessage {
|
||||||
|
fn from(inner: Box<Event>) -> Self {
|
||||||
|
(*inner).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Box<Event>> for RenderedMessage {
|
||||||
|
fn from(inner: &Box<Event>) -> Self {
|
||||||
|
inner.to_owned().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for RenderedMessage {}
|
||||||
|
|
||||||
|
impl PartialEq for RenderedMessage {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.id == other.id
|
self.id == other.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ord for Message {
|
impl Ord for RenderedMessage {
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
self.created_at.cmp(&other.created_at)
|
self.created_at.cmp(&other.created_at)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for Message {
|
impl PartialOrd for RenderedMessage {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
Some(self.cmp(other))
|
Some(self.cmp(other))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hash for Message {
|
impl Hash for RenderedMessage {
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
self.id.hash(state);
|
self.id.hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder pattern implementation for constructing Message objects.
|
impl RenderedMessage {
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MessageBuilder {
|
|
||||||
id: EventId,
|
|
||||||
author: PublicKey,
|
|
||||||
content: Option<SharedString>,
|
|
||||||
created_at: Option<Timestamp>,
|
|
||||||
mentions: Vec<PublicKey>,
|
|
||||||
replies_to: Option<Vec<EventId>>,
|
|
||||||
errors: Option<Vec<SendError>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBuilder {
|
|
||||||
/// Creates a new MessageBuilder with default values
|
|
||||||
pub fn new(id: EventId, author: PublicKey) -> Self {
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
author,
|
|
||||||
content: None,
|
|
||||||
created_at: None,
|
|
||||||
mentions: vec![],
|
|
||||||
replies_to: None,
|
|
||||||
errors: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the message content
|
|
||||||
pub fn content(mut self, content: impl Into<SharedString>) -> Self {
|
|
||||||
self.content = Some(content.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the creation timestamp
|
|
||||||
pub fn created_at(mut self, created_at: Timestamp) -> Self {
|
|
||||||
self.created_at = Some(created_at);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a single mention to the message
|
|
||||||
pub fn mention(mut self, mention: PublicKey) -> Self {
|
|
||||||
self.mentions.push(mention);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds multiple mentions to the message
|
|
||||||
pub fn mentions<I>(mut self, mentions: I) -> Self
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = PublicKey>,
|
|
||||||
{
|
|
||||||
self.mentions.extend(mentions);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a single message this is replying to
|
|
||||||
pub fn reply_to(mut self, reply_to: EventId) -> Self {
|
|
||||||
self.replies_to = Some(vec![reply_to]);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets multiple messages this is replying to
|
|
||||||
pub fn replies_to<I>(mut self, replies_to: I) -> Self
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = EventId>,
|
|
||||||
{
|
|
||||||
let replies: Vec<EventId> = replies_to.into_iter().collect();
|
|
||||||
if !replies.is_empty() {
|
|
||||||
self.replies_to = Some(replies);
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds errors that occurred during sending
|
|
||||||
pub fn errors<I>(mut self, errors: I) -> Self
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = SendError>,
|
|
||||||
{
|
|
||||||
self.errors = Some(errors.into_iter().collect());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds the message
|
|
||||||
pub fn build(self) -> Result<Message, String> {
|
|
||||||
Ok(Message {
|
|
||||||
id: self.id,
|
|
||||||
author: self.author,
|
|
||||||
content: self.content.ok_or("Content is required")?,
|
|
||||||
created_at: self.created_at.unwrap_or_else(Timestamp::now),
|
|
||||||
mentions: self.mentions,
|
|
||||||
replies_to: self.replies_to,
|
|
||||||
errors: self.errors,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Message {
|
|
||||||
/// Creates a new MessageBuilder
|
|
||||||
pub fn builder(id: EventId, author: PublicKey) -> MessageBuilder {
|
|
||||||
MessageBuilder::new(id, author)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a human-readable string representing how long ago the message was created
|
/// Returns a human-readable string representing how long ago the message was created
|
||||||
pub fn ago(&self) -> SharedString {
|
pub fn ago(&self) -> SharedString {
|
||||||
let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) {
|
let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) {
|
||||||
@@ -177,3 +113,41 @@ impl Message {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extract_mentions(content: &str) -> Vec<PublicKey> {
|
||||||
|
let parser = NostrParser::new();
|
||||||
|
let tokens = parser.parse(content);
|
||||||
|
|
||||||
|
tokens
|
||||||
|
.filter_map(|token| match token {
|
||||||
|
Token::Nostr(nip21) => match nip21 {
|
||||||
|
Nip21::Pubkey(pubkey) => Some(pubkey),
|
||||||
|
Nip21::Profile(profile) => Some(profile.public_key),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_reply_ids(inner: &Tags) -> Vec<EventId> {
|
||||||
|
let mut replies_to = vec![];
|
||||||
|
|
||||||
|
for tag in inner.filter(TagKind::e()) {
|
||||||
|
if let Some(content) = tag.content() {
|
||||||
|
if let Ok(id) = EventId::from_hex(content) {
|
||||||
|
replies_to.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for tag in inner.filter(TagKind::q()) {
|
||||||
|
if let Some(content) = tag.content() {
|
||||||
|
if let Ok(id) = EventId::from_hex(content) {
|
||||||
|
replies_to.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
replies_to
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ use chrono::{Local, TimeZone};
|
|||||||
use common::display::DisplayProfile;
|
use common::display::DisplayProfile;
|
||||||
use common::event::EventUtils;
|
use common::event::EventUtils;
|
||||||
use global::nostr_client;
|
use global::nostr_client;
|
||||||
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window};
|
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::message::Message;
|
|
||||||
use crate::Registry;
|
use crate::Registry;
|
||||||
|
|
||||||
pub(crate) const NOW: &str = "now";
|
pub(crate) const NOW: &str = "now";
|
||||||
@@ -20,15 +19,58 @@ pub(crate) const HOURS_IN_DAY: i64 = 24;
|
|||||||
pub(crate) const DAYS_IN_MONTH: i64 = 30;
|
pub(crate) const DAYS_IN_MONTH: i64 = 30;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum RoomSignal {
|
pub struct SendReport {
|
||||||
NewMessage(Message),
|
pub receiver: PublicKey,
|
||||||
Refresh,
|
pub output: Option<Output<EventId>>,
|
||||||
|
pub local_error: Option<SharedString>,
|
||||||
|
pub nip17_relays_not_found: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
impl SendReport {
|
||||||
pub struct SendError {
|
pub fn output(receiver: PublicKey, output: Output<EventId>) -> Self {
|
||||||
pub profile: Profile,
|
Self {
|
||||||
pub message: SharedString,
|
receiver,
|
||||||
|
output: Some(output),
|
||||||
|
local_error: None,
|
||||||
|
nip17_relays_not_found: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error(receiver: PublicKey, error: impl Into<SharedString>) -> Self {
|
||||||
|
Self {
|
||||||
|
receiver,
|
||||||
|
output: None,
|
||||||
|
local_error: Some(error.into()),
|
||||||
|
nip17_relays_not_found: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nip17_relays_not_found(receiver: PublicKey) -> Self {
|
||||||
|
Self {
|
||||||
|
receiver,
|
||||||
|
output: None,
|
||||||
|
local_error: None,
|
||||||
|
nip17_relays_not_found: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_relay_error(&self) -> bool {
|
||||||
|
self.local_error.is_some() || self.nip17_relays_not_found
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_sent_success(&self) -> bool {
|
||||||
|
if let Some(output) = self.output.as_ref() {
|
||||||
|
!output.success.is_empty()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum RoomSignal {
|
||||||
|
NewMessage(Box<Event>),
|
||||||
|
Refresh,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
|
#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
|
||||||
@@ -343,201 +385,80 @@ impl Room {
|
|||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// A Task that resolves to Result<Vec<RoomMessage>, Error> containing all messages for this room
|
/// A Task that resolves to Result<Vec<Event>, Error> containing all messages for this room
|
||||||
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<Message>, Error>> {
|
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<Event>, Error>> {
|
||||||
let pubkeys = self.members.clone();
|
let members = self.members.clone();
|
||||||
|
let members_clone = members.clone();
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::PrivateDirectMessage)
|
|
||||||
.authors(self.members.clone())
|
|
||||||
.pubkeys(self.members.clone());
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let mut messages = vec![];
|
let client = nostr_client();
|
||||||
let parser = NostrParser::new();
|
let signer = client.signer().await?;
|
||||||
let database = nostr_client().database();
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
// Get all events from database
|
let send = Filter::new()
|
||||||
let events = database
|
.kind(Kind::PrivateDirectMessage)
|
||||||
.query(filter)
|
.author(public_key)
|
||||||
.await?
|
.pubkeys(members.clone());
|
||||||
|
|
||||||
|
let recv = Filter::new()
|
||||||
|
.kind(Kind::PrivateDirectMessage)
|
||||||
|
.authors(members)
|
||||||
|
.pubkey(public_key);
|
||||||
|
|
||||||
|
let send_events = client.database().query(send).await?;
|
||||||
|
let recv_events = client.database().query(recv).await?;
|
||||||
|
|
||||||
|
let events = send_events
|
||||||
|
.merge(recv_events)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.sorted_by_key(|ev| ev.created_at)
|
.sorted_by_key(|ev| ev.created_at)
|
||||||
.filter(|ev| ev.compare_pubkeys(&pubkeys))
|
.filter(|ev| ev.compare_pubkeys(&members_clone))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for event in events.into_iter() {
|
Ok(events)
|
||||||
let content = event.content.clone();
|
|
||||||
let tokens = parser.parse(&content);
|
|
||||||
let mut replies_to = vec![];
|
|
||||||
|
|
||||||
for tag in event.tags.filter(TagKind::e()) {
|
|
||||||
if let Some(content) = tag.content() {
|
|
||||||
if let Ok(id) = EventId::from_hex(content) {
|
|
||||||
replies_to.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for tag in event.tags.filter(TagKind::q()) {
|
|
||||||
if let Some(content) = tag.content() {
|
|
||||||
if let Ok(id) = EventId::from_hex(content) {
|
|
||||||
replies_to.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mentions = tokens
|
|
||||||
.filter_map(|token| match token {
|
|
||||||
Token::Nostr(nip21) => match nip21 {
|
|
||||||
Nip21::Pubkey(pubkey) => Some(pubkey),
|
|
||||||
Nip21::Profile(profile) => Some(profile.public_key),
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if let Ok(message) = Message::builder(event.id, event.pubkey)
|
|
||||||
.content(content)
|
|
||||||
.created_at(event.created_at)
|
|
||||||
.replies_to(replies_to)
|
|
||||||
.mentions(mentions)
|
|
||||||
.build()
|
|
||||||
{
|
|
||||||
messages.push(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(messages)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Emits a message event to the GPUI
|
/// Emits a new message signal to the current room
|
||||||
///
|
pub fn emit_message(&self, event: Event, cx: &mut Context<Self>) {
|
||||||
/// # Arguments
|
cx.emit(RoomSignal::NewMessage(Box::new(event)));
|
||||||
///
|
|
||||||
/// * `event` - The Nostr event to emit
|
|
||||||
/// * `window` - The Window to emit the event to
|
|
||||||
/// * `cx` - The context for the room
|
|
||||||
///
|
|
||||||
/// # Effects
|
|
||||||
///
|
|
||||||
/// Processes the event and emits an Incoming to the UI when complete
|
|
||||||
pub fn emit_message(&self, event: Event, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
// Extract all mentions from content
|
|
||||||
let mentions = extract_mentions(&event.content);
|
|
||||||
|
|
||||||
// Extract reply_to if present
|
|
||||||
let mut replies_to = vec![];
|
|
||||||
|
|
||||||
for tag in event.tags.filter(TagKind::e()) {
|
|
||||||
if let Some(content) = tag.content() {
|
|
||||||
if let Ok(id) = EventId::from_hex(content) {
|
|
||||||
replies_to.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for tag in event.tags.filter(TagKind::q()) {
|
|
||||||
if let Some(content) = tag.content() {
|
|
||||||
if let Ok(id) = EventId::from_hex(content) {
|
|
||||||
replies_to.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(message) = Message::builder(event.id, event.pubkey)
|
|
||||||
.content(event.content)
|
|
||||||
.created_at(event.created_at)
|
|
||||||
.replies_to(replies_to)
|
|
||||||
.mentions(mentions)
|
|
||||||
.build()
|
|
||||||
{
|
|
||||||
cx.emit(RoomSignal::NewMessage(message));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Emits a signal to refresh the current room's messages.
|
/// Emits a signal to refresh the current room's messages.
|
||||||
pub fn emit_refresh(&mut self, cx: &mut Context<Self>) {
|
pub fn emit_refresh(&mut self, cx: &mut Context<Self>) {
|
||||||
cx.emit(RoomSignal::Refresh);
|
cx.emit(RoomSignal::Refresh);
|
||||||
log::info!("refresh room: {}", self.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a temporary message for optimistic updates
|
/// Creates a temporary message for optimistic updates
|
||||||
///
|
///
|
||||||
/// This constructs an unsigned message with the current user as the author,
|
/// The event must not been published to relays.
|
||||||
/// extracts any mentions from the content, and packages it as a Message struct.
|
|
||||||
/// The message will have a generated ID but hasn't been published to relays.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `content` - The message content text
|
|
||||||
/// * `cx` - The application context containing user profile information
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// Returns `Some(Message)` containing the temporary message if the current user's profile is available,
|
|
||||||
/// or `None` if no account is found.
|
|
||||||
pub fn create_temp_message(
|
pub fn create_temp_message(
|
||||||
&self,
|
&self,
|
||||||
public_key: PublicKey,
|
receiver: PublicKey,
|
||||||
content: &str,
|
content: &str,
|
||||||
replies: Option<&Vec<Message>>,
|
replies: &[EventId],
|
||||||
) -> Option<Message> {
|
) -> UnsignedEvent {
|
||||||
let builder = EventBuilder::private_msg_rumor(public_key, content);
|
let builder = EventBuilder::private_msg_rumor(receiver, content);
|
||||||
|
let mut tags = vec![];
|
||||||
|
|
||||||
// Add event reference if it's present (replying to another event)
|
// Add event reference if it's present (replying to another event)
|
||||||
let mut refs = vec![];
|
if replies.len() == 1 {
|
||||||
|
tags.push(Tag::event(replies[0]))
|
||||||
if let Some(replies) = replies {
|
} else {
|
||||||
if replies.len() == 1 {
|
for id in replies.iter() {
|
||||||
refs.push(Tag::event(replies[0].id))
|
tags.push(Tag::from_standardized(TagStandard::Quote {
|
||||||
} else {
|
event_id: id.to_owned(),
|
||||||
for message in replies.iter() {
|
relay_url: None,
|
||||||
refs.push(Tag::custom(TagKind::q(), vec![message.id]))
|
public_key: None,
|
||||||
}
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut event = if !refs.is_empty() {
|
let mut event = builder.tags(tags).build(receiver);
|
||||||
builder.tags(refs).build(public_key)
|
// Ensure event ID is set
|
||||||
} else {
|
|
||||||
builder.build(public_key)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a unsigned event to convert to Coop Message
|
|
||||||
event.ensure_id();
|
event.ensure_id();
|
||||||
|
|
||||||
// Extract all mentions from content
|
event
|
||||||
let mentions = extract_mentions(&event.content);
|
|
||||||
|
|
||||||
// Extract reply_to if present
|
|
||||||
let mut replies_to = vec![];
|
|
||||||
|
|
||||||
for tag in event.tags.filter(TagKind::e()) {
|
|
||||||
if let Some(content) = tag.content() {
|
|
||||||
if let Ok(id) = EventId::from_hex(content) {
|
|
||||||
replies_to.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for tag in event.tags.filter(TagKind::q()) {
|
|
||||||
if let Some(content) = tag.content() {
|
|
||||||
if let Ok(id) = EventId::from_hex(content) {
|
|
||||||
replies_to.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::builder(event.id.unwrap(), public_key)
|
|
||||||
.content(event.content)
|
|
||||||
.created_at(event.created_at)
|
|
||||||
.replies_to(replies_to)
|
|
||||||
.mentions(mentions)
|
|
||||||
.build()
|
|
||||||
.ok()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a message to all members in the background task
|
/// Sends a message to all members in the background task
|
||||||
@@ -554,12 +475,11 @@ impl Room {
|
|||||||
pub fn send_in_background(
|
pub fn send_in_background(
|
||||||
&self,
|
&self,
|
||||||
content: &str,
|
content: &str,
|
||||||
replies: Option<&Vec<Message>>,
|
replies: Vec<EventId>,
|
||||||
backup: bool,
|
backup: bool,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Task<Result<Vec<SendError>, Error>> {
|
) -> Task<Result<Vec<SendReport>, Error>> {
|
||||||
let content = content.to_owned();
|
let content = content.to_owned();
|
||||||
let replies = replies.cloned();
|
|
||||||
let subject = self.subject.clone();
|
let subject = self.subject.clone();
|
||||||
let picture = self.picture.clone();
|
let picture = self.picture.clone();
|
||||||
let public_keys = self.members.clone();
|
let public_keys = self.members.clone();
|
||||||
@@ -569,8 +489,7 @@ impl Room {
|
|||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
let mut reports = vec![];
|
let mut tags = public_keys
|
||||||
let mut tags: Vec<Tag> = public_keys
|
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|pubkey| {
|
.filter_map(|pubkey| {
|
||||||
if pubkey != &public_key {
|
if pubkey != &public_key {
|
||||||
@@ -579,16 +498,18 @@ impl Room {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect_vec();
|
||||||
|
|
||||||
// Add event reference if it's present (replying to another event)
|
// Add event reference if it's present (replying to another event)
|
||||||
if let Some(replies) = replies {
|
if replies.len() == 1 {
|
||||||
if replies.len() == 1 {
|
tags.push(Tag::event(replies[0]))
|
||||||
tags.push(Tag::event(replies[0].id))
|
} else {
|
||||||
} else {
|
for id in replies.iter() {
|
||||||
for message in replies.iter() {
|
tags.push(Tag::from_standardized(TagStandard::Quote {
|
||||||
tags.push(Tag::custom(TagKind::q(), vec![message.id]))
|
event_id: id.to_owned(),
|
||||||
}
|
relay_url: None,
|
||||||
|
public_key: None,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,43 +529,43 @@ impl Room {
|
|||||||
return Err(anyhow!("Something is wrong. Cannot get receivers list."));
|
return Err(anyhow!("Something is wrong. Cannot get receivers list."));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Stored all send errors
|
||||||
|
let mut reports = vec![];
|
||||||
|
|
||||||
for receiver in receivers.iter() {
|
for receiver in receivers.iter() {
|
||||||
if let Err(e) = client
|
match client
|
||||||
.send_private_msg(*receiver, &content, tags.clone())
|
.send_private_msg(*receiver, &content, tags.clone())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
let metadata = client
|
Ok(output) => {
|
||||||
.database()
|
reports.push(SendReport::output(*receiver, output));
|
||||||
.metadata(*receiver)
|
}
|
||||||
.await?
|
Err(e) => {
|
||||||
.unwrap_or_default();
|
if let nostr_sdk::client::Error::PrivateMsgRelaysNotFound = e {
|
||||||
let profile = Profile::new(*receiver, metadata);
|
reports.push(SendReport::nip17_relays_not_found(*receiver));
|
||||||
let report = SendError {
|
} else {
|
||||||
profile,
|
reports.push(SendReport::error(*receiver, e.to_string()));
|
||||||
message: e.to_string().into(),
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
reports.push(report);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only send a backup message to current user if there are no issues when sending to others
|
// Only send a backup message to current user if sent successfully to others
|
||||||
if backup && reports.is_empty() {
|
if reports.iter().all(|r| r.is_sent_success()) && backup {
|
||||||
if let Err(e) = client
|
match client
|
||||||
.send_private_msg(*current_user, &content, tags.clone())
|
.send_private_msg(*current_user, &content, tags.clone())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
let metadata = client
|
Ok(output) => {
|
||||||
.database()
|
reports.push(SendReport::output(*current_user, output));
|
||||||
.metadata(*current_user)
|
}
|
||||||
.await?
|
Err(e) => {
|
||||||
.unwrap_or_default();
|
if let nostr_sdk::client::Error::PrivateMsgRelaysNotFound = e {
|
||||||
let profile = Profile::new(*current_user, metadata);
|
reports.push(SendReport::nip17_relays_not_found(*current_user));
|
||||||
let report = SendError {
|
} else {
|
||||||
profile,
|
reports.push(SendReport::error(*current_user, e.to_string()));
|
||||||
message: e.to_string().into(),
|
}
|
||||||
};
|
}
|
||||||
reports.push(report);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,19 +573,3 @@ impl Room {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn extract_mentions(content: &str) -> Vec<PublicKey> {
|
|
||||||
let parser = NostrParser::new();
|
|
||||||
let tokens = parser.parse(content);
|
|
||||||
|
|
||||||
tokens
|
|
||||||
.filter_map(|token| match token {
|
|
||||||
Token::Nostr(nip21) => match nip21 {
|
|
||||||
Nip21::Pubkey(pubkey) => Some(pubkey),
|
|
||||||
Nip21::Profile(profile) => Some(profile.public_key),
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ pub enum IconName {
|
|||||||
Forward,
|
Forward,
|
||||||
Search,
|
Search,
|
||||||
SearchFill,
|
SearchFill,
|
||||||
|
Sent,
|
||||||
Settings,
|
Settings,
|
||||||
SortAscending,
|
SortAscending,
|
||||||
SortDescending,
|
SortDescending,
|
||||||
@@ -133,6 +134,7 @@ impl IconName {
|
|||||||
Self::Forward => "icons/forward.svg",
|
Self::Forward => "icons/forward.svg",
|
||||||
Self::Search => "icons/search.svg",
|
Self::Search => "icons/search.svg",
|
||||||
Self::SearchFill => "icons/search-fill.svg",
|
Self::SearchFill => "icons/search-fill.svg",
|
||||||
|
Self::Sent => "icons/sent.svg",
|
||||||
Self::Settings => "icons/settings.svg",
|
Self::Settings => "icons/settings.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",
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ type CustomRangeTooltipFn =
|
|||||||
Option<Arc<dyn Fn(usize, Range<usize>, &mut Window, &mut App) -> Option<AnyView>>>;
|
Option<Arc<dyn Fn(usize, Range<usize>, &mut Window, &mut App) -> Option<AnyView>>>;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct RichText {
|
pub struct RenderedText {
|
||||||
pub text: SharedString,
|
pub text: SharedString,
|
||||||
pub highlights: Vec<(Range<usize>, Highlight)>,
|
pub highlights: Vec<(Range<usize>, Highlight)>,
|
||||||
pub link_ranges: Vec<Range<usize>>,
|
pub link_ranges: Vec<Range<usize>>,
|
||||||
@@ -63,7 +63,7 @@ pub struct RichText {
|
|||||||
custom_ranges_tooltip_fn: CustomRangeTooltipFn,
|
custom_ranges_tooltip_fn: CustomRangeTooltipFn,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RichText {
|
impl RenderedText {
|
||||||
pub fn new(content: &str, cx: &App) -> Self {
|
pub fn new(content: &str, cx: &App) -> Self {
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
let mut highlights = Vec::new();
|
let mut highlights = Vec::new();
|
||||||
@@ -81,7 +81,7 @@ impl RichText {
|
|||||||
|
|
||||||
text.truncate(text.trim_end().len());
|
text.truncate(text.trim_end().len());
|
||||||
|
|
||||||
RichText {
|
RenderedText {
|
||||||
text: SharedString::from(text),
|
text: SharedString::from(text),
|
||||||
link_urls: link_urls.into(),
|
link_urls: link_urls.into(),
|
||||||
link_ranges,
|
link_ranges,
|
||||||
@@ -98,7 +98,7 @@ impl RichText {
|
|||||||
self.custom_ranges_tooltip_fn = Some(Arc::new(f));
|
self.custom_ranges_tooltip_fn = Some(Arc::new(f));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn element(&self, id: ElementId, window: &mut Window, cx: &App) -> AnyElement {
|
pub fn element(&self, id: ElementId, window: &Window, cx: &App) -> AnyElement {
|
||||||
let link_color = cx.theme().text_accent;
|
let link_color = cx.theme().text_accent;
|
||||||
|
|
||||||
InteractiveText::new(
|
InteractiveText::new(
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ compose:
|
|||||||
en: "Subject:"
|
en: "Subject:"
|
||||||
|
|
||||||
chat:
|
chat:
|
||||||
private_conversation_notice:
|
notice:
|
||||||
en: "This conversation is private. Only members can see each other's messages."
|
en: "This conversation is private. Only members can see each other's messages."
|
||||||
placeholder:
|
placeholder:
|
||||||
en: "Message..."
|
en: "Message..."
|
||||||
@@ -303,12 +303,18 @@ chat:
|
|||||||
en: "Change the subject of the conversation"
|
en: "Change the subject of the conversation"
|
||||||
replying_to_label:
|
replying_to_label:
|
||||||
en: "Replying to:"
|
en: "Replying to:"
|
||||||
send_fail:
|
sent_to:
|
||||||
|
en: "Sent to:"
|
||||||
|
sent:
|
||||||
|
en: "• Sent"
|
||||||
|
sent_failed:
|
||||||
en: "Failed to send message. Click to see details."
|
en: "Failed to send message. Click to see details."
|
||||||
logs_title:
|
sent_success:
|
||||||
en: "Error Logs"
|
en: "Successfully"
|
||||||
send_to_label:
|
reports:
|
||||||
en: "Send to:"
|
en: "Sent Reports"
|
||||||
|
nip17_not_found:
|
||||||
|
en: "%{u} has not set up Messaging Relays, so they won't receive your message."
|
||||||
|
|
||||||
sidebar:
|
sidebar:
|
||||||
find_or_start_conversation:
|
find_or_start_conversation:
|
||||||
|
|||||||
Reference in New Issue
Block a user