From 08980e55a4e819ed57f77392b42267c3dfec1cd6 Mon Sep 17 00:00:00 2001 From: reya Date: Sat, 11 Jan 2025 09:16:30 +0700 Subject: [PATCH] wip: refactor --- crates/app/src/main.rs | 20 +- crates/app/src/states/chat.rs | 199 ------------------ crates/app/src/states/chat/mod.rs | 173 ++++++++++++++++ crates/app/src/states/chat/room.rs | 97 +++++++++ crates/app/src/utils.rs | 5 + crates/app/src/views/account.rs | 4 +- crates/app/src/views/app.rs | 15 +- crates/app/src/views/chat/mod.rs | 6 +- crates/app/src/views/mod.rs | 2 +- crates/app/src/views/sidebar/inbox.rs | 286 ++++++++------------------ 10 files changed, 379 insertions(+), 428 deletions(-) delete mode 100644 crates/app/src/states/chat.rs create mode 100644 crates/app/src/states/chat/mod.rs create mode 100644 crates/app/src/states/chat/room.rs diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index 373c35a..ad60073 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -277,8 +277,8 @@ async fn main() { while let Ok(signal) = rx.recv().await { match signal { Signal::Eose => { - _ = async_cx.update_global::(|state, cx| { - state.init(cx); + _ = async_cx.update_global::(|chat, cx| { + chat.init(cx); }); } Signal::Metadata(public_key) => { @@ -290,16 +290,18 @@ async fn main() { let metadata = async_cx .background_executor() .spawn(async move { - client - .database() - .metadata(event.pubkey) - .await - .unwrap_or_default() + if let Ok(metadata) = + client.database().metadata(event.pubkey).await + { + metadata.unwrap_or_default() + } else { + Metadata::new() + } }) .await; - _ = async_cx.update_global::(|state, cx| { - state.new_message(event, metadata, cx) + _ = async_cx.update_global::(|chat, cx| { + chat.receive(event, metadata, cx) }); } } diff --git a/crates/app/src/states/chat.rs b/crates/app/src/states/chat.rs deleted file mode 100644 index f06d008..0000000 --- a/crates/app/src/states/chat.rs +++ /dev/null @@ -1,199 +0,0 @@ -use crate::{get_client, utils::room_hash}; -use gpui::{AppContext, Context, Global, Model, SharedString, WeakModel}; -use itertools::Itertools; -use nostr_sdk::prelude::*; -use profile::cut_public_key; -use rnglib::{Language, RNG}; -use serde::Deserialize; -use std::{ - cmp::Reverse, - collections::HashMap, - sync::{Arc, RwLock}, -}; - -#[derive(Clone, PartialEq, Eq, Deserialize)] -pub struct Member { - public_key: PublicKey, - metadata: Metadata, -} - -impl Member { - pub fn new(public_key: PublicKey, metadata: Metadata) -> Self { - Self { - public_key, - metadata, - } - } - - pub fn public_key(&self) -> PublicKey { - self.public_key - } - - pub fn metadata(&self) -> Metadata { - self.metadata.clone() - } - - pub fn name(&self) -> String { - if let Some(display_name) = &self.metadata.display_name { - if !display_name.is_empty() { - return display_name.clone(); - } - } - - if let Some(name) = &self.metadata.name { - if !name.is_empty() { - return name.clone(); - } - } - - cut_public_key(self.public_key) - } -} - -#[derive(Clone, PartialEq, Eq, Deserialize)] -pub struct Room { - pub id: SharedString, - pub owner: PublicKey, - pub members: Vec, - pub last_seen: Timestamp, - pub title: Option, -} - -impl Room { - pub fn new( - id: SharedString, - owner: PublicKey, - last_seen: Timestamp, - title: Option, - members: Vec, - ) -> Self { - let title = if title.is_none() { - let rng = RNG::from(&Language::Roman); - let name = rng.generate_names(2, true).join("-").to_lowercase(); - - Some(name.into()) - } else { - title - }; - - Self { - id, - title, - members, - last_seen, - owner, - } - } -} - -#[derive(Clone, Debug)] -pub struct Message { - pub event: Event, - pub metadata: Option, -} - -impl Message { - pub fn new(event: Event, metadata: Option) -> Self { - // TODO: parse event's content - Self { event, metadata } - } -} - -type Inbox = Vec; -type Messages = RwLock>>>>; - -pub struct ChatRegistry { - messages: Model, - inbox: Model, -} - -impl Global for ChatRegistry {} - -impl ChatRegistry { - pub fn set_global(cx: &mut AppContext) { - let inbox = cx.new_model(|_| Vec::new()); - let messages = cx.new_model(|_| RwLock::new(HashMap::new())); - - cx.set_global(Self { inbox, messages }); - } - - pub fn init(&mut self, cx: &mut AppContext) { - let async_cx = cx.to_async(); - // Get all current room's hashes - let hashes: Vec = self - .inbox - .read(cx) - .iter() - .map(|ev| room_hash(&ev.tags)) - .collect(); - - cx.foreground_executor() - .spawn(async move { - let client = get_client(); - let query: anyhow::Result, anyhow::Error> = async_cx - .background_executor() - .spawn(async move { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - - let filter = Filter::new() - .kind(Kind::PrivateDirectMessage) - .author(public_key); - - // Get all DM events from database - let events = client.database().query(vec![filter]).await?; - - // Filter result - // 1. Only new rooms - // 2. Only unique rooms - // 3. Sorted by created_at - let result = events - .into_iter() - .filter(|ev| !hashes.iter().any(|h| h == &room_hash(&ev.tags))) - .unique_by(|ev| room_hash(&ev.tags)) - .sorted_by_key(|ev| Reverse(ev.created_at)) - .collect::>(); - - Ok(result) - }) - .await; - - if let Ok(events) = query { - _ = async_cx.update_global::(|state, cx| { - state.inbox.update(cx, |model, cx| { - model.extend(events); - cx.notify(); - }); - }); - } - }) - .detach(); - } - - pub fn new_message(&mut self, event: Event, metadata: Option, cx: &mut AppContext) { - // Get room id - let room_id = SharedString::from(room_hash(&event.tags).to_string()); - // Create message - let message = Message::new(event, metadata); - - self.messages.update(cx, |this, cx| { - this.write() - .unwrap() - .entry(room_id) - .or_insert(Arc::new(RwLock::new(Vec::new()))) - .write() - .unwrap() - .push(message); - - cx.notify(); - }); - } - - pub fn messages(&self) -> WeakModel { - self.messages.downgrade() - } - - pub fn inbox(&self) -> WeakModel { - self.inbox.downgrade() - } -} diff --git a/crates/app/src/states/chat/mod.rs b/crates/app/src/states/chat/mod.rs new file mode 100644 index 0000000..60181fc --- /dev/null +++ b/crates/app/src/states/chat/mod.rs @@ -0,0 +1,173 @@ +use crate::{get_client, utils::room_hash}; +use gpui::{AppContext, Context, Global, Model, SharedString, WeakModel}; +use itertools::Itertools; +use nostr_sdk::prelude::*; +use room::Room; +use std::{ + cmp::Reverse, + collections::HashMap, + sync::{Arc, RwLock}, +}; + +pub mod room; + +#[derive(Clone, Debug)] +pub struct NewMessage { + pub event: Event, + pub metadata: Metadata, +} + +impl NewMessage { + pub fn new(event: Event, metadata: Metadata) -> Self { + // TODO: parse event's content + Self { event, metadata } + } +} + +type NewMessages = RwLock>>>>; + +pub struct ChatRegistry { + inbox: Model>>, + new_messages: Model, +} + +impl Global for ChatRegistry {} + +impl ChatRegistry { + pub fn set_global(cx: &mut AppContext) { + let inbox = cx.new_model(|_| Vec::new()); + let new_messages = cx.new_model(|_| RwLock::new(HashMap::new())); + + cx.observe_new_models::(|this, cx| { + // Get all pubkeys to load metadata + let pubkeys: Vec = this.members.iter().map(|m| m.public_key()).collect(); + + cx.spawn(|weak_model, mut async_cx| async move { + let query: Result, Error> = async_cx + .background_executor() + .spawn(async move { + let client = get_client(); + let mut profiles = Vec::new(); + + for public_key in pubkeys.into_iter() { + let query = client.database().metadata(public_key).await?; + let metadata = query.unwrap_or_default(); + + profiles.push((public_key, metadata)); + } + + Ok(profiles) + }) + .await; + + if let Ok(profiles) = query { + if let Some(model) = weak_model.upgrade() { + _ = async_cx.update_model(&model, |model, cx| { + for profile in profiles.into_iter() { + model.set_metadata(profile.0, profile.1); + } + cx.notify(); + }); + } + } + }) + .detach(); + }) + .detach(); + + cx.set_global(Self { + inbox, + new_messages, + }); + } + + pub fn init(&mut self, cx: &mut AppContext) { + let mut async_cx = cx.to_async(); + let async_inbox = self.inbox.clone(); + + // Get all current room's id + let hashes: Vec = self + .inbox + .read(cx) + .iter() + .map(|room| room.read(cx).id) + .collect(); + + cx.foreground_executor() + .spawn(async move { + let client = get_client(); + let query: anyhow::Result, anyhow::Error> = async_cx + .background_executor() + .spawn(async move { + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + + let filter = Filter::new() + .kind(Kind::PrivateDirectMessage) + .author(public_key); + + // Get all DM events from database + let events = client.database().query(vec![filter]).await?; + + // Filter result + // - Only unique rooms + // - Sorted by created_at + let result = events + .into_iter() + .filter(|ev| ev.tags.public_keys().peekable().peek().is_some()) + .unique_by(|ev| room_hash(&ev.tags)) + .sorted_by_key(|ev| Reverse(ev.created_at)) + .collect::>(); + + Ok(result) + }) + .await; + + if let Ok(events) = query { + _ = async_cx.update_model(&async_inbox, |model, cx| { + let items: Vec> = events + .into_iter() + .filter_map(|ev| { + let id = room_hash(&ev.tags); + // Filter all seen events + if !hashes.iter().any(|h| h == &id) { + Some(cx.new_model(|_| Room::new(&ev))) + } else { + None + } + }) + .collect(); + + model.extend(items); + cx.notify(); + }); + } + }) + .detach(); + } + + pub fn inbox(&self) -> WeakModel>> { + self.inbox.downgrade() + } + + pub fn new_messages(&self) -> WeakModel { + self.new_messages.downgrade() + } + + pub fn receive(&mut self, event: Event, metadata: Metadata, cx: &mut AppContext) { + let entry = room_hash(&event.tags).to_string().into(); + let message = NewMessage::new(event, metadata); + + self.new_messages.update(cx, |this, cx| { + this.write() + .unwrap() + .entry(entry) + .or_insert(Arc::new(RwLock::new(Vec::new()))) + .write() + .unwrap() + .push(message); + + cx.notify(); + }) + } +} diff --git a/crates/app/src/states/chat/room.rs b/crates/app/src/states/chat/room.rs new file mode 100644 index 0000000..373754a --- /dev/null +++ b/crates/app/src/states/chat/room.rs @@ -0,0 +1,97 @@ +use crate::utils::{room_hash, shorted_public_key}; +use gpui::SharedString; +use nostr_sdk::prelude::*; +use rnglib::{Language, RNG}; + +#[derive(Debug, Clone)] +pub struct Member { + public_key: PublicKey, + metadata: Metadata, +} + +impl Member { + pub fn new(public_key: PublicKey, metadata: Metadata) -> Self { + Self { + public_key, + metadata, + } + } + + pub fn public_key(&self) -> PublicKey { + self.public_key + } + + pub fn metadata(&self) -> Metadata { + self.metadata.clone() + } + + pub fn name(&self) -> String { + if let Some(display_name) = &self.metadata.display_name { + if !display_name.is_empty() { + return display_name.clone(); + } + } + + if let Some(name) = &self.metadata.name { + if !name.is_empty() { + return name.clone(); + } + } + + shorted_public_key(self.public_key) + } + + pub fn update(&mut self, metadata: &Metadata) { + self.metadata = metadata.clone() + } +} + +#[derive(Debug)] +pub struct Room { + pub id: u64, + pub title: Option, + pub members: Vec, + pub last_seen: Timestamp, + pub is_group: bool, +} + +impl Room { + pub fn new(event: &Event) -> Self { + let id = room_hash(&event.tags); + let last_seen = event.created_at; + + let members: Vec = event + .tags + .public_keys() + .copied() + .map(|public_key| Member::new(public_key, Metadata::default())) + .collect(); + + let title = if let Some(tag) = event.tags.find(TagKind::Title) { + tag.content().map(|s| s.to_owned().into()) + } else { + let rng = RNG::from(&Language::Roman); + let name = rng.generate_names(2, true).join("-").to_lowercase(); + + Some(name.into()) + }; + + let is_group = members.len() > 1; + + Self { + id, + members, + title, + last_seen, + is_group, + } + } + + pub fn set_metadata(&mut self, public_key: PublicKey, metadata: Metadata) { + for member in self.members.iter_mut() { + if member.public_key() == public_key { + member.update(&metadata); + } + } + } +} diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 1a2daad..efdc3c6 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -11,6 +11,11 @@ pub fn room_hash(tags: &Tags) -> u64 { hasher.finish() } +pub fn shorted_public_key(public_key: PublicKey) -> String { + let pk = public_key.to_string(); + format!("{}:{}", &pk[0..4], &pk[pk.len() - 4..]) +} + pub fn show_npub(public_key: PublicKey, len: usize) -> String { let bech32 = public_key.to_bech32().unwrap_or_default(); let separator = " ... "; diff --git a/crates/app/src/views/account.rs b/crates/app/src/views/account.rs index 1aead04..f07f967 100644 --- a/crates/app/src/views/account.rs +++ b/crates/app/src/views/account.rs @@ -1,3 +1,4 @@ +use crate::{constants::IMAGE_SERVICE, get_client, states::app::AppRegistry}; use gpui::prelude::FluentBuilder; use gpui::{ actions, img, Context, IntoElement, Model, ObjectFit, ParentElement, Render, Styled, @@ -10,9 +11,6 @@ use ui::{ Icon, IconName, Sizable, }; -use crate::states::app::AppRegistry; -use crate::{constants::IMAGE_SERVICE, get_client}; - actions!(account, [ToDo]); pub struct Account { diff --git a/crates/app/src/views/app.rs b/crates/app/src/views/app.rs index fe2d70e..ed225ea 100644 --- a/crates/app/src/views/app.rs +++ b/crates/app/src/views/app.rs @@ -1,8 +1,5 @@ -use super::{ - account::Account, chat::ChatPanel, onboarding::Onboarding, sidebar::Sidebar, - welcome::WelcomePanel, -}; -use crate::states::{app::AppRegistry, chat::Room}; +use super::{account::Account, onboarding::Onboarding, sidebar::Sidebar, welcome::WelcomePanel}; +use crate::states::app::AppRegistry; use gpui::{ div, impl_actions, px, Axis, Context, Edges, InteractiveElement, IntoElement, Model, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, WindowContext, @@ -18,7 +15,7 @@ use ui::{ #[derive(Clone, PartialEq, Eq, Deserialize)] pub enum PanelKind { - Room(Arc), + Room(u64), } #[derive(Clone, PartialEq, Eq, Deserialize)] @@ -132,15 +129,17 @@ impl AppView { } fn on_action_add_panel(&mut self, action: &AddPanel, cx: &mut ViewContext) { + /* match &action.panel { - PanelKind::Room(room) => { - let panel = Arc::new(ChatPanel::new(room, cx)); + PanelKind::Room(id) => { + let panel = Arc::new(ChatPanel::new(id, cx)); self.dock.update(cx, |dock_area, cx| { dock_area.add_panel(panel, action.position, cx); }); } }; + */ } } diff --git a/crates/app/src/views/chat/mod.rs b/crates/app/src/views/chat/mod.rs index c919f72..0bda9a9 100644 --- a/crates/app/src/views/chat/mod.rs +++ b/crates/app/src/views/chat/mod.rs @@ -1,4 +1,4 @@ -use crate::{get_client, states::chat::Room, utils::room_hash}; +use crate::{get_client, states::chat::room::Room}; use gpui::{ div, list, px, AnyElement, AppContext, Context, EventEmitter, Flatten, FocusHandle, FocusableView, IntoElement, ListAlignment, ListState, Model, ParentElement, PathPromptOptions, @@ -40,8 +40,8 @@ pub struct ChatPanel { } impl ChatPanel { - pub fn new(room: &Arc, cx: &mut WindowContext) -> View { - let room = Arc::clone(room); + pub fn new(room_id: &u64, cx: &mut WindowContext) -> View { + let room = Arc::new(room); let id = room.id.clone(); let name = room.title.clone().unwrap_or("Untitled".into()); diff --git a/crates/app/src/views/mod.rs b/crates/app/src/views/mod.rs index 1a8dfbb..6836672 100644 --- a/crates/app/src/views/mod.rs +++ b/crates/app/src/views/mod.rs @@ -1,5 +1,5 @@ mod account; -mod chat; +// mod chat; mod onboarding; mod sidebar; mod welcome; diff --git a/crates/app/src/views/sidebar/inbox.rs b/crates/app/src/views/sidebar/inbox.rs index 39a6b96..b725b6d 100644 --- a/crates/app/src/views/sidebar/inbox.rs +++ b/crates/app/src/views/sidebar/inbox.rs @@ -1,72 +1,35 @@ -use crate::{ - constants::IMAGE_SERVICE, - get_client, - states::{ - app::AppRegistry, - chat::{ChatRegistry, Member, Room}, - }, - utils::{ago, room_hash}, - views::app::{AddPanel, PanelKind}, -}; -use gpui::prelude::FluentBuilder; +use crate::{constants::IMAGE_SERVICE, states::chat::ChatRegistry, utils::ago}; use gpui::{ - div, img, percentage, Context, InteractiveElement, IntoElement, Model, ParentElement, Render, - SharedString, StatefulInteractiveElement, Styled, View, ViewContext, VisualContext, + div, img, percentage, prelude::FluentBuilder, InteractiveElement, IntoElement, ParentElement, + Render, RenderOnce, SharedString, StatefulInteractiveElement, Styled, ViewContext, WindowContext, }; -use nostr_sdk::prelude::*; -use std::sync::Arc; use ui::{skeleton::Skeleton, theme::ActiveTheme, v_flex, Collapsible, Icon, IconName, StyledExt}; pub struct Inbox { label: SharedString, - items: Model>>>, - is_loading: bool, is_collapsed: bool, } impl Inbox { - pub fn new(cx: &mut ViewContext<'_, Self>) -> Self { - let items = cx.new_model(|_| None); - let inbox = cx.global::().inbox(); - - if let Some(inbox) = inbox.upgrade() { - cx.observe(&inbox, |this, model, cx| { - this.load(model, cx); - }) - .detach(); - } - + pub fn new(_cx: &mut ViewContext<'_, Self>) -> Self { Self { - items, label: "Inbox".into(), - is_loading: true, is_collapsed: false, } } - pub fn load(&mut self, model: Model>, cx: &mut ViewContext) { - let events = model.read(cx).clone(); - let views: Vec> = events - .into_iter() - .map(|event| { - cx.new_view(|cx| { - let view = InboxListItem::new(event, cx); - // Initial metadata - view.load_metadata(cx); - - view - }) - }) - .collect(); - - self.items.update(cx, |model, cx| { - *model = Some(views); - cx.notify(); - }); - - self.is_loading = false; - cx.notify(); + fn skeleton(&self, total: i32) -> impl IntoIterator { + (0..total).map(|_| { + div() + .h_8() + .px_1() + .flex() + .items_center() + .gap_2() + .child(Skeleton::new().flex_shrink_0().size_6().rounded_full()) + .child(Skeleton::new().w_20().h_3().rounded_sm()) + }) } } @@ -84,22 +47,35 @@ impl Collapsible for Inbox { impl Render for Inbox { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let mut content = div(); + let weak_model = cx.global::().inbox(); - if self.is_loading { - content = content.children((0..5).map(|_| { - div() - .h_8() - .px_1() - .flex() - .items_center() - .gap_2() - .child(Skeleton::new().flex_shrink_0().size_6().rounded_full()) - .child(Skeleton::new().w_20().h_3().rounded_sm()) + if let Some(model) = weak_model.upgrade() { + content = content.children(model.read(cx).iter().map(|model| { + let room = model.read(cx); + let id = room.id.to_string().into(); + let ago = ago(room.last_seen.as_u64()).into(); + // Get first member + let sender = room.members.first().unwrap(); + // Compute group name based on member' names + let name: SharedString = room + .members + .iter() + .map(|profile| profile.name()) + .collect::>() + .join(", ") + .into(); + + InboxListItem::new( + id, + ago, + room.is_group, + name, + sender.metadata().picture, + sender.name(), + ) })) - } else if let Some(items) = self.items.read(cx).as_ref() { - content = content.children(items.clone()) } else { - // TODO: handle error + content = content.children(self.skeleton(5)) } v_flex() @@ -134,110 +110,38 @@ impl Render for Inbox { } } +#[derive(Clone, IntoElement)] struct InboxListItem { id: SharedString, - created_at: Timestamp, - owner: PublicKey, - pubkeys: Vec, - members: Model>, + ago: SharedString, is_group: bool, + group_name: SharedString, + sender_avatar: Option, + sender_name: String, } impl InboxListItem { - pub fn new(event: Event, cx: &mut ViewContext<'_, Self>) -> Self { - let id = room_hash(&event.tags).to_string().into(); - let created_at = event.created_at; - let owner = event.pubkey; - - let pubkeys: Vec = event.tags.public_keys().copied().collect(); - let is_group = pubkeys.len() > 1; - - let members = cx.new_model(|_| Vec::new()); - let refreshs = cx.global_mut::().refreshs(); - - if let Some(refreshs) = refreshs.upgrade() { - cx.observe(&refreshs, |this, _, cx| { - this.load_metadata(cx); - }) - .detach(); - } - + pub fn new( + id: SharedString, + ago: SharedString, + is_group: bool, + group_name: SharedString, + sender_avatar: Option, + sender_name: String, + ) -> Self { Self { id, - created_at, - owner, - pubkeys, - members, + ago, is_group, + group_name, + sender_avatar, + sender_name, } } - - pub fn load_metadata(&self, cx: &mut ViewContext) { - let owner = self.owner; - let public_keys = self.pubkeys.clone(); - let async_members = self.members.clone(); - - let mut async_cx = cx.to_async(); - - cx.foreground_executor() - .spawn({ - let client = get_client(); - - async move { - let metadata: anyhow::Result, anyhow::Error> = async_cx - .background_executor() - .spawn(async move { - let mut result = Vec::new(); - - for public_key in public_keys.into_iter() { - let metadata = client.database().metadata(public_key).await?; - let profile = Member::new(public_key, metadata.unwrap_or_default()); - - result.push(profile); - } - - let metadata = client.database().metadata(owner).await?; - let profile = Member::new(owner, metadata.unwrap_or_default()); - - result.push(profile); - - Ok(result) - }) - .await; - - if let Ok(metadata) = metadata { - _ = async_cx.update_model(&async_members, |model, cx| { - *model = metadata; - cx.notify(); - }); - } - } - }) - .detach(); - } - - pub fn action(&self, cx: &mut WindowContext<'_>) { - let members = self.members.read(cx).clone(); - let room = Arc::new(Room::new( - self.id.clone(), - self.owner, - self.created_at, - None, - members, - )); - - cx.dispatch_action(Box::new(AddPanel { - panel: PanelKind::Room(room), - position: ui::dock::DockPlacement::Center, - })) - } } -impl Render for InboxListItem { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let ago = ago(self.created_at.as_u64()); - let members = self.members.read(cx); - +impl RenderOnce for InboxListItem { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { let mut content = div() .font_medium() .text_color(cx.theme().sidebar_accent_foreground); @@ -248,58 +152,33 @@ impl Render for InboxListItem { .items_center() .gap_2() .child(img("brand/avatar.png").size_6().rounded_full()) - .map(|this| { - let names: Vec = members - .iter() - .filter_map(|m| { - if m.public_key() != self.owner { - Some(m.name()) - } else { - None - } - }) - .collect(); - - this.child(names.join(", ")) - }) + .child(self.group_name) } else { - content = content.flex().items_center().gap_2().map(|this| { - if let Some(member) = members.first() { - let mut child = this; - - // Avatar - if let Some(picture) = member.metadata().picture.clone() { - child = child.child( - img(format!( - "{}/?url={}&w=72&h=72&fit=cover&mask=circle&n=-1", - IMAGE_SERVICE, picture - )) - .flex_shrink_0() - .size_6() - .rounded_full(), - ); - } else { - child = child.child( - img("brand/avatar.png") - .flex_shrink_0() - .size_6() - .rounded_full(), - ); - } - - // Display name - child = child.child(member.name()); - - child + content = content.flex().items_center().gap_2().map(|mut this| { + // Avatar + if let Some(picture) = self.sender_avatar { + this = this.child( + img(format!( + "{}/?url={}&w=72&h=72&fit=cover&mask=circle&n=-1", + IMAGE_SERVICE, picture + )) + .flex_shrink_0() + .size_6() + .rounded_full(), + ); } else { - this.child( + this = this.child( img("brand/avatar.png") .flex_shrink_0() .size_6() .rounded_full(), - ) - .child("Unknown") + ); } + + // Display name + this = this.child(self.sender_name); + + this }) } @@ -319,11 +198,8 @@ impl Render for InboxListItem { .child(content) .child( div() - .child(ago) + .child(self.ago) .text_color(cx.theme().sidebar_accent_foreground.opacity(0.7)), ) - .on_click(cx.listener(|this, _, cx| { - this.action(cx); - })) } }