diff --git a/crates/app/src/views/app.rs b/crates/app/src/views/app.rs index 6b2a263..6cc1d2f 100644 --- a/crates/app/src/views/app.rs +++ b/crates/app/src/views/app.rs @@ -43,11 +43,8 @@ impl AddPanel { } } -// Dock actions impl_internal_actions!(dock, [AddPanel]); - -// Account actions -actions!(account, [OpenProfile, OpenContacts, OpenSettings, Logout]); +actions!(account, [Logout]); pub fn init(account: NostrProfile, window: &mut Window, cx: &mut App) -> Entity { AppView::new(account, window, cx) @@ -89,9 +86,6 @@ impl AppView { view.set_center(center_panel, window, cx); }); - let public_key = account.public_key(); - let window_handle = window.window_handle(); - // Check and auto update to the latest version cx.background_spawn(async move { // Set auto updater config @@ -112,13 +106,17 @@ impl AppView { }) .detach(); - // Check user's messaging relays and determine user is ready for NIP17 or not. - // If not, show the setup modal and instruct user setup inbox relays - cx.spawn(|mut cx| async move { + cx.new(|cx| { + // Check user's messaging relays and determine user is ready for NIP17 or not. + // If not, show the setup modal and instruct user setup inbox relays + let client = get_client(); + let public_key = account.public_key(); + let window_handle = window.window_handle(); let (tx, rx) = oneshot::channel::(); + let this = Self { account, dock }; + cx.background_spawn(async move { - let client = get_client(); let filter = Filter::new() .kind(Kind::InboxRelays) .author(public_key) @@ -134,53 +132,53 @@ impl AppView { }) .detach(); - if let Ok(is_ready) = rx.await { - if is_ready { - // - } else { - cx.update_window(window_handle, |_, window, cx| { - let relays = cx.new(|cx| Relays::new(window, cx)); - - window.open_modal(cx, move |this, window, cx| { - let is_loading = relays.read(cx).loading(); - - this.keyboard(false) - .closable(false) - .width(px(420.)) - .title("Your Messaging Relays is not configured") - .child(relays.clone()) - .footer( - div() - .p_2() - .border_t_1() - .border_color( - cx.theme().base.step(cx, ColorScaleStep::FIVE), - ) - .child( - Button::new("update_inbox_relays_btn") - .label("Update") - .primary() - .bold() - .rounded(ButtonRounded::Large) - .w_full() - .loading(is_loading) - .on_click(window.listener_for( - &relays, - |this, _, window, cx| { - this.update(window, cx); - }, - )), - ), - ) + cx.spawn(|this, mut cx| async move { + if let Ok(is_ready) = rx.await { + if !is_ready { + _ = cx.update_window(window_handle, |_, window, cx| { + this.update(cx, |this: &mut Self, cx| { + this.render_relays_setup(window, cx) + }) }); - }) - .unwrap(); + } } - } - }) - .detach(); + }) + .detach(); - cx.new(|_| Self { account, dock }) + this + }) + } + + fn render_relays_setup(&self, window: &mut Window, cx: &mut Context) { + let relays = cx.new(|cx| Relays::new(window, cx)); + + window.open_modal(cx, move |this, window, cx| { + let is_loading = relays.read(cx).loading(); + + this.keyboard(false) + .closable(false) + .width(px(420.)) + .title("Your Messaging Relays is not configured") + .child(relays.clone()) + .footer( + div() + .p_2() + .border_t_1() + .border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE)) + .child( + Button::new("update_inbox_relays_btn") + .label("Update") + .primary() + .bold() + .rounded(ButtonRounded::Large) + .w_full() + .loading(is_loading) + .on_click(window.listener_for(&relays, |this, _, window, cx| { + this.update(window, cx); + })), + ), + ) + }); } fn render_account(&self) -> impl IntoElement { @@ -215,13 +213,14 @@ impl AppView { fn on_panel_action(&mut self, action: &AddPanel, window: &mut Window, cx: &mut Context) { match &action.panel { - PanelKind::Room(id) => { - if let Ok(panel) = chat::init(id, window, cx) { + PanelKind::Room(id) => match chat::init(id, window, cx) { + Ok(panel) => { self.dock.update(cx, |dock_area, cx| { dock_area.add_panel(panel, action.position, window, cx); }); } - } + Err(e) => window.push_notification(e.to_string(), cx), + }, PanelKind::Profile => { let panel = Arc::new(profile::init(self.account.clone(), window, cx)); diff --git a/crates/app/src/views/chat/mod.rs b/crates/app/src/views/chat.rs similarity index 52% rename from crates/app/src/views/chat/mod.rs rename to crates/app/src/views/chat.rs index e32fe0d..86874b0 100644 --- a/crates/app/src/views/chat/mod.rs +++ b/crates/app/src/views/chat.rs @@ -1,25 +1,24 @@ -use std::sync::Arc; - use anyhow::anyhow; use async_utility::task::spawn; -use chats::registry::ChatRegistry; -use chats::room::{LastSeen, Room}; +use chats::{registry::ChatRegistry, room::Room}; use common::{ constants::IMAGE_SERVICE, + last_seen::LastSeen, profile::NostrProfile, utils::{compare, nip96_upload}, }; use gpui::{ div, img, list, prelude::FluentBuilder, px, white, AnyElement, App, AppContext, Context, - Entity, EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement, IntoElement, - ListAlignment, ListState, ObjectFit, ParentElement, PathPromptOptions, Pixels, Render, - SharedString, StatefulInteractiveElement, Styled, StyledImage, Window, + Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement, + IntoElement, ListAlignment, ListState, ObjectFit, ParentElement, PathPromptOptions, Render, + SharedString, StatefulInteractiveElement, Styled, StyledImage, Subscription, WeakEntity, + Window, }; use itertools::Itertools; -use message::Message; use nostr_sdk::prelude::*; use smol::fs; use state::get_client; +use std::sync::Arc; use tokio::sync::oneshot; use ui::{ button::{Button, ButtonRounded, ButtonVariants}, @@ -27,11 +26,9 @@ use ui::{ input::{InputEvent, TextInput}, popup_menu::PopupMenu, theme::{scale::ColorScaleStep, ActiveTheme}, - v_flex, ContextModal, Icon, IconName, Sizable, + v_flex, ContextModal, Icon, IconName, Sizable, StyledExt, }; -mod message; - pub fn init( id: &u64, window: &mut Window, @@ -39,7 +36,7 @@ pub fn init( ) -> Result>, anyhow::Error> { if let Some(chats) = ChatRegistry::global(cx) { if let Some(room) = chats.read(cx).get(id, cx) { - Ok(Arc::new(Chat::new(&room, window, cx))) + Ok(Arc::new(Chat::new(id, &room, window, cx))) } else { Err(anyhow!("Chat room is not exist")) } @@ -48,24 +45,43 @@ pub fn init( } } -#[derive(Clone)] -pub struct State { - count: usize, - items: Vec, +struct Message { + profile: NostrProfile, + content: SharedString, + ago: SharedString, } +impl PartialEq for Message { + fn eq(&self, other: &Self) -> bool { + let content = self.content == other.content; + let member = self.profile == other.profile; + let ago = self.ago == other.ago; + + content && member && ago + } +} + +impl Message { + pub fn new(profile: NostrProfile, content: SharedString, ago: SharedString) -> Self { + Self { + profile, + content, + ago, + } + } +} pub struct Chat { // Panel + id: SharedString, closable: bool, zoomable: bool, focus_handle: FocusHandle, // Chat Room - id: SharedString, - name: SharedString, - owner: NostrProfile, - members: Vec, - state: Entity, - list: ListState, + room: WeakEntity, + messages: Entity>, + new_messages: WeakEntity>, + list_state: ListState, + subscriptions: Vec, // New Message input: Entity, // Media @@ -74,29 +90,14 @@ pub struct Chat { } impl Chat { - pub fn new(model: &Entity, window: &mut Window, cx: &mut App) -> Entity { - let room = model.read(cx); - let id = room.id.to_string().into(); - let name = room.title.clone().unwrap_or("Untitled".into()); - let owner = room.owner.clone(); - let members = room.members.clone(); + pub fn new(id: &u64, model: &Entity, window: &mut Window, cx: &mut App) -> Entity { + let room = model.downgrade(); + let new_messages = model.read(cx).new_messages.downgrade(); cx.new(|cx| { - // Load all messages - cx.observe_new::(|this, window, cx| { - if let Some(window) = window { - this.load_messages(window, cx); - } - }) - .detach(); + let messages = cx.new(|_| Vec::new()); + let attaches = cx.new(|_| None); - // Observe and load new messages - cx.observe_in(model, window, |this: &mut Chat, model, _, cx| { - this.load_new_messages(&model, cx); - }) - .detach(); - - // New message form let input = cx.new(|cx| { TextInput::new(window, cx) .appearance(false) @@ -104,8 +105,7 @@ impl Chat { .placeholder("Message...") }); - // Send message when user presses enter - cx.subscribe_in( + let subscriptions = vec![cx.subscribe_in( &input, window, move |this: &mut Chat, _, input_event, window, cx| { @@ -113,188 +113,209 @@ impl Chat { this.send_message(window, cx); } }, - ) - .detach(); + )]; - // List state model - let state = cx.new(|_| State { - count: 0, - items: vec![], + let list_state = ListState::new(0, ListAlignment::Bottom, px(1024.), { + let this = cx.entity().downgrade(); + move |ix, window, cx| { + this.update(cx, |this, cx| { + this.render_message(ix, window, cx).into_any_element() + }) + .unwrap() + } }); - // Update list on every state changes - cx.observe(&state, |this, model, cx| { - this.list = ListState::new( - model.read(cx).items.len(), - ListAlignment::Bottom, - Pixels(1024.), - move |idx, _window, cx| { - if let Some(message) = model.read(cx).items.get(idx) { - div().child(message.clone()).into_any_element() - } else { - div().into_any_element() - } - }, - ); - cx.notify(); - }) - .detach(); - - let attaches = cx.new(|_| None); - - Self { + let mut this = Self { closable: true, zoomable: true, focus_handle: cx.focus_handle(), - list: ListState::new(0, ListAlignment::Bottom, Pixels(1024.), move |_, _, _| { - div().into_any_element() - }), is_uploading: false, - id, - name, - owner, - members, + id: id.to_string().into(), + room, + new_messages, + messages, + list_state, input, - state, attaches, - } + subscriptions, + }; + + // Load all messages from database + this.load_messages(cx); + // Subscribe and load new messages + this.load_new_messages(cx); + + this }) } - fn load_messages(&self, window: &mut Window, cx: &mut Context) { - let window_handle = window.window_handle(); - // Get current user - let author = self.owner.public_key(); - // Get other users in room - let pubkeys = self + fn load_messages(&self, cx: &mut Context) { + let Some(model) = self.room.upgrade() else { + return; + }; + + let client = get_client(); + let (tx, rx) = oneshot::channel::(); + + let room = model.read(cx); + let pubkeys = room .members .iter() .map(|m| m.public_key()) .collect::>(); - // Get all public keys for comparisation - let mut all_keys = pubkeys.clone(); - all_keys.push(author); - cx.spawn(|this, mut cx| async move { - let (tx, rx) = oneshot::channel::(); + let recv = Filter::new() + .kind(Kind::PrivateDirectMessage) + .author(room.owner.public_key()) + .pubkeys(pubkeys.iter().copied()); - cx.background_spawn({ - let client = get_client(); + let send = Filter::new() + .kind(Kind::PrivateDirectMessage) + .authors(pubkeys) + .pubkey(room.owner.public_key()); - let recv = Filter::new() - .kind(Kind::PrivateDirectMessage) - .author(author) - .pubkeys(pubkeys.iter().copied()); + cx.background_spawn(async move { + let Ok(recv_events) = client.database().query(recv).await else { + return; + }; + let Ok(send_events) = client.database().query(send).await else { + return; + }; + let events = recv_events.merge(send_events); - let send = Filter::new() - .kind(Kind::PrivateDirectMessage) - .authors(pubkeys) - .pubkey(author); - - // Get all DM events in database - async move { - let recv_events = client.database().query(recv).await.unwrap(); - let send_events = client.database().query(send).await.unwrap(); - let events = recv_events.merge(send_events); - _ = tx.send(events); - } - }) - .detach(); + _ = tx.send(events); + }) + .detach(); + cx.spawn(|this, cx| async move { if let Ok(events) = rx.await { - _ = cx.update_window(window_handle, |_, _, cx| { + _ = cx.update(|cx| { _ = this.update(cx, |this, cx| { - let items: Vec = events - .into_iter() - .sorted_by_key(|ev| ev.created_at) - .filter_map(|ev| { - let mut pubkeys: Vec<_> = ev.tags.public_keys().copied().collect(); - pubkeys.push(ev.pubkey); - - if compare(&pubkeys, &all_keys) { - let member = if let Some(member) = - this.members.iter().find(|&m| m.public_key() == ev.pubkey) - { - member.to_owned() - } else { - this.owner.clone() - }; - - Some(Message::new( - member, - ev.content.into(), - LastSeen(ev.created_at).human_readable(), - )) - } else { - None - } - }) - .collect(); - - cx.update_entity(&this.state, |this, cx| { - this.count = items.len(); - this.items = items; - cx.notify(); - }); + this.push_messages(events, cx); }); - }); + }) } }) .detach(); } - fn load_new_messages(&self, model: &Entity, cx: &mut Context) { - let room = model.read(cx); - let items: Vec = room - .new_messages - .iter() - .filter_map(|event| { - room.member(&event.pubkey).map(|member| { - Message::new( - member, - event.content.clone().into(), - LastSeen(event.created_at).human_readable(), - ) - }) - }) - .collect(); + fn push_messages(&self, events: Events, cx: &mut Context) { + let Some(model) = self.room.upgrade() else { + return; + }; - cx.update_entity(&self.state, |this, cx| { - let messages: Vec = items + let room = model.read(cx); + let pubkeys = room.pubkeys(); + + let (messages, total) = { + let items: Vec = events .into_iter() - .filter_map(|new| { - if !this.items.iter().any(|old| old == &new) { - Some(new) + .sorted_by_key(|ev| ev.created_at) + .filter_map(|ev| { + let mut other_pubkeys: Vec<_> = ev.tags.public_keys().copied().collect(); + other_pubkeys.push(ev.pubkey); + + if compare(&other_pubkeys, &pubkeys) { + let member = if let Some(member) = + room.members.iter().find(|&m| m.public_key() == ev.pubkey) + { + member.to_owned() + } else { + room.owner.to_owned() + }; + + Some(Message::new( + member, + ev.content.into(), + LastSeen(ev.created_at).human_readable(), + )) + } else { + None + } + }) + .collect(); + let total = items.len(); + + (items, total) + }; + + cx.update_entity(&self.messages, |this, cx| { + this.extend(messages); + cx.notify(); + }); + + self.list_state.reset(total); + } + + fn load_new_messages(&mut self, cx: &mut Context) { + let Some(model) = self.new_messages.upgrade() else { + return; + }; + + let subscription = cx.observe(&model, |view, this, cx| { + let Some(model) = view.room.upgrade() else { + return; + }; + + let room = model.read(cx); + let old_messages = view.messages.read(cx); + let old_len = old_messages.len(); + + let items: Vec = this + .read(cx) + .iter() + .filter_map(|event| { + if let Some(profile) = room.member(&event.pubkey) { + let message = Message::new( + profile, + event.content.clone().into(), + LastSeen(event.created_at).human_readable(), + ); + + if !old_messages.iter().any(|old| old == &message) { + Some(message) + } else { + None + } } else { None } }) .collect(); - this.items.extend(messages); - this.count = this.items.len(); - cx.notify(); + let total = items.len(); + + cx.update_entity(&view.messages, |this, cx| { + let messages: Vec = items + .into_iter() + .filter_map(|new| { + if !this.iter().any(|old| old == &new) { + Some(new) + } else { + None + } + }) + .collect(); + + this.extend(messages); + cx.notify(); + }); + + view.list_state.splice(old_len..old_len, total); }); + + self.subscriptions.push(subscription); } fn send_message(&mut self, window: &mut Window, cx: &mut Context) { - let window_handle = window.window_handle(); - - // Get current user - let author = self.owner.public_key(); - - // Get other users in room - let mut pubkeys = self - .members - .iter() - .map(|m| m.public_key()) - .collect::>(); - pubkeys.push(author); + let Some(model) = self.room.upgrade() else { + return; + }; // Get message let mut content = self.input.read(cx).text().to_string(); - // Get all attaches and merge with message + // Get all attaches and merge its with message if let Some(attaches) = self.attaches.read(cx).as_ref() { let merged = attaches .iter() @@ -316,63 +337,74 @@ impl Chat { this.set_disabled(true, window, cx); }); - cx.spawn(|this, mut cx| async move { - cx.background_spawn({ - let client = get_client(); - let content = content.clone(); - let tags: Vec = pubkeys - .iter() - .filter_map(|pubkey| { - if pubkey != &author { - Some(Tag::public_key(*pubkey)) - } else { - None - } - }) - .collect(); + let client = get_client(); + let window_handle = window.window_handle(); - async move { - // Send message to all members - for pubkey in pubkeys.iter() { - if let Err(_e) = client - .send_private_msg(*pubkey, &content, tags.clone()) - .await - { - // TODO: handle error - } - } + let room = model.read(cx); + let pubkeys = room.pubkeys(); + let async_content = content.clone(); + let tags: Vec = room + .pubkeys() + .iter() + .filter_map(|pubkey| { + if pubkey != &room.owner.public_key() { + Some(Tag::public_key(*pubkey)) + } else { + None } }) - .detach(); + .collect(); + // Send message to all pubkeys + cx.background_spawn(async move { + for pubkey in pubkeys.iter() { + if let Err(_e) = client + .send_private_msg(*pubkey, &async_content, tags.clone()) + .await + { + // TODO: handle error + } + } + }) + .detach(); + + cx.spawn(|this, mut cx| async move { _ = cx.update_window(window_handle, |_, window, cx| { _ = this.update(cx, |this, cx| { - let message = Message::new( - this.owner.clone(), - content.to_string().into(), - LastSeen(Timestamp::now()).human_readable(), - ); - - // Update message list - cx.update_entity(&this.state, |this, cx| { - this.items.extend(vec![message]); - this.count = this.items.len(); - cx.notify(); - }); - - // Reset message input - cx.update_entity(&this.input, |this, cx| { - this.set_loading(false, window, cx); - this.set_disabled(false, window, cx); - this.set_text("", window, cx); - cx.notify(); - }); + this.force_push_message(content.clone(), window, cx); }); }); }) .detach(); } + fn force_push_message(&self, content: String, window: &mut Window, cx: &mut Context) { + let Some(model) = self.room.upgrade() else { + return; + }; + + let room = model.read(cx); + let ago = LastSeen(Timestamp::now()).human_readable(); + let message = Message::new(room.owner.clone(), content.into(), ago); + let old_len = self.messages.read(cx).len(); + + // Update message list + cx.update_entity(&self.messages, |this, cx| { + this.extend(vec![message]); + cx.notify(); + }); + + // Reset message input + cx.update_entity(&self.input, |this, cx| { + this.set_loading(false, window, cx); + this.set_disabled(false, window, cx); + this.set_text("", window, cx); + cx.notify(); + }); + + self.list_state.splice(old_len..old_len, 1); + } + fn upload(&mut self, window: &mut Window, cx: &mut Context) { let window_handle = window.window_handle(); @@ -449,6 +481,65 @@ impl Chat { self.is_uploading = status; cx.notify(); } + + fn render_message( + &self, + ix: usize, + _window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { + if let Some(message) = self.messages.read(cx).get(ix) { + div() + .group("") + .relative() + .flex() + .gap_3() + .w_full() + .p_2() + .hover(|this| this.bg(cx.theme().accent.step(cx, ColorScaleStep::ONE))) + .child( + div() + .absolute() + .left_0() + .top_0() + .w(px(2.)) + .h_full() + .bg(cx.theme().transparent) + .group_hover("", |this| { + this.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE)) + }), + ) + .child( + img(message.profile.avatar()) + .size_8() + .rounded_full() + .flex_shrink_0(), + ) + .child( + div() + .flex() + .flex_col() + .flex_initial() + .overflow_hidden() + .child( + div() + .flex() + .items_baseline() + .gap_2() + .text_xs() + .child(div().font_semibold().child(message.profile.name())) + .child( + div().child(message.ago.clone()).text_color( + cx.theme().base.step(cx, ColorScaleStep::ELEVEN), + ), + ), + ) + .child(div().text_sm().child(message.content.clone())), + ) + } else { + div() + } + } } impl Panel for Chat { @@ -456,12 +547,36 @@ impl Panel for Chat { self.id.clone() } - fn panel_facepile(&self, _cx: &App) -> Option> { - Some(self.members.iter().map(|member| member.avatar()).collect()) - } + fn title(&self, cx: &App) -> AnyElement { + self.room + .read_with(cx, |this, _cx| { + let name = this.name(); + let facepill: Vec = + this.members.iter().map(|member| member.avatar()).collect(); - fn title(&self, _cx: &App) -> AnyElement { - self.name.clone().into_any_element() + div() + .flex() + .items_center() + .gap_1() + .child( + div() + .flex() + .flex_row_reverse() + .items_center() + .justify_start() + .children(facepill.into_iter().enumerate().rev().map(|(ix, face)| { + div().when(ix > 0, |div| div.ml_neg_1()).child( + img(face) + .size_4() + .rounded_full() + .object_fit(ObjectFit::Cover), + ) + })), + ) + .child(name) + .into_any() + }) + .unwrap_or("Unnamed".into_any()) } fn closable(&self, _cx: &App) -> bool { @@ -493,7 +608,7 @@ impl Render for Chat { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() - .child(list(self.list.clone()).flex_1()) + .child(list(self.list_state.clone()).flex_1()) .child( div().flex_shrink_0().p_2().child( div() diff --git a/crates/app/src/views/chat/message.rs b/crates/app/src/views/chat/message.rs deleted file mode 100644 index 4938717..0000000 --- a/crates/app/src/views/chat/message.rs +++ /dev/null @@ -1,88 +0,0 @@ -use common::profile::NostrProfile; -use gpui::{ - div, img, px, App, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, - Styled, Window, -}; -use ui::{ - theme::{scale::ColorScaleStep, ActiveTheme}, - StyledExt, -}; - -#[derive(Clone, Debug, IntoElement)] -pub struct Message { - profile: NostrProfile, - content: SharedString, - ago: SharedString, -} - -impl PartialEq for Message { - fn eq(&self, other: &Self) -> bool { - let content = self.content == other.content; - let member = self.profile == other.profile; - let ago = self.ago == other.ago; - - content && member && ago - } -} - -impl Message { - pub fn new(profile: NostrProfile, content: SharedString, ago: SharedString) -> Self { - Self { - profile, - content, - ago, - } - } -} - -impl RenderOnce for Message { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - div() - .group(&self.ago) - .relative() - .flex() - .gap_3() - .w_full() - .p_2() - .hover(|this| this.bg(cx.theme().accent.step(cx, ColorScaleStep::ONE))) - .child( - div() - .absolute() - .left_0() - .top_0() - .w(px(2.)) - .h_full() - .bg(cx.theme().transparent) - .group_hover(&self.ago, |this| { - this.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE)) - }), - ) - .child( - img(self.profile.avatar()) - .size_8() - .rounded_full() - .flex_shrink_0(), - ) - .child( - div() - .flex() - .flex_col() - .flex_initial() - .overflow_hidden() - .child( - div() - .flex() - .items_baseline() - .gap_2() - .text_xs() - .child(div().font_semibold().child(self.profile.name())) - .child( - div() - .child(self.ago) - .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)), - ), - ) - .child(div().text_sm().child(self.content)), - ) - } -} diff --git a/crates/chats/src/registry.rs b/crates/chats/src/registry.rs index 26747b5..8f761dc 100644 --- a/crates/chats/src/registry.rs +++ b/crates/chats/src/registry.rs @@ -118,7 +118,7 @@ impl ChatRegistry { let new = room_hash(&ev); // Filter all seen events if !current_rooms.iter().any(|this| this == &new) { - Some(cx.new(|_| Room::parse(&ev))) + Some(cx.new(|cx| Room::parse(&ev, cx))) } else { None } @@ -163,11 +163,14 @@ impl ChatRegistry { { room.update(cx, |this, cx| { this.last_seen.set(event.created_at); - this.new_messages.push(event); + this.new_messages.update(cx, |this, cx| { + this.push(event); + cx.notify(); + }); cx.notify(); }); } else { - let room = cx.new(|_| Room::parse(&event)); + let room = cx.new(|cx| Room::parse(&event, cx)); self.rooms.insert(0, room); cx.notify(); } diff --git a/crates/chats/src/room.rs b/crates/chats/src/room.rs index 6e6574d..20b3c9d 100644 --- a/crates/chats/src/room.rs +++ b/crates/chats/src/room.rs @@ -1,66 +1,12 @@ -use chrono::{Datelike, Local, TimeZone}; use common::{ + last_seen::LastSeen, profile::NostrProfile, utils::{compare, random_name, room_hash}, }; -use gpui::SharedString; +use gpui::{App, AppContext, Entity, SharedString}; use nostr_sdk::prelude::*; use std::collections::HashSet; -pub struct LastSeen(pub Timestamp); - -impl LastSeen { - pub fn ago(&self) -> SharedString { - let now = Local::now(); - let input_time = Local.timestamp_opt(self.0.as_u64() as i64, 0).unwrap(); - let diff = (now - input_time).num_hours(); - - if diff < 24 { - let duration = now.signed_duration_since(input_time); - - if duration.num_seconds() < 60 { - "now".to_string().into() - } else if duration.num_minutes() == 1 { - "1m".to_string().into() - } else if duration.num_minutes() < 60 { - format!("{}m", duration.num_minutes()).into() - } else if duration.num_hours() == 1 { - "1h".to_string().into() - } else if duration.num_hours() < 24 { - format!("{}h", duration.num_hours()).into() - } else if duration.num_days() == 1 { - "1d".to_string().into() - } else { - format!("{}d", duration.num_days()).into() - } - } else { - input_time.format("%b %d").to_string().into() - } - } - - pub fn human_readable(&self) -> SharedString { - let now = Local::now(); - let input_time = Local.timestamp_opt(self.0.as_u64() as i64, 0).unwrap(); - - if input_time.day() == now.day() { - format!("Today at {}", input_time.format("%H:%M %p")).into() - } else if input_time.day() == now.day() - 1 { - format!("Yesterday at {}", input_time.format("%H:%M %p")).into() - } else { - format!( - "{}, {}", - input_time.format("%d/%m/%y"), - input_time.format("%H:%M %p") - ) - .into() - } - } - - pub fn set(&mut self, created_at: Timestamp) { - self.0 = created_at - } -} - pub struct Room { pub id: u64, pub title: Option, @@ -68,18 +14,12 @@ pub struct Room { pub members: Vec, // Extract from event's tags pub last_seen: LastSeen, pub is_group: bool, - pub new_messages: Vec, // Hold all new messages + pub new_messages: Entity>, // Hold all new messages } impl PartialEq for Room { fn eq(&self, other: &Self) -> bool { - let mut pubkeys: Vec = self.members.iter().map(|m| m.public_key()).collect(); - pubkeys.push(self.owner.public_key()); - - let mut pubkeys2: Vec = other.members.iter().map(|m| m.public_key()).collect(); - pubkeys2.push(other.owner.public_key()); - - compare(&pubkeys, &pubkeys2) + compare(&self.pubkeys(), &other.pubkeys()) } } @@ -90,7 +30,9 @@ impl Room { members: Vec, title: Option, last_seen: LastSeen, + cx: &mut App, ) -> Self { + let new_messages = cx.new(|_| Vec::new()); let is_group = members.len() > 1; let title = if title.is_none() { Some(random_name(2).into()) @@ -105,12 +47,12 @@ impl Room { title, last_seen, is_group, - new_messages: vec![], + new_messages, } } /// Convert nostr event to room - pub fn parse(event: &Event) -> Room { + pub fn parse(event: &Event, cx: &mut App) -> Room { let id = room_hash(event); let last_seen = LastSeen(event.created_at); @@ -133,7 +75,7 @@ impl Room { None }; - Self::new(id, owner, members, title, last_seen) + Self::new(id, owner, members, title, last_seen, cx) } /// Set contact's metadata by public key diff --git a/crates/common/src/last_seen.rs b/crates/common/src/last_seen.rs new file mode 100644 index 0000000..972a714 --- /dev/null +++ b/crates/common/src/last_seen.rs @@ -0,0 +1,57 @@ +use chrono::{Datelike, Local, TimeZone}; +use gpui::SharedString; +use nostr_sdk::prelude::*; + +pub struct LastSeen(pub Timestamp); + +impl LastSeen { + pub fn ago(&self) -> SharedString { + let now = Local::now(); + let input_time = Local.timestamp_opt(self.0.as_u64() as i64, 0).unwrap(); + let diff = (now - input_time).num_hours(); + + if diff < 24 { + let duration = now.signed_duration_since(input_time); + + if duration.num_seconds() < 60 { + "now".to_string().into() + } else if duration.num_minutes() == 1 { + "1m".to_string().into() + } else if duration.num_minutes() < 60 { + format!("{}m", duration.num_minutes()).into() + } else if duration.num_hours() == 1 { + "1h".to_string().into() + } else if duration.num_hours() < 24 { + format!("{}h", duration.num_hours()).into() + } else if duration.num_days() == 1 { + "1d".to_string().into() + } else { + format!("{}d", duration.num_days()).into() + } + } else { + input_time.format("%b %d").to_string().into() + } + } + + pub fn human_readable(&self) -> SharedString { + let now = Local::now(); + let input_time = Local.timestamp_opt(self.0.as_u64() as i64, 0).unwrap(); + + if input_time.day() == now.day() { + format!("Today at {}", input_time.format("%H:%M %p")).into() + } else if input_time.day() == now.day() - 1 { + format!("Yesterday at {}", input_time.format("%H:%M %p")).into() + } else { + format!( + "{}, {}", + input_time.format("%d/%m/%y"), + input_time.format("%H:%M %p") + ) + .into() + } + } + + pub fn set(&mut self, created_at: Timestamp) { + self.0 = created_at + } +} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index fa33fb5..38b803f 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -1,4 +1,5 @@ pub mod constants; +pub mod last_seen; pub mod profile; pub mod qr; pub mod utils; diff --git a/crates/ui/src/dock_area/dock.rs b/crates/ui/src/dock_area/dock.rs index 5ffff81..f711900 100644 --- a/crates/ui/src/dock_area/dock.rs +++ b/crates/ui/src/dock_area/dock.rs @@ -1,3 +1,11 @@ +use gpui::{ + div, prelude::FluentBuilder as _, px, AnyView, App, AppContext, Axis, Context, Element, Entity, + InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, + Point, Render, StatefulInteractiveElement, Style, Styled as _, WeakEntity, Window, +}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + use super::{DockArea, DockItem}; use crate::{ dock_area::{panel::PanelView, tab_panel::TabPanel}, @@ -5,14 +13,6 @@ use crate::{ theme::{scale::ColorScaleStep, ActiveTheme as _}, AxisExt as _, StyledExt, }; -use gpui::{ - div, prelude::FluentBuilder as _, px, AnyView, App, AppContext, Axis, Context, Element, Entity, - InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, - Point, Render, StatefulInteractiveElement, Style, StyleRefinement, Styled as _, WeakEntity, - Window, -}; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; #[derive(Clone, Render)] struct ResizePanel; @@ -296,6 +296,7 @@ impl Dock { .upgrade() .expect("DockArea is missing") .read(cx); + let area_bounds = dock_area.bounds; let mut left_dock_size = Pixels(0.0); let mut right_dock_size = Pixels(0.0); @@ -326,6 +327,7 @@ impl Dock { DockPlacement::Bottom => area_bounds.bottom() - mouse_position.y, DockPlacement::Center => unreachable!(), }; + match self.placement { DockPlacement::Left => { let max_size = area_bounds.size.width - PANEL_MIN_SIZE - right_dock_size; @@ -356,7 +358,7 @@ impl Render for Dock { return div(); } - let cache_style = StyleRefinement::default().v_flex().size_full(); + let cache_style = gpui::StyleRefinement::default().v_flex().size_full(); div() .relative() diff --git a/crates/ui/src/dock_area/panel.rs b/crates/ui/src/dock_area/panel.rs index 5b83de6..497c311 100644 --- a/crates/ui/src/dock_area/panel.rs +++ b/crates/ui/src/dock_area/panel.rs @@ -1,7 +1,7 @@ use crate::{button::Button, popup_menu::PopupMenu}; use gpui::{ - AnyElement, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, Hsla, IntoElement, - Render, SharedString, Window, + AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Render, + SharedString, Window, }; pub enum PanelEvent { @@ -31,14 +31,9 @@ pub trait Panel: EventEmitter + Render + Focusable { /// Once you have defined a panel id, this must not be changed. fn panel_id(&self) -> SharedString; - /// The optional facepile of the panel - fn panel_facepile(&self, _cx: &App) -> Option> { - None - } - /// The title of the panel fn title(&self, _cx: &App) -> AnyElement { - SharedString::from("Unamed").into_any_element() + SharedString::from("Unnamed").into_any() } /// Whether the panel can be closed, default is `true`. @@ -85,7 +80,6 @@ pub trait Panel: EventEmitter + Render + Focusable { pub trait PanelView: 'static + Send + Sync { fn panel_id(&self, cx: &App) -> SharedString; - fn panel_facepile(&self, cx: &App) -> Option>; fn title(&self, cx: &App) -> AnyElement; fn closable(&self, cx: &App) -> bool; fn zoomable(&self, cx: &App) -> bool; @@ -103,10 +97,6 @@ impl PanelView for Entity { self.read(cx).panel_id() } - fn panel_facepile(&self, cx: &App) -> Option> { - self.read(cx).panel_facepile(cx) - } - fn title(&self, cx: &App) -> AnyElement { self.read(cx).title(cx) } diff --git a/crates/ui/src/dock_area/tab_panel.rs b/crates/ui/src/dock_area/tab_panel.rs index 94ea1d7..c46a532 100644 --- a/crates/ui/src/dock_area/tab_panel.rs +++ b/crates/ui/src/dock_area/tab_panel.rs @@ -12,11 +12,10 @@ use crate::{ v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt, }; use gpui::{ - div, img, prelude::FluentBuilder, px, rems, App, AppContext, Context, Corner, DefiniteLength, + div, prelude::FluentBuilder, px, rems, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, - InteractiveElement as _, IntoElement, ObjectFit, ParentElement, Pixels, Render, ScrollHandle, - SharedString, StatefulInteractiveElement, StyleRefinement, Styled, StyledImage, WeakEntity, - Window, + InteractiveElement as _, IntoElement, ParentElement, Pixels, Render, ScrollHandle, + SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window, }; use std::sync::Arc; @@ -591,30 +590,8 @@ impl TabPanel { .child( div() .w_full() - .flex() - .items_center() - .gap_1() .text_ellipsis() .text_xs() - .when_some(panel.panel_facepile(cx), |this, facepill| { - this.child( - div() - .flex() - .flex_row_reverse() - .items_center() - .justify_start() - .children(facepill.into_iter().enumerate().rev().map( - |(ix, face)| { - div().when(ix > 0, |div| div.ml_neg_1()).child( - img(face) - .size_4() - .rounded_full() - .object_fit(ObjectFit::Cover), - ) - }, - )), - ) - }) .child(panel.title(cx)), ) .when(state.draggable, |this| { @@ -675,7 +652,7 @@ impl TabPanel { } Some( - Tab::new(("tab", ix), panel.title(cx), panel.panel_facepile(cx)) + Tab::new(("tab", ix), panel.title(cx)) .py_2() .selected(active) .disabled(disabled) @@ -783,7 +760,7 @@ impl TabPanel { .child( active_panel .view() - .cached(StyleRefinement::default().v_flex().size_full()), + .cached(gpui::StyleRefinement::default().v_flex().size_full()), ), ) .when(state.droppable, |this| { diff --git a/crates/ui/src/tab/mod.rs b/crates/ui/src/tab/mod.rs index c981914..aca0d22 100644 --- a/crates/ui/src/tab/mod.rs +++ b/crates/ui/src/tab/mod.rs @@ -11,7 +11,6 @@ pub struct Tab { id: ElementId, base: Stateful
, label: AnyElement, - facepill: Option>, prefix: Option, suffix: Option, disabled: bool, @@ -19,11 +18,7 @@ pub struct Tab { } impl Tab { - pub fn new( - id: impl Into, - label: impl IntoElement, - facepill: Option>, - ) -> Self { + pub fn new(id: impl Into, label: impl IntoElement) -> Self { let id: ElementId = id.into(); Self { @@ -34,7 +29,6 @@ impl Tab { selected: false, prefix: None, suffix: None, - facepill, } } @@ -132,25 +126,6 @@ impl RenderOnce for Tab { .gap_1() .text_ellipsis() .text_xs() - .when_some(self.facepill, |this, facepill| { - this.child( - div() - .flex() - .flex_row_reverse() - .items_center() - .justify_start() - .children(facepill.into_iter().enumerate().rev().map( - |(ix, face)| { - div().when(ix > 0, |div| div.ml_neg_1()).child( - img(face) - .size_4() - .rounded_full() - .object_fit(ObjectFit::Cover), - ) - }, - )), - ) - }) .child(self.label), ) .when_some(self.suffix, |this, suffix| this.child(suffix))