diff --git a/assets/icons/info.svg b/assets/icons/info.svg new file mode 100644 index 0000000..1b3641a --- /dev/null +++ b/assets/icons/info.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/app/src/states/chat/room.rs b/crates/app/src/states/chat/room.rs index 72769dd..c67a9bb 100644 --- a/crates/app/src/states/chat/room.rs +++ b/crates/app/src/states/chat/room.rs @@ -11,6 +11,12 @@ pub struct Member { metadata: Metadata, } +impl PartialEq for Member { + fn eq(&self, other: &Self) -> bool { + self.public_key() == other.public_key() + } +} + impl Member { pub fn new(public_key: PublicKey, metadata: Metadata) -> Self { Self { diff --git a/crates/app/src/views/chat/message.rs b/crates/app/src/views/chat/message.rs index 80f42c5..d48de6d 100644 --- a/crates/app/src/views/chat/message.rs +++ b/crates/app/src/views/chat/message.rs @@ -15,6 +15,16 @@ pub struct Message { ago: SharedString, } +impl PartialEq for Message { + fn eq(&self, other: &Self) -> bool { + let content = self.content == other.content; + let member = self.member == other.member; + let ago = self.ago == other.ago; + + content && member && ago + } +} + impl Message { pub fn new(member: Member, content: SharedString, ago: SharedString) -> Self { Self { diff --git a/crates/app/src/views/chat/mod.rs b/crates/app/src/views/chat/mod.rs index e384f05..573320c 100644 --- a/crates/app/src/views/chat/mod.rs +++ b/crates/app/src/views/chat/mod.rs @@ -17,7 +17,7 @@ use nostr_sdk::prelude::*; use smol::fs; use tokio::sync::oneshot; use ui::{ - button::{Button, ButtonVariants}, + button::{Button, ButtonRounded, ButtonVariants}, dock_area::{ panel::{Panel, PanelEvent}, state::PanelState, @@ -26,7 +26,7 @@ use ui::{ popup_menu::PopupMenu, prelude::FluentBuilder, theme::{scale::ColorScaleStep, ActiveTheme}, - v_flex, Icon, IconName, + v_flex, ContextModal, Icon, IconName, Sizable, }; mod message; @@ -73,7 +73,6 @@ impl ChatPanel { .appearance(false) .text_size(ui::Size::Small) .placeholder("Message...") - .cleanable() }); // List @@ -235,7 +234,18 @@ impl ChatPanel { .collect(); cx.update_model(&self.state, |model, cx| { - model.items.extend(items); + let messages: Vec = items + .into_iter() + .filter_map(|new| { + if !model.items.iter().any(|old| old == &new) { + Some(new) + } else { + None + } + }) + .collect(); + + model.items.extend(messages); model.count = model.items.len(); cx.notify(); }); @@ -245,10 +255,17 @@ impl ChatPanel { fn send_message(&mut self, view: WeakView, cx: &mut ViewContext) { let room = self.room.read(cx); let owner = room.owner.clone(); - let members = room.members.to_vec(); + let mut members = room.members.to_vec(); + members.push(owner.clone()); // Get message let mut content = self.input.read(cx).text().to_string(); + + if content.is_empty() { + cx.push_notification("Message cannot be empty"); + return; + } + // Get all attaches and merge with message if let Some(attaches) = self.attaches.read(cx).as_ref() { let merged = attaches @@ -260,59 +277,68 @@ impl ChatPanel { content = format!("{}\n{}", content, merged) } - // Async - let async_state = self.state.clone(); - let mut async_cx = cx.to_async(); + // Update input state + if let Some(input) = view.upgrade() { + cx.update_view(&input, |input, cx| { + input.set_loading(true, cx); + input.set_disabled(true, cx); + }); + } - cx.foreground_executor() - .spawn(async move { - // Send message to all members - async_cx - .background_executor() - .spawn({ - let client = get_client(); - let content = content.clone().to_string(); - let tags: Vec = members - .iter() - .filter_map(|m| { - if m.public_key() != owner.public_key() { - Some(Tag::public_key(m.public_key())) - } else { - None - } - }) - .collect(); - - async move { - // Send message to all members - for member in members.iter() { - _ = client - .send_private_msg(member.public_key(), &content, tags.clone()) - .await + cx.spawn(|this, mut async_cx| async move { + // Send message to all members + async_cx + .background_executor() + .spawn({ + let client = get_client(); + let content = content.clone().to_string(); + let tags: Vec = members + .iter() + .filter_map(|m| { + if m.public_key() != owner.public_key() { + Some(Tag::public_key(m.public_key())) + } else { + None } + }) + .collect(); + + async move { + // Send message to all members + for member in members.iter() { + _ = client + .send_private_msg(member.public_key(), &content, tags.clone()) + .await } + } + }) + .detach(); + + if let Some(view) = this.upgrade() { + _ = async_cx.update_view(&view, |this, cx| { + cx.update_model(&this.state, |model, cx| { + let message = Message::new( + owner, + content.to_string().into(), + message_time(Timestamp::now()).into(), + ); + + model.items.extend(vec![message]); + model.count = model.items.len(); + cx.notify(); }) - .detach(); - - _ = async_cx.update_model(&async_state, |model, cx| { - let message = Message::new( - owner, - content.to_string().into(), - message_time(Timestamp::now()).into(), - ); - - model.items.extend(vec![message]); - model.count = model.items.len(); - cx.notify(); }); + } - if let Some(input) = view.upgrade() { - _ = async_cx.update_view(&input, |input, cx| { - input.set_text("", cx); - }); - } - }) - .detach(); + if let Some(input) = view.upgrade() { + _ = async_cx.update_view(&input, |input, cx| { + input.set_loading(false, cx); + input.set_disabled(false, cx); + input.set_text("", cx); + }); + } + }) + .detach(); } fn upload(&mut self, cx: &mut ViewContext) { @@ -501,10 +527,23 @@ impl Render for ChatPanel { div() .flex_1() .flex() + .items_center() .bg(cx.theme().base.step(cx, ColorScaleStep::THREE)) .rounded(px(cx.theme().radius)) - .px_2() - .child(self.input.clone()), + .pl_2() + .pr_1() + .child(self.input.clone()) + .child( + Button::new("send") + .ghost() + .xsmall() + .bold() + .rounded(ButtonRounded::Medium) + .label("SEND") + .on_click(cx.listener(|this, _, cx| { + this.send_message(this.input.downgrade(), cx) + })), + ), ), ), ), diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index c6dc325..5fc046f 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -497,6 +497,7 @@ impl ButtonVariant { _ => cx.theme().accent.step(cx, ColorScaleStep::ONE), }, ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::NINE), + ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::ELEVEN), ButtonVariant::Custom(colors) => colors.foreground, _ => cx.theme().base.step(cx, ColorScaleStep::TWELVE), } @@ -543,13 +544,14 @@ impl ButtonVariant { fn hovered(&self, cx: &WindowContext) -> ButtonVariantStyle { let bg = match self { ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::TEN), - ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::THREE), + ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::FOUR), ButtonVariant::Link => cx.theme().transparent, ButtonVariant::Text => cx.theme().transparent, ButtonVariant::Custom(colors) => colors.hover, }; let border = self.border_color(cx); let fg = match self { + ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::TWELVE), ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::TEN), _ => self.text_color(cx), }; diff --git a/crates/ui/src/divider.rs b/crates/ui/src/divider.rs index 82771bf..2f82849 100644 --- a/crates/ui/src/divider.rs +++ b/crates/ui/src/divider.rs @@ -65,7 +65,7 @@ impl RenderOnce for Divider { }) .bg(self .color - .unwrap_or(cx.theme().base.step(cx, ColorScaleStep::THREE))), + .unwrap_or(cx.theme().base.step(cx, ColorScaleStep::FIVE))), ) .when_some(self.label, |this, label| { this.child( diff --git a/crates/ui/src/dock_area/dock.rs b/crates/ui/src/dock_area/dock.rs index ebab635..01fb80d 100644 --- a/crates/ui/src/dock_area/dock.rs +++ b/crates/ui/src/dock_area/dock.rs @@ -274,7 +274,7 @@ impl Dock { }) .child( div() - .bg(cx.theme().base.step(cx, ColorScaleStep::THREE)) + .bg(cx.theme().base.step(cx, ColorScaleStep::FIVE)) .when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE)) .when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)), ) diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index a8fece9..ddc55b1 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -1389,16 +1389,12 @@ impl Render for TextInput { .cursor_text() .when(self.multi_line, |this| this.h_auto()) .when(self.appearance, |this| { - this.bg(if self.disabled { - cx.theme().transparent - } else { - cx.theme().base.step(cx, ColorScaleStep::THREE) - }) - .rounded(px(cx.theme().radius)) - .when(cx.theme().shadow, |this| this.shadow_sm()) - .when(focused, |this| this.outline(cx)) - .when(prefix.is_none(), |this| this.input_pl(self.size)) - .when(suffix.is_none(), |this| this.input_pr(self.size)) + this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)) + .rounded(px(cx.theme().radius)) + .when(cx.theme().shadow, |this| this.shadow_sm()) + .when(focused, |this| this.outline(cx)) + .when(prefix.is_none(), |this| this.input_pl(self.size)) + .when(suffix.is_none(), |this| this.input_pr(self.size)) }) .children(prefix) .gap_1() diff --git a/crates/ui/src/notification.rs b/crates/ui/src/notification.rs index 2baf943..73e6734 100644 --- a/crates/ui/src/notification.rs +++ b/crates/ui/src/notification.rs @@ -200,8 +200,11 @@ impl Notification { .detach() } } + impl EventEmitter for Notification {} + impl FluentBuilder for Notification {} + impl Render for Notification { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let closing = self.closing; @@ -209,15 +212,15 @@ impl Render for Notification { Some(icon) => icon, None => match self.type_ { NotificationType::Info => { - Icon::new(IconName::Info).text_color(blue().step(cx, ColorScaleStep::FIVE)) + Icon::new(IconName::Info).text_color(blue().step(cx, ColorScaleStep::NINE)) } NotificationType::Error => { - Icon::new(IconName::CircleX).text_color(red().step(cx, ColorScaleStep::FIVE)) + Icon::new(IconName::CircleX).text_color(red().step(cx, ColorScaleStep::NINE)) } NotificationType::Success => Icon::new(IconName::CircleCheck) - .text_color(green().step(cx, ColorScaleStep::FIVE)), + .text_color(green().step(cx, ColorScaleStep::NINE)), NotificationType::Warning => Icon::new(IconName::TriangleAlert) - .text_color(yellow().step(cx, ColorScaleStep::FIVE)), + .text_color(yellow().step(cx, ColorScaleStep::NINE)), }, }; @@ -226,16 +229,15 @@ impl Render for Notification { .group("") .occlude() .relative() - .w_96() + .w_72() .border_1() .border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE)) .bg(cx.theme().background) - .rounded_md() + .rounded(px(cx.theme().radius)) .shadow_md() - .py_2() - .px_4() + .p_2() .gap_3() - .child(div().absolute().top_3().left_4().child(icon)) + .child(div().absolute().top_3().left_2().child(icon)) .child( v_flex() .pl_6()