group message

This commit is contained in:
2026-06-10 14:37:50 +07:00
parent e812ae05a9
commit 5edc8d311e
3 changed files with 244 additions and 284 deletions

72
Cargo.lock generated
View File

@@ -247,9 +247,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "ar_archive_writer" name = "ar_archive_writer"
version = "0.5.1" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348"
dependencies = [ dependencies = [
"object", "object",
] ]
@@ -1369,7 +1369,7 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"gpui_util", "gpui_util",
"indexmap", "indexmap",
@@ -1891,7 +1891,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#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2865,7 +2865,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.2.2" version = "0.2.2"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"anyhow", "anyhow",
@@ -2947,7 +2947,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_linux" name = "gpui_linux"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"accesskit_unix", "accesskit_unix",
@@ -2998,7 +2998,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_macos" name = "gpui_macos"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"accesskit_macos", "accesskit_macos",
@@ -3043,7 +3043,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#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -3054,7 +3054,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_platform" name = "gpui_platform"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"gpui", "gpui",
@@ -3067,7 +3067,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_shared_string" name = "gpui_shared_string"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"schemars", "schemars",
"serde", "serde",
@@ -3077,7 +3077,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#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gpui", "gpui",
@@ -3088,7 +3088,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_util" name = "gpui_util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"log", "log",
@@ -3097,7 +3097,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_web" name = "gpui_web"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"console_error_panic_hook", "console_error_panic_hook",
@@ -3121,7 +3121,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_wgpu" name = "gpui_wgpu"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytemuck", "bytemuck",
@@ -3150,7 +3150,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_windows" name = "gpui_windows"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"accesskit_windows", "accesskit_windows",
@@ -3439,7 +3439,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#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-compression", "async-compression",
@@ -3464,7 +3464,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#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"rustls", "rustls",
"rustls-platform-verifier", "rustls-platform-verifier",
@@ -4322,7 +4322,7 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen", "bindgen",
@@ -5225,7 +5225,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "perf" name = "perf"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"collections", "collections",
"serde", "serde",
@@ -6018,16 +6018,16 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"derive_refineable", "derive_refineable",
] ]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.12.3" version = "1.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -6048,9 +6048,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.10" version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]] [[package]]
name = "relay_auth" name = "relay_auth"
@@ -6118,7 +6118,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#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -6433,7 +6433,7 @@ dependencies = [
[[package]] [[package]]
name = "scheduler" name = "scheduler"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"async-task", "async-task",
"backtrace", "backtrace",
@@ -7077,7 +7077,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#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"heapless 0.9.3", "heapless 0.9.3",
"log", "log",
@@ -8071,7 +8071,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "util" name = "util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-fs", "async-fs",
@@ -8110,7 +8110,7 @@ dependencies = [
[[package]] [[package]]
name = "util_macros" name = "util_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"perf", "perf",
"quote", "quote",
@@ -9875,18 +9875,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.50" version = "0.8.52"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.50" version = "0.8.52"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -9970,7 +9970,7 @@ dependencies = [
[[package]] [[package]]
name = "zlog" name = "zlog"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -9987,7 +9987,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]] [[package]]
name = "ztracing" name = "ztracing"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
dependencies = [ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
@@ -9998,7 +9998,7 @@ dependencies = [
[[package]] [[package]]
name = "ztracing_macro" name = "ztracing_macro"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c"
[[package]] [[package]]
name = "zune-core" name = "zune-core"

View File

@@ -5,6 +5,106 @@ use common::{EventExt, NostrParser, extract_and_remove_media_urls};
use gpui::{SharedString, SharedUri}; use gpui::{SharedString, SharedUri};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
/// Rendered message.
#[derive(Debug, Clone)]
pub struct Message {
pub id: EventId,
/// Author's public key
pub author: PublicKey,
/// The content/text of the message
pub content: String,
/// List of media URLs in the message
pub media: Vec<SharedUri>,
/// Message created time as unix timestamp
pub created_at: Timestamp,
/// List of mentioned public keys in the message
pub mentions: Vec<Mention>,
/// List of event of the message this message is a reply to
pub replies_to: Vec<EventId>,
}
impl From<&Event> for Message {
fn from(val: &Event) -> Self {
let mentions = extract_mentions(&val.content);
let replies_to = extract_reply_ids(&val.tags);
let (media, string) = extract_and_remove_media_urls(&val.content);
Self {
id: val.id,
author: val.pubkey,
content: string,
media,
created_at: val.created_at,
mentions,
replies_to,
}
}
}
impl From<&UnsignedEvent> for Message {
fn from(val: &UnsignedEvent) -> Self {
let mentions = extract_mentions(&val.content);
let replies_to = extract_reply_ids(&val.tags);
let (media, string) = extract_and_remove_media_urls(&val.content);
Self {
// Event ID must be known
id: val.id.unwrap(),
author: val.pubkey,
content: string,
media,
created_at: val.created_at,
mentions,
replies_to,
}
}
}
impl From<&NewMessage> for Message {
fn from(val: &NewMessage) -> Self {
let mentions = extract_mentions(&val.rumor.content);
let replies_to = extract_reply_ids(&val.rumor.tags);
let (media, string) = extract_and_remove_media_urls(&val.rumor.content);
Self {
// Event ID must be known
id: val.rumor.id.unwrap(),
author: val.rumor.pubkey,
content: string,
media,
created_at: val.rumor.created_at,
mentions,
replies_to,
}
}
}
impl Eq for Message {}
impl PartialEq for Message {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Ord for Message {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.created_at.cmp(&other.created_at)
}
}
impl PartialOrd for Message {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Hash for Message {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
/// New message. /// New message.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct NewMessage { pub struct NewMessage {
@@ -44,74 +144,6 @@ impl FailedMessage {
} }
} }
/// Message.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Message {
User(RenderedMessage),
Warning(String, Timestamp),
System(Timestamp),
}
impl Message {
pub fn user<I>(user: I) -> Self
where
I: Into<RenderedMessage>,
{
Self::User(user.into())
}
pub fn warning<I>(content: I) -> Self
where
I: Into<String>,
{
Self::Warning(content.into(), Timestamp::now())
}
pub fn system() -> Self {
Self::System(Timestamp::default())
}
fn timestamp(&self) -> &Timestamp {
match self {
Message::User(msg) => &msg.created_at,
Message::Warning(_, ts) => ts,
Message::System(ts) => ts,
}
}
}
impl From<&NewMessage> for Message {
fn from(val: &NewMessage) -> Self {
Self::User(val.into())
}
}
impl From<&UnsignedEvent> for Message {
fn from(val: &UnsignedEvent) -> Self {
Self::User(val.into())
}
}
impl Ord for Message {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
// System always comes first
(Message::System(_), Message::System(_)) => self.timestamp().cmp(other.timestamp()),
(Message::System(_), _) => std::cmp::Ordering::Less,
(_, Message::System(_)) => std::cmp::Ordering::Greater,
// For non-system messages, compare by timestamp
_ => self.timestamp().cmp(other.timestamp()),
}
}
}
impl PartialOrd for Message {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Mention { pub struct Mention {
pub public_key: PublicKey, pub public_key: PublicKey,
@@ -124,106 +156,6 @@ impl Mention {
} }
} }
/// Rendered message.
#[derive(Debug, Clone)]
pub struct RenderedMessage {
pub id: EventId,
/// Author's public key
pub author: PublicKey,
/// The content/text of the message
pub content: String,
/// List of media URLs in the message
pub media: Vec<SharedUri>,
/// Message created time as unix timestamp
pub created_at: Timestamp,
/// List of mentioned public keys in the message
pub mentions: Vec<Mention>,
/// List of event of the message this message is a reply to
pub replies_to: Vec<EventId>,
}
impl From<&Event> for RenderedMessage {
fn from(val: &Event) -> Self {
let mentions = extract_mentions(&val.content);
let replies_to = extract_reply_ids(&val.tags);
let (media, string) = extract_and_remove_media_urls(&val.content);
Self {
id: val.id,
author: val.pubkey,
content: string,
media,
created_at: val.created_at,
mentions,
replies_to,
}
}
}
impl From<&UnsignedEvent> for RenderedMessage {
fn from(val: &UnsignedEvent) -> Self {
let mentions = extract_mentions(&val.content);
let replies_to = extract_reply_ids(&val.tags);
let (media, string) = extract_and_remove_media_urls(&val.content);
Self {
// Event ID must be known
id: val.id.unwrap(),
author: val.pubkey,
content: string,
media,
created_at: val.created_at,
mentions,
replies_to,
}
}
}
impl From<&NewMessage> for RenderedMessage {
fn from(val: &NewMessage) -> Self {
let mentions = extract_mentions(&val.rumor.content);
let replies_to = extract_reply_ids(&val.rumor.tags);
let (media, string) = extract_and_remove_media_urls(&val.rumor.content);
Self {
// Event ID must be known
id: val.rumor.id.unwrap(),
author: val.rumor.pubkey,
content: string,
media,
created_at: val.rumor.created_at,
mentions,
replies_to,
}
}
}
impl Eq for RenderedMessage {}
impl PartialEq for RenderedMessage {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Ord for RenderedMessage {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.created_at.cmp(&other.created_at)
}
}
impl PartialOrd for RenderedMessage {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Hash for RenderedMessage {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
/// Extracts all mentions (public keys) from a content string. /// Extracts all mentions (public keys) from a content string.
fn extract_mentions(content: &str) -> Vec<Mention> { fn extract_mentions(content: &str) -> Vec<Mention> {
let parser = NostrParser::new(); let parser = NostrParser::new();

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
pub use actions::*; pub use actions::*;
use anyhow::{Context as AnyhowContext, Error}; use anyhow::{Context as AnyhowContext, Error};
use chat::{ChatRegistry, Message, RenderedMessage, Room, RoomEvent, SendReport, SendStatus}; use chat::{ChatRegistry, Message, Room, RoomEvent, SendReport, SendStatus};
use common::{TimestampExt, coop_cache}; use common::{TimestampExt, coop_cache};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
@@ -38,9 +38,6 @@ use crate::text::RenderedText;
mod actions; mod actions;
mod text; mod text;
const ANNOUNCEMENT: &str =
"This conversation is private. Only members can see each other's messages.";
pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> { pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
cx.new(|cx| ChatPanel::new(room, window, cx)) cx.new(|cx| ChatPanel::new(room, window, cx))
} }
@@ -101,7 +98,7 @@ impl ChatPanel {
let reports_by_id = cx.new(|_| BTreeMap::new()); let reports_by_id = cx.new(|_| BTreeMap::new());
// Define list of messages // Define list of messages
let messages = BTreeSet::from([Message::system()]); let messages = BTreeSet::default();
let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.)); let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
// Get room id and name // Get room id and name
@@ -476,25 +473,13 @@ impl ChatPanel {
} }
/// Get a message by its ID /// Get a message by its ID
fn message(&self, id: &EventId) -> Option<&RenderedMessage> { fn message(&self, id: &EventId) -> Option<&Message> {
self.messages.iter().find_map(|msg| { self.messages.iter().find(|msg| &msg.id == id)
if let Message::User(rendered) = msg
&& &rendered.id == id
{
return Some(rendered);
}
None
})
} }
fn scroll_to(&self, id: EventId) { /// Scroll to a message by its ID
if let Some(ix) = self.messages.iter().position(|m| { fn scroll_to(&self, id: &EventId) {
if let Message::User(msg) = m { if let Some(ix) = self.messages.iter().position(|msg| &msg.id == id) {
msg.id == id
} else {
false
}
}) {
self.list_state.scroll_to_reveal_item(ix); self.list_state.scroll_to_reveal_item(ix);
} }
} }
@@ -742,9 +727,11 @@ impl ChatPanel {
cx.open_url(&content); cx.open_url(&content);
} }
fn render_announcement(&self, ix: usize, cx: &Context<Self>) -> AnyElement { fn render_announcement(&self, cx: &Context<Self>) -> AnyElement {
const MSG: &str =
"This conversation is private. Only members can see each other's messages.";
v_flex() v_flex()
.id(ix)
.h_40() .h_40()
.w_full() .w_full()
.gap_3() .gap_3()
@@ -761,7 +748,7 @@ impl ChatPanel {
.size_12() .size_12()
.text_color(cx.theme().ghost_element_active), .text_color(cx.theme().ghost_element_active),
) )
.child(SharedString::from(ANNOUNCEMENT)) .child(MSG)
.into_any_element() .into_any_element()
} }
@@ -798,6 +785,34 @@ impl ChatPanel {
.into_any_element() .into_any_element()
} }
fn is_group_start(&self, ix: usize) -> bool {
// 5 minutes
const GROUP_WINDOW: u64 = 300;
if ix == 0 {
return true;
}
let mut iter = self.messages.iter();
if let Some(previous) = iter.nth(ix - 1)
&& let Some(current) = iter.next()
{
if current.author != previous.author {
return true;
}
let gap = current
.created_at
.as_secs()
.saturating_sub(previous.created_at.as_secs());
return gap > GROUP_WINDOW;
}
false
}
fn render_message( fn render_message(
&mut self, &mut self,
ix: usize, ix: usize,
@@ -805,24 +820,17 @@ impl ChatPanel {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> AnyElement { ) -> AnyElement {
if let Some(message) = self.messages.iter().nth(ix) { if let Some(message) = self.messages.iter().nth(ix) {
match message { let persons = PersonRegistry::global(cx);
Message::User(rendered) => { let show_author = self.is_group_start(ix);
let persons = PersonRegistry::global(cx); let text = self
let text = self .rendered_texts_by_id
.rendered_texts_by_id .entry(message.id)
.entry(rendered.id) .or_insert_with(|| {
.or_insert_with(|| { RenderedText::new(&message.content, &message.mentions, &persons, cx)
RenderedText::new(&rendered.content, &rendered.mentions, &persons, cx) })
}) .element(ix.into(), window, cx);
.element(ix.into(), window, cx);
self.render_text_message(ix, rendered, text, cx) self.render_text_message(ix, message, text, show_author, cx)
}
Message::Warning(content, _timestamp) => {
self.render_warning(ix, SharedString::from(content), cx)
}
Message::System(_timestamp) => self.render_announcement(ix, cx),
}
} else { } else {
self.render_warning(ix, SharedString::from("Message not found"), cx) self.render_warning(ix, SharedString::from("Message not found"), cx)
} }
@@ -831,8 +839,9 @@ impl ChatPanel {
fn render_text_message( fn render_text_message(
&self, &self,
ix: usize, ix: usize,
message: &RenderedMessage, message: &Message,
rendered_text: AnyElement, rendered_text: AnyElement,
show_author: bool,
cx: &Context<Self>, cx: &Context<Self>,
) -> AnyElement { ) -> AnyElement {
let id = message.id; let id = message.id;
@@ -858,17 +867,21 @@ impl ChatPanel {
.flex() .flex()
.gap_3() .gap_3()
.when(!hide_avatar, |this| { .when(!hide_avatar, |this| {
this.child( if show_author {
Avatar::new(author.avatar()) this.child(
.flex_shrink_0() Avatar::new(author.avatar())
.relative() .flex_shrink_0()
.dropdown_menu(move |this, _window, _cx| { .relative()
this.menu("Public Key", Box::new(Command::Copy(pk))) .dropdown_menu(move |this, _window, _cx| {
.menu("View Relays", Box::new(Command::Relays(pk))) this.menu("Public Key", Box::new(Command::Copy(pk)))
.separator() .menu("View Relays", Box::new(Command::Relays(pk)))
.menu("View on njump.me", Box::new(Command::Njump(pk))) .separator()
}), .menu("View on njump.me", Box::new(Command::Njump(pk)))
) }),
)
} else {
this.child(div().flex_shrink_0().w(px(32.)))
}
}) })
.child( .child(
v_flex() v_flex()
@@ -876,22 +889,24 @@ impl ChatPanel {
.w_full() .w_full()
.flex_initial() .flex_initial()
.overflow_hidden() .overflow_hidden()
.child( .when(show_author, |this| {
h_flex() this.child(
.gap_2() h_flex()
.text_sm() .gap_2()
.text_color(cx.theme().text_placeholder) .text_sm()
.child( .text_color(cx.theme().text_placeholder)
div() .child(
.font_semibold() div()
.text_color(cx.theme().text) .font_semibold()
.child(author.name()), .text_color(cx.theme().text)
) .child(author.name()),
.child(message.created_at.to_human_time()) )
.when(has_reports, |this| { .child(message.created_at.to_human_time())
this.child(self.render_sent_reports(&id, cx)) .when(has_reports, |this| {
}), this.child(self.render_sent_reports(&id, cx))
) }),
)
})
.when(has_replies, |this| { .when(has_replies, |this| {
this.children(self.render_message_replies(replies, cx)) this.children(self.render_message_replies(replies, cx))
}) })
@@ -1009,7 +1024,7 @@ impl ChatPanel {
.on_click({ .on_click({
let id = *id; let id = *id;
cx.listener(move |this, _event, _window, _cx| { cx.listener(move |this, _event, _window, _cx| {
this.scroll_to(id); this.scroll_to(&id);
}) })
}), }),
); );
@@ -1500,15 +1515,28 @@ impl Render for ChatPanel {
v_flex() v_flex()
.flex_1() .flex_1()
.relative() .relative()
.child( .map(|this| {
list( if self.messages.is_empty() {
self.list_state.clone(), this.child(
cx.processor(move |this, ix, window, cx| { div()
this.render_message(ix, window, cx) .size_full()
}), .flex()
) .items_center()
.size_full(), .justify_end()
) .child(self.render_announcement(cx)),
)
} else {
this.child(
list(
self.list_state.clone(),
cx.processor(move |this, ix, window, cx| {
this.render_message(ix, window, cx)
}),
)
.size_full(),
)
}
})
.child(Scrollbar::vertical(&self.list_state)), .child(Scrollbar::vertical(&self.list_state)),
) )
.child( .child(