feat: Rich Text Rendering (#13)

* add text

* fix avatar is not show

* refactor chats

* improve rich text

* add benchmark for text

* update
This commit is contained in:
reya
2025-03-28 09:49:07 +07:00
committed by GitHub
parent 42d6328d82
commit cfc2300c0c
23 changed files with 1180 additions and 489 deletions

View File

@@ -7,8 +7,10 @@ publish.workspace = true
[dependencies]
common = { path = "../common" }
global = { path = "../global" }
ui = { path = "../ui" }
gpui.workspace = true
nostr.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true
itertools.workspace = true

View File

@@ -3,12 +3,13 @@ use std::cmp::Reverse;
use anyhow::anyhow;
use common::{last_seen::LastSeen, utils::room_hash};
use global::get_client;
use gpui::{App, AppContext, Context, Entity, Global, Task, WeakEntity};
use gpui::{App, AppContext, Context, Entity, Global, Task, Window};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use crate::room::{IncomingEvent, Room};
use crate::room::Room;
pub mod message;
pub mod room;
pub fn init(cx: &mut App) {
@@ -44,7 +45,7 @@ impl ChatRegistry {
self.rooms.iter().map(|room| room.read(cx).id).collect()
}
pub fn load_chat_rooms(&mut self, cx: &mut Context<Self>) {
pub fn load_chat_rooms(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let client = get_client();
let task: Task<Result<Vec<Event>, Error>> = cx.background_spawn(async move {
@@ -73,9 +74,9 @@ impl ChatRegistry {
Ok(result)
});
cx.spawn(async move |this, cx| {
cx.spawn_in(window, async move |this, cx| {
if let Ok(events) = task.await {
cx.update(|cx| {
cx.update(|_, cx| {
this.update(cx, |this, cx| {
if !events.is_empty() {
let current_ids = this.current_rooms_ids(cx);
@@ -118,11 +119,11 @@ impl ChatRegistry {
self.is_loading
}
pub fn get(&self, id: &u64, cx: &App) -> Option<WeakEntity<Room>> {
pub fn get(&self, id: &u64, cx: &App) -> Option<Entity<Room>> {
self.rooms
.iter()
.find(|model| model.read(cx).id == *id)
.map(|room| room.downgrade())
.cloned()
}
pub fn push_room(
@@ -144,14 +145,13 @@ impl ChatRegistry {
}
}
pub fn push_message(&mut self, event: Event, cx: &mut Context<Self>) {
pub fn push_message(&mut self, event: Event, window: &mut Window, cx: &mut Context<Self>) {
let id = room_hash(&event);
if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) {
room.update(cx, |this, cx| {
this.last_seen = LastSeen(event.created_at);
cx.emit(IncomingEvent { event });
cx.notify();
this.set_last_seen(LastSeen(event.created_at), cx);
this.emit_message(event, window, cx);
});
// Re-sort rooms by last seen

View File

@@ -0,0 +1,57 @@
use common::{last_seen::LastSeen, profile::NostrProfile};
use gpui::SharedString;
use nostr_sdk::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message {
pub id: EventId,
pub content: String,
pub author: NostrProfile,
pub mentions: Vec<NostrProfile>,
pub created_at: LastSeen,
}
impl Message {
pub fn new(
id: EventId,
content: String,
author: NostrProfile,
mentions: Vec<NostrProfile>,
created_at: Timestamp,
) -> Self {
let created_at = LastSeen(created_at);
Self {
id,
content,
author,
mentions,
created_at,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RoomMessage {
/// User message
User(Box<Message>),
/// System message
System(SharedString),
/// Only use for UI purposes.
/// Placeholder will be used for display room announcement
Announcement,
}
impl RoomMessage {
pub fn user(message: Message) -> Self {
Self::User(Box::new(message))
}
pub fn system(content: SharedString) -> Self {
Self::System(content)
}
pub fn announcement() -> Self {
Self::Announcement
}
}

View File

@@ -1,15 +1,22 @@
use std::collections::HashSet;
use std::{collections::HashSet, sync::Arc};
use anyhow::Error;
use common::{last_seen::LastSeen, profile::NostrProfile, utils::room_hash};
use common::{
last_seen::LastSeen,
profile::NostrProfile,
utils::{compare, room_hash},
};
use global::get_client;
use gpui::{App, AppContext, Entity, EventEmitter, SharedString, Task};
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task, Window};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use crate::message::{Message, RoomMessage};
#[derive(Debug, Clone)]
pub struct IncomingEvent {
pub event: Event,
pub event: RoomMessage,
}
pub struct Room {
@@ -18,7 +25,7 @@ pub struct Room {
/// Subject of the room
pub name: Option<SharedString>,
/// All members of the room
pub members: SmallVec<[NostrProfile; 2]>,
pub members: Arc<SmallVec<[NostrProfile; 2]>>,
}
impl EventEmitter<IncomingEvent> for Room {}
@@ -44,18 +51,19 @@ impl Room {
// Create a task for loading metadata
let load_metadata = Self::load_metadata(event, cx);
// Create a new GPUI's Entity
cx.new(|cx| {
let this = Self {
id,
last_seen,
name,
members: smallvec![],
members: Arc::new(smallvec![]),
};
cx.spawn(async move |this, cx| {
if let Ok(profiles) = load_metadata.await {
_ = cx.update(|cx| {
_ = this.update(cx, |this: &mut Room, cx| {
cx.update(|cx| {
this.update(cx, |this: &mut Room, cx| {
// Update the room's name if it's not already set
if this.name.is_none() {
let mut name = profiles
@@ -71,12 +79,16 @@ impl Room {
this.name = Some(name.into())
};
// Update the room's members
this.members.extend(profiles);
let mut new_members = SmallVec::new();
new_members.extend(profiles);
this.members = Arc::new(new_members);
cx.notify();
});
});
})
.ok();
})
.ok();
}
})
.detach();
@@ -122,13 +134,19 @@ impl Room {
self.last_seen
}
/// Set room's last seen
pub fn set_last_seen(&mut self, last_seen: LastSeen, cx: &mut Context<Self>) {
self.last_seen = last_seen;
cx.notify();
}
/// Get room's last seen as ago format
pub fn ago(&self) -> SharedString {
self.last_seen.ago()
}
/// Sync inbox relays for all room's members
pub fn verify_inbox_relays(&self, cx: &App) -> Task<Result<Vec<(PublicKey, bool)>, Error>> {
/// Verify messaging_relays for all room's members
pub fn messaging_relays(&self, cx: &App) -> Task<Result<Vec<(PublicKey, bool)>, Error>> {
let client = get_client();
let pubkeys = self.public_keys();
@@ -157,8 +175,6 @@ impl Room {
}
/// Send message to all room's members
///
/// NIP-4e: Message will be signed by the device signer
pub fn send_message(&self, content: String, cx: &App) -> Task<Result<Vec<String>, Error>> {
let client = get_client();
let pubkeys = self.public_keys();
@@ -192,21 +208,149 @@ impl Room {
})
}
/// Load metadata for all members
pub fn load_messages(&self, cx: &App) -> Task<Result<Events, Error>> {
/// Load room messages
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<RoomMessage>, Error>> {
let client = get_client();
let pubkeys = self.public_keys();
let members = Arc::clone(&self.members);
let filter = Filter::new()
.kind(Kind::PrivateDirectMessage)
.authors(pubkeys.iter().copied())
.pubkeys(pubkeys);
.authors(pubkeys.clone())
.pubkeys(pubkeys.clone());
cx.background_spawn(async move {
let query = client.database().query(filter).await?;
Ok(query)
let mut messages = vec![];
let parser = NostrParser::new();
// Get all events from database
let events = client
.database()
.query(filter)
.await?
.into_iter()
.sorted_by_key(|ev| ev.created_at)
.filter(|ev| {
let mut other_pubkeys = ev.tags.public_keys().copied().collect::<Vec<_>>();
other_pubkeys.push(ev.pubkey);
// Check if the event is from a member of the room
compare(&other_pubkeys, &pubkeys)
})
.collect::<Vec<_>>();
for event in events.into_iter() {
let mut mentions = vec![];
let content = event.content.clone();
let tokens = parser.parse(&content);
let author = members
.iter()
.find(|profile| profile.public_key == event.pubkey)
.cloned()
.unwrap_or_else(|| NostrProfile::new(event.pubkey, Metadata::default()));
let pubkey_tokens = 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<_>>();
for pubkey in pubkey_tokens {
if let Some(profile) =
members.iter().find(|profile| profile.public_key == pubkey)
{
mentions.push(profile.clone());
} else {
let metadata = client
.database()
.metadata(pubkey)
.await?
.unwrap_or_default();
mentions.push(NostrProfile::new(pubkey, metadata));
}
}
let message = Message::new(event.id, content, author, mentions, event.created_at);
let room_message = RoomMessage::user(message);
messages.push(room_message);
}
Ok(messages)
})
}
/// Emit message to GPUI
pub fn emit_message(&self, event: Event, window: &mut Window, cx: &mut Context<Self>) {
let client = get_client();
let members = Arc::clone(&self.members);
let task: Task<Result<RoomMessage, Error>> = cx.background_spawn(async move {
let parser = NostrParser::new();
let content = event.content.clone();
let tokens = parser.parse(&content);
let mut mentions = vec![];
let author = members
.iter()
.find(|profile| profile.public_key == event.pubkey)
.cloned()
.unwrap_or_else(|| NostrProfile::new(event.pubkey, Metadata::default()));
let pubkey_tokens = 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<_>>();
for pubkey in pubkey_tokens {
if let Some(profile) = members
.iter()
.find(|profile| profile.public_key == event.pubkey)
{
mentions.push(profile.clone());
} else {
let metadata = client
.database()
.metadata(pubkey)
.await?
.unwrap_or_default();
mentions.push(NostrProfile::new(pubkey, metadata));
}
}
let message = Message::new(event.id, content, author, mentions, event.created_at);
let room_message = RoomMessage::user(message);
Ok(room_message)
});
cx.spawn_in(window, async move |this, cx| {
if let Ok(message) = task.await {
cx.update(|_, cx| {
this.update(cx, |_, cx| {
cx.emit(IncomingEvent { event: message });
})
.ok();
})
.ok();
}
})
.detach();
}
/// Load metadata for all members
fn load_metadata(event: &Event, cx: &App) -> Task<Result<Vec<NostrProfile>, Error>> {
let client = get_client();
@@ -219,18 +363,23 @@ impl Room {
cx.background_spawn(async move {
let signer = client.signer().await?;
let signer_pubkey = signer.get_public_key().await?;
let mut profiles = vec![];
let mut profiles = Vec::with_capacity(pubkeys.len());
for public_key in pubkeys.into_iter() {
if let Ok(result) = client.database().metadata(public_key).await {
let metadata = result.unwrap_or_default();
let profile = NostrProfile::new(public_key, metadata);
let metadata = client
.database()
.metadata(public_key)
.await?
.unwrap_or_default();
if public_key == signer_pubkey {
profiles.push(profile);
} else {
profiles.insert(0, profile);
}
// Convert metadata to profile
let profile = NostrProfile::new(public_key, metadata);
if public_key == signer_pubkey {
// Room's owner always push to the end of the vector
profiles.push(profile);
} else {
profiles.insert(0, profile);
}
}