diff --git a/crates/app/src/states/chat.rs b/crates/app/src/states/chat.rs index 35b3f16..f06d008 100644 --- a/crates/app/src/states/chat.rs +++ b/crates/app/src/states/chat.rs @@ -1,8 +1,8 @@ -use crate::get_client; -use crate::utils::get_room_id; +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::{ @@ -11,56 +11,79 @@ use std::{ 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 members: Vec, pub last_seen: Timestamp, pub title: Option, - pub metadata: Option, } impl Room { pub fn new( id: SharedString, owner: PublicKey, - members: Vec, last_seen: Timestamp, title: Option, - metadata: 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, - metadata, } } - - pub fn parse(event: &Event, metadata: Option) -> Self { - let owner = event.pubkey; - let last_seen = event.created_at; - let id = SharedString::from(get_room_id(&owner, &event.tags)); - - // Get all members from event's tag - let mut members: Vec = event.tags.public_keys().copied().collect(); - members.push(owner); - - // Get title from event's tag - 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()) - }; - - Self::new(id, owner, members, last_seen, title, metadata) - } } #[derive(Clone, Debug)] @@ -76,32 +99,32 @@ impl Message { } } +type Inbox = Vec; type Messages = RwLock>>>>; pub struct ChatRegistry { messages: Model, - rooms: Model>, + inbox: Model, } impl Global for ChatRegistry {} impl ChatRegistry { pub fn set_global(cx: &mut AppContext) { - let rooms = cx.new_model(|_| Vec::new()); + let inbox = cx.new_model(|_| Vec::new()); let messages = cx.new_model(|_| RwLock::new(HashMap::new())); - cx.set_global(Self { messages, rooms }); + 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 ids - let ids: Vec = self - .rooms + // Get all current room's hashes + let hashes: Vec = self + .inbox .read(cx) .iter() - .map(|ev| get_room_id(&ev.pubkey, &ev.tags)) + .map(|ev| room_hash(&ev.tags)) .collect(); cx.foreground_executor() @@ -115,18 +138,19 @@ impl ChatRegistry { let filter = Filter::new() .kind(Kind::PrivateDirectMessage) - .pubkey(public_key); + .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| ev.pubkey != public_key) - .filter(|ev| { - let new_id = get_room_id(&ev.pubkey, &ev.tags); - // Get new events only - !ids.iter().any(|id| id == &new_id) - }) // Filter all messages from current user - .unique_by(|ev| ev.pubkey) + .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::>(); @@ -136,7 +160,7 @@ impl ChatRegistry { if let Ok(events) = query { _ = async_cx.update_global::(|state, cx| { - state.rooms.update(cx, |model, cx| { + state.inbox.update(cx, |model, cx| { model.extend(events); cx.notify(); }); @@ -148,7 +172,7 @@ impl ChatRegistry { pub fn new_message(&mut self, event: Event, metadata: Option, cx: &mut AppContext) { // Get room id - let room_id = SharedString::from(get_room_id(&event.pubkey, &event.tags)); + let room_id = SharedString::from(room_hash(&event.tags).to_string()); // Create message let message = Message::new(event, metadata); @@ -169,7 +193,7 @@ impl ChatRegistry { self.messages.downgrade() } - pub fn rooms(&self) -> WeakModel> { - self.rooms.downgrade() + pub fn inbox(&self) -> WeakModel { + self.inbox.downgrade() } } diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 4abbb79..1a2daad 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -1,23 +1,14 @@ use chrono::{Duration, Local, TimeZone}; use nostr_sdk::prelude::*; +use std::hash::{DefaultHasher, Hash, Hasher}; -pub fn get_room_id(author: &PublicKey, tags: &Tags) -> String { - // Get all public keys - let mut pubkeys: Vec = tags.public_keys().copied().collect(); - // Add author to public keys list - pubkeys.insert(0, *author); +pub fn room_hash(tags: &Tags) -> u64 { + let pubkeys: Vec = tags.public_keys().copied().collect(); + let mut hasher = DefaultHasher::new(); + // Generate unique hash + pubkeys.hash(&mut hasher); - let hex: Vec = pubkeys - .iter() - .map(|m| { - let hex = m.to_hex(); - let split = &hex[..6]; - - split.to_owned() - }) - .collect(); - - hex.join("-") + hasher.finish() } pub fn show_npub(public_key: PublicKey, len: usize) -> String { diff --git a/crates/app/src/views/chat/message.rs b/crates/app/src/views/chat/message.rs index 25a23ad..7f7f4ac 100644 --- a/crates/app/src/views/chat/message.rs +++ b/crates/app/src/views/chat/message.rs @@ -15,7 +15,7 @@ pub struct RoomMessage { #[allow(dead_code)] author: PublicKey, fallback: SharedString, - metadata: Option, + metadata: Metadata, content: SharedString, created_at: SharedString, } @@ -23,7 +23,7 @@ pub struct RoomMessage { impl RoomMessage { pub fn new( author: PublicKey, - metadata: Option, + metadata: Metadata, content: String, created_at: Timestamp, ) -> Self { @@ -55,18 +55,14 @@ impl RenderOnce for RoomMessage { .text_color(cx.theme().muted_foreground) }) .child(div().flex_shrink_0().map(|this| { - if let Some(metadata) = self.metadata.clone() { - if let Some(picture) = metadata.picture { - this.child( - img(format!( - "{}/?url={}&w=100&h=100&fit=cover&mask=circle&n=-1", - IMAGE_SERVICE, picture - )) - .size_8(), - ) - } else { - this.child(img("brand/avatar.png").size_8().rounded_full()) - } + if let Some(picture) = self.metadata.picture { + this.child( + img(format!( + "{}/?url={}&w=100&h=100&fit=cover&mask=circle&n=-1", + IMAGE_SERVICE, picture + )) + .size_8(), + ) } else { this.child(img("brand/avatar.png").size_8().rounded_full()) } @@ -84,12 +80,8 @@ impl RenderOnce for RoomMessage { .gap_2() .text_xs() .child(div().font_semibold().map(|this| { - if let Some(metadata) = self.metadata { - if let Some(display_name) = metadata.display_name { - this.child(display_name) - } else { - this.child(self.fallback) - } + if let Some(display_name) = self.metadata.display_name { + this.child(display_name) } else { this.child(self.fallback) } diff --git a/crates/app/src/views/chat/mod.rs b/crates/app/src/views/chat/mod.rs index 1b3d0f4..c919f72 100644 --- a/crates/app/src/views/chat/mod.rs +++ b/crates/app/src/views/chat/mod.rs @@ -1,20 +1,29 @@ +use crate::{get_client, states::chat::Room, utils::room_hash}; use gpui::{ - div, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, IntoElement, - ParentElement, Render, SharedString, Styled, View, VisualContext, WindowContext, + div, list, px, AnyElement, AppContext, Context, EventEmitter, Flatten, FocusHandle, + FocusableView, IntoElement, ListAlignment, ListState, Model, ParentElement, PathPromptOptions, + Pixels, Render, SharedString, Styled, View, ViewContext, VisualContext, WindowContext, }; +use itertools::Itertools; +use message::RoomMessage; use nostr_sdk::prelude::*; -use room::RoomPanel; use std::sync::Arc; use ui::{ - button::Button, + button::{Button, ButtonVariants}, dock::{Panel, PanelEvent, PanelState}, + input::{InputEvent, TextInput}, popup_menu::PopupMenu, + theme::ActiveTheme, + v_flex, Icon, IconName, }; -use crate::states::chat::Room; - mod message; -mod room; + +#[derive(Clone)] +pub struct State { + count: usize, + items: Vec, +} pub struct ChatPanel { // Panel @@ -22,38 +31,148 @@ pub struct ChatPanel { closeable: bool, zoomable: bool, focus_handle: FocusHandle, - // Room + // Chat Room id: SharedString, - room: View, - metadata: Option, + room: Arc, + input: View, + list: ListState, + state: Model, } impl ChatPanel { pub fn new(room: &Arc, cx: &mut WindowContext) -> View { + let room = Arc::clone(room); let id = room.id.clone(); - let title = room.title.clone(); - let metadata = room.metadata.clone(); + let name = room.title.clone().unwrap_or("Untitled".into()); - let room = cx.new_view(|cx| { - let view = RoomPanel::new(room, cx); - // Load messages - view.load(cx); - // Subscribe for new messages - view.subscribe(cx); + cx.observe_new_views::(|this, cx| { + this.load_messages(cx); + }) + .detach(); - view - }); + cx.new_view(|cx| { + // Form + let input = cx.new_view(|cx| { + TextInput::new(cx) + .appearance(false) + .text_size(ui::Size::Small) + .placeholder("Message...") + .cleanable() + }); - cx.new_view(|cx| Self { - name: title.unwrap_or("Untitled".into()), - closeable: true, - zoomable: true, - focus_handle: cx.focus_handle(), - id, - room, - metadata, + // Send message when user presses enter on form. + cx.subscribe(&input, move |this: &mut ChatPanel, _, input_event, cx| { + if let InputEvent::PressEnter = input_event { + this.send_message(cx); + } + }) + .detach(); + + let state = cx.new_model(|_| State { + count: 0, + items: vec![], + }); + + cx.observe(&state, |this, model, cx| { + let items = model.read(cx).items.clone(); + + this.list = ListState::new( + items.len(), + ListAlignment::Bottom, + Pixels(256.), + move |idx, _cx| { + let item = items.get(idx).unwrap().clone(); + div().child(item).into_any_element() + }, + ); + + cx.notify(); + }) + .detach(); + + let list = ListState::new(0, ListAlignment::Bottom, Pixels(256.), move |_, _| { + div().into_any_element() + }); + + Self { + closeable: true, + zoomable: true, + focus_handle: cx.focus_handle(), + id, + name, + room, + input, + list, + state, + } }) } + + fn load_messages(&self, cx: &mut ViewContext) { + let members = self.room.members.clone(); + let async_state = self.state.clone(); + let id = self.room.id.to_string(); + + let client = get_client(); + let mut async_cx = cx.to_async(); + + cx.foreground_executor() + .spawn(async move { + let events: anyhow::Result = async_cx + .background_executor() + .spawn({ + let pubkeys = members.iter().map(|m| m.public_key()).collect::>(); + + async move { + let signer = client.signer().await?; + let author = signer.get_public_key().await?; + + let recv = Filter::new() + .kind(Kind::PrivateDirectMessage) + .author(author) + .pubkeys(pubkeys.clone()); + + let send = Filter::new() + .kind(Kind::PrivateDirectMessage) + .authors(pubkeys) + .pubkey(author); + + // Get all DM events in database + let query = client.database().query(vec![recv, send]).await?; + + Ok(query) + } + }) + .await; + + if let Ok(events) = events { + let items: Vec = events + .into_iter() + .sorted_by_key(|ev| ev.created_at) + .map(|ev| { + let metadata = members + .iter() + .find(|&m| m.public_key() == ev.pubkey) + .unwrap() + .metadata(); + + RoomMessage::new(ev.pubkey, metadata, ev.content, ev.created_at) + }) + .collect(); + + let total = items.len(); + + _ = async_cx.update_model(&async_state, |a, b| { + a.items = items; + a.count = total; + b.notify(); + }); + } + }) + .detach(); + } + + fn send_message(&mut self, cx: &mut ViewContext) {} } impl Panel for ChatPanel { @@ -62,7 +181,7 @@ impl Panel for ChatPanel { } fn panel_metadata(&self) -> Option { - self.metadata.clone() + None } fn title(&self, _cx: &WindowContext) -> AnyElement { @@ -99,7 +218,52 @@ impl FocusableView for ChatPanel { } impl Render for ChatPanel { - fn render(&mut self, _cx: &mut gpui::ViewContext) -> impl IntoElement { - div().size_full().child(self.room.clone()) + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + v_flex() + .size_full() + .child(list(self.list.clone()).flex_1()) + .child( + div() + .flex_shrink_0() + .w_full() + .h_12() + .flex() + .items_center() + .gap_2() + .px_2() + .child( + Button::new("upload") + .icon(Icon::new(IconName::Upload)) + .ghost() + .on_click(|_, cx| { + let paths = cx.prompt_for_paths(PathPromptOptions { + files: true, + directories: false, + multiple: false, + }); + + cx.spawn(move |_async_cx| async move { + match Flatten::flatten(paths.await.map_err(|e| e.into())) { + Ok(Some(paths)) => { + // TODO: upload file to blossom server + println!("Paths: {:?}", paths) + } + Ok(None) => {} + Err(_) => {} + } + }) + .detach(); + }), + ) + .child( + div() + .flex_1() + .flex() + .bg(cx.theme().muted) + .rounded(px(cx.theme().radius)) + .px_2() + .child(self.input.clone()), + ), + ) } } diff --git a/crates/app/src/views/chat/room.rs b/crates/app/src/views/chat/room.rs index e4002fc..76c6c9c 100644 --- a/crates/app/src/views/chat/room.rs +++ b/crates/app/src/views/chat/room.rs @@ -15,7 +15,7 @@ use ui::{ use super::message::RoomMessage; use crate::{ get_client, - states::chat::{ChatRegistry, Room}, + states::chat::{ChatRegistry, Member, Room}, }; #[derive(Clone)] @@ -27,7 +27,7 @@ pub struct Messages { pub struct RoomPanel { id: SharedString, owner: PublicKey, - members: Arc<[PublicKey]>, + members: Vec, // Form input: View, // Messages @@ -38,7 +38,7 @@ pub struct RoomPanel { impl RoomPanel { pub fn new(room: &Arc, cx: &mut ViewContext<'_, Self>) -> Self { let id = room.id.clone(); - let members: Arc<[PublicKey]> = room.members.clone().into(); + let members = room.members.clone(); let owner = room.owner; // Form @@ -98,20 +98,21 @@ impl RoomPanel { let async_messages = self.messages.clone(); let mut async_cx = cx.to_async(); + let public_keys: Vec = self.members.iter().map(|m| m.public_key()).collect(); + cx.foreground_executor() .spawn({ let client = get_client(); let owner = self.owner; - let members = self.members.to_vec(); let recv = Filter::new() .kind(Kind::PrivateDirectMessage) .author(owner) - .pubkeys(members.clone()); + .pubkeys(public_keys.clone()); let send = Filter::new() .kind(Kind::PrivateDirectMessage) - .authors(members) + .authors(public_keys) .pubkey(owner); async move { @@ -200,8 +201,9 @@ impl RoomPanel { } fn send_message(&mut self, cx: &mut ViewContext) { - let members = self.members.clone(); + let members: Vec = self.members.iter().map(|m| m.public_key()).collect(); let members2 = members.clone(); + let content = self.input.read(cx).text().to_string(); let content2 = content.clone(); let content3 = content2.clone(); @@ -227,19 +229,21 @@ impl RoomPanel { async_cx .background_executor() .spawn(async move { - for member in members.iter() { - let tags: Vec = members - .iter() - .filter_map(|public_key| { - if public_key != member { - Some(Tag::public_key(*public_key)) - } else { - None - } - }) - .collect(); + let extra_tags: Vec = members + .iter() + .filter_map(|m| { + if m != ¤t_user { + Some(Tag::public_key(*m)) + } else { + None + } + }) + .collect(); - _ = client.send_private_msg(*member, &content, tags).await; + for member in members.iter() { + _ = client + .send_private_msg(*member, &content, extra_tags.clone()) + .await } }) .detach(); @@ -248,18 +252,20 @@ impl RoomPanel { async_cx .background_executor() .spawn(async move { - let tags: Vec = members2 + let extra_tags: Vec = members2 .iter() - .filter_map(|public_key| { - if public_key != ¤t_user { - Some(Tag::public_key(*public_key)) + .filter_map(|m| { + if m != ¤t_user { + Some(Tag::public_key(*m)) } else { None } }) .collect(); - _ = client.send_private_msg(current_user, content2, tags).await; + _ = client + .send_private_msg(current_user, content2, extra_tags) + .await; }) .detach(); diff --git a/crates/app/src/views/sidebar/inbox.rs b/crates/app/src/views/sidebar/inbox.rs index f37594b..39a6b96 100644 --- a/crates/app/src/views/sidebar/inbox.rs +++ b/crates/app/src/views/sidebar/inbox.rs @@ -3,9 +3,9 @@ use crate::{ get_client, states::{ app::AppRegistry, - chat::{ChatRegistry, Room}, + chat::{ChatRegistry, Member, Room}, }, - utils::{ago, get_room_id, show_npub}, + utils::{ago, room_hash}, views::app::{AddPanel, PanelKind}, }; use gpui::prelude::FluentBuilder; @@ -18,143 +18,6 @@ use nostr_sdk::prelude::*; use std::sync::Arc; use ui::{skeleton::Skeleton, theme::ActiveTheme, v_flex, Collapsible, Icon, IconName, StyledExt}; -struct InboxListItem { - id: SharedString, - event: Event, - metadata: Model>, -} - -impl InboxListItem { - pub fn new(event: Event, metadata: Option, cx: &mut ViewContext<'_, Self>) -> Self { - let id = SharedString::from(get_room_id(&event.pubkey, &event.tags)); - let metadata = cx.new_model(|_| metadata); - let refreshs = cx.global_mut::().refreshs(); - - if let Some(refreshs) = refreshs.upgrade() { - cx.observe(&refreshs, |this, _, cx| { - this.load_metadata(cx); - }) - .detach(); - } - - Self { - id, - event, - metadata, - } - } - - pub fn load_metadata(&self, cx: &mut ViewContext) { - let mut async_cx = cx.to_async(); - let async_metadata = self.metadata.clone(); - - cx.foreground_executor() - .spawn({ - let client = get_client(); - let public_key = self.event.pubkey; - - async move { - let metadata = async_cx - .background_executor() - .spawn(async move { client.database().metadata(public_key).await }) - .await; - - if let Ok(metadata) = metadata { - _ = async_cx.update_model(&async_metadata, |model, cx| { - *model = metadata; - cx.notify(); - }); - } - } - }) - .detach(); - } - - pub fn action(&self, cx: &mut WindowContext<'_>) { - let metadata = self.metadata.read(cx).clone(); - let room = Arc::new(Room::parse(&self.event, metadata)); - - 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.event.created_at.as_u64()); - let fallback_name = show_npub(self.event.pubkey, 16); - - let mut content = div() - .font_medium() - .text_color(cx.theme().sidebar_accent_foreground); - - if let Some(metadata) = self.metadata.read(cx).as_ref() { - content = content - .flex() - .items_center() - .gap_2() - .map(|this| { - if let Some(picture) = metadata.picture.clone() { - this.flex_shrink_0().child( - img(format!( - "{}/?url={}&w=72&h=72&fit=cover&mask=circle&n=-1", - IMAGE_SERVICE, picture - )) - .size_6(), - ) - } else { - this.flex_shrink_0() - .child(img("brand/avatar.png").size_6().rounded_full()) - } - }) - .map(|this| { - if let Some(display_name) = metadata.display_name.clone() { - this.child(display_name) - } else { - this.child(fallback_name) - } - }) - } else { - content = content - .flex() - .items_center() - .gap_2() - .child( - img("brand/avatar.png") - .flex_shrink_0() - .size_6() - .rounded_full(), - ) - .child(fallback_name) - } - - div() - .id(self.id.clone()) - .h_8() - .px_1() - .flex() - .items_center() - .justify_between() - .text_xs() - .rounded_md() - .hover(|this| { - this.bg(cx.theme().sidebar_accent) - .text_color(cx.theme().sidebar_accent_foreground) - }) - .child(content) - .child( - div() - .child(ago) - .text_color(cx.theme().sidebar_accent_foreground.opacity(0.7)), - ) - .on_click(cx.listener(|this, _, cx| { - this.action(cx); - })) - } -} - pub struct Inbox { label: SharedString, items: Model>>>, @@ -165,10 +28,10 @@ pub struct Inbox { impl Inbox { pub fn new(cx: &mut ViewContext<'_, Self>) -> Self { let items = cx.new_model(|_| None); - let events = cx.global::().rooms(); + let inbox = cx.global::().inbox(); - if let Some(events) = events.upgrade() { - cx.observe(&events, |this, model, cx| { + if let Some(inbox) = inbox.upgrade() { + cx.observe(&inbox, |this, model, cx| { this.load(model, cx); }) .detach(); @@ -184,41 +47,26 @@ impl Inbox { 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); - cx.spawn(|view, mut async_cx| async move { - let client = get_client(); - let mut views = Vec::new(); + view + }) + }) + .collect(); - for event in events.into_iter() { - let metadata = async_cx - .background_executor() - .spawn(async move { client.database().metadata(event.pubkey).await }) - .await; + self.items.update(cx, |model, cx| { + *model = Some(views); + cx.notify(); + }); - let item = async_cx - .new_view(|cx| { - if let Ok(metadata) = metadata { - InboxListItem::new(event, metadata, cx) - } else { - InboxListItem::new(event, None, cx) - } - }) - .unwrap(); - - views.push(item); - } - - _ = view.update(&mut async_cx, |this, cx| { - this.items.update(cx, |model, cx| { - *model = Some(views); - cx.notify() - }); - - this.is_loading = false; - cx.notify(); - }); - }) - .detach(); + self.is_loading = false; + cx.notify(); } } @@ -285,3 +133,197 @@ impl Render for Inbox { .when(!self.is_collapsed, |this| this.child(content)) } } + +struct InboxListItem { + id: SharedString, + created_at: Timestamp, + owner: PublicKey, + pubkeys: Vec, + members: Model>, + is_group: bool, +} + +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(); + } + + Self { + id, + created_at, + owner, + pubkeys, + members, + is_group, + } + } + + 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); + + let mut content = div() + .font_medium() + .text_color(cx.theme().sidebar_accent_foreground); + + if self.is_group { + content = content + .flex() + .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(", ")) + }) + } 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 + } else { + this.child( + img("brand/avatar.png") + .flex_shrink_0() + .size_6() + .rounded_full(), + ) + .child("Unknown") + } + }) + } + + div() + .id(self.id.clone()) + .h_8() + .px_1() + .flex() + .items_center() + .justify_between() + .text_xs() + .rounded_md() + .hover(|this| { + this.bg(cx.theme().sidebar_accent) + .text_color(cx.theme().sidebar_accent_foreground) + }) + .child(content) + .child( + div() + .child(ago) + .text_color(cx.theme().sidebar_accent_foreground.opacity(0.7)), + ) + .on_click(cx.listener(|this, _, cx| { + this.action(cx); + })) + } +}