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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
57
crates/chats/src/message.rs
Normal file
57
crates/chats/src/message.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user