feat: improve message list

This commit is contained in:
2025-01-22 09:04:31 +07:00
parent 582db29209
commit 0d445dfca1
9 changed files with 134 additions and 76 deletions

3
assets/icons/info.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fill="#000" fill-rule="evenodd" d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm-2 9a.75.75 0 0 1 .75-.75H12a.75.75 0 0 1 .75.75v5.25a.75.75 0 0 1-1.5 0v-4.5h-.5A.75.75 0 0 1 10 11Zm2-3.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z" clip-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 396 B

View File

@@ -11,6 +11,12 @@ pub struct Member {
metadata: Metadata, metadata: Metadata,
} }
impl PartialEq for Member {
fn eq(&self, other: &Self) -> bool {
self.public_key() == other.public_key()
}
}
impl Member { impl Member {
pub fn new(public_key: PublicKey, metadata: Metadata) -> Self { pub fn new(public_key: PublicKey, metadata: Metadata) -> Self {
Self { Self {

View File

@@ -15,6 +15,16 @@ pub struct Message {
ago: SharedString, 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 { impl Message {
pub fn new(member: Member, content: SharedString, ago: SharedString) -> Self { pub fn new(member: Member, content: SharedString, ago: SharedString) -> Self {
Self { Self {

View File

@@ -17,7 +17,7 @@ use nostr_sdk::prelude::*;
use smol::fs; use smol::fs;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use ui::{ use ui::{
button::{Button, ButtonVariants}, button::{Button, ButtonRounded, ButtonVariants},
dock_area::{ dock_area::{
panel::{Panel, PanelEvent}, panel::{Panel, PanelEvent},
state::PanelState, state::PanelState,
@@ -26,7 +26,7 @@ use ui::{
popup_menu::PopupMenu, popup_menu::PopupMenu,
prelude::FluentBuilder, prelude::FluentBuilder,
theme::{scale::ColorScaleStep, ActiveTheme}, theme::{scale::ColorScaleStep, ActiveTheme},
v_flex, Icon, IconName, v_flex, ContextModal, Icon, IconName, Sizable,
}; };
mod message; mod message;
@@ -73,7 +73,6 @@ impl ChatPanel {
.appearance(false) .appearance(false)
.text_size(ui::Size::Small) .text_size(ui::Size::Small)
.placeholder("Message...") .placeholder("Message...")
.cleanable()
}); });
// List // List
@@ -235,7 +234,18 @@ impl ChatPanel {
.collect(); .collect();
cx.update_model(&self.state, |model, cx| { cx.update_model(&self.state, |model, cx| {
model.items.extend(items); let messages: Vec<Message> = 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(); model.count = model.items.len();
cx.notify(); cx.notify();
}); });
@@ -245,10 +255,17 @@ impl ChatPanel {
fn send_message(&mut self, view: WeakView<TextInput>, cx: &mut ViewContext<Self>) { fn send_message(&mut self, view: WeakView<TextInput>, cx: &mut ViewContext<Self>) {
let room = self.room.read(cx); let room = self.room.read(cx);
let owner = room.owner.clone(); let owner = room.owner.clone();
let members = room.members.to_vec(); let mut members = room.members.to_vec();
members.push(owner.clone());
// Get message // Get message
let mut content = self.input.read(cx).text().to_string(); 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 // Get all attaches and merge with message
if let Some(attaches) = self.attaches.read(cx).as_ref() { if let Some(attaches) = self.attaches.read(cx).as_ref() {
let merged = attaches let merged = attaches
@@ -260,59 +277,68 @@ impl ChatPanel {
content = format!("{}\n{}", content, merged) content = format!("{}\n{}", content, merged)
} }
// Async // Update input state
let async_state = self.state.clone(); if let Some(input) = view.upgrade() {
let mut async_cx = cx.to_async(); cx.update_view(&input, |input, cx| {
input.set_loading(true, cx);
input.set_disabled(true, cx);
});
}
cx.foreground_executor() cx.spawn(|this, mut async_cx| async move {
.spawn(async move { // Send message to all members
// Send message to all members async_cx
async_cx .background_executor()
.background_executor() .spawn({
.spawn({ let client = get_client();
let client = get_client(); let content = content.clone().to_string();
let content = content.clone().to_string(); let tags: Vec<Tag> = members
let tags: Vec<Tag> = members .iter()
.iter() .filter_map(|m| {
.filter_map(|m| { if m.public_key() != owner.public_key() {
if m.public_key() != owner.public_key() { Some(Tag::public_key(m.public_key()))
Some(Tag::public_key(m.public_key())) } else {
} else { None
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
} }
})
.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() { if let Some(input) = view.upgrade() {
_ = async_cx.update_view(&input, |input, cx| { _ = async_cx.update_view(&input, |input, cx| {
input.set_text("", cx); input.set_loading(false, cx);
}); input.set_disabled(false, cx);
} input.set_text("", cx);
}) });
.detach(); }
})
.detach();
} }
fn upload(&mut self, cx: &mut ViewContext<Self>) { fn upload(&mut self, cx: &mut ViewContext<Self>) {
@@ -501,10 +527,23 @@ impl Render for ChatPanel {
div() div()
.flex_1() .flex_1()
.flex() .flex()
.items_center()
.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)) .bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
.rounded(px(cx.theme().radius)) .rounded(px(cx.theme().radius))
.px_2() .pl_2()
.child(self.input.clone()), .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)
})),
),
), ),
), ),
), ),

View File

@@ -497,6 +497,7 @@ impl ButtonVariant {
_ => cx.theme().accent.step(cx, ColorScaleStep::ONE), _ => cx.theme().accent.step(cx, ColorScaleStep::ONE),
}, },
ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::NINE), ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::NINE),
ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
ButtonVariant::Custom(colors) => colors.foreground, ButtonVariant::Custom(colors) => colors.foreground,
_ => cx.theme().base.step(cx, ColorScaleStep::TWELVE), _ => cx.theme().base.step(cx, ColorScaleStep::TWELVE),
} }
@@ -543,13 +544,14 @@ impl ButtonVariant {
fn hovered(&self, cx: &WindowContext) -> ButtonVariantStyle { fn hovered(&self, cx: &WindowContext) -> ButtonVariantStyle {
let bg = match self { let bg = match self {
ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::TEN), 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::Link => cx.theme().transparent,
ButtonVariant::Text => cx.theme().transparent, ButtonVariant::Text => cx.theme().transparent,
ButtonVariant::Custom(colors) => colors.hover, ButtonVariant::Custom(colors) => colors.hover,
}; };
let border = self.border_color(cx); let border = self.border_color(cx);
let fg = match self { let fg = match self {
ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::TWELVE),
ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::TEN), ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::TEN),
_ => self.text_color(cx), _ => self.text_color(cx),
}; };

View File

@@ -65,7 +65,7 @@ impl RenderOnce for Divider {
}) })
.bg(self .bg(self
.color .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| { .when_some(self.label, |this, label| {
this.child( this.child(

View File

@@ -274,7 +274,7 @@ impl Dock {
}) })
.child( .child(
div() 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_horizontal(), |this| this.h_full().w(HANDLE_SIZE))
.when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)), .when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)),
) )

View File

@@ -1389,16 +1389,12 @@ impl Render for TextInput {
.cursor_text() .cursor_text()
.when(self.multi_line, |this| this.h_auto()) .when(self.multi_line, |this| this.h_auto())
.when(self.appearance, |this| { .when(self.appearance, |this| {
this.bg(if self.disabled { this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
cx.theme().transparent .rounded(px(cx.theme().radius))
} else { .when(cx.theme().shadow, |this| this.shadow_sm())
cx.theme().base.step(cx, ColorScaleStep::THREE) .when(focused, |this| this.outline(cx))
}) .when(prefix.is_none(), |this| this.input_pl(self.size))
.rounded(px(cx.theme().radius)) .when(suffix.is_none(), |this| this.input_pr(self.size))
.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) .children(prefix)
.gap_1() .gap_1()

View File

@@ -200,8 +200,11 @@ impl Notification {
.detach() .detach()
} }
} }
impl EventEmitter<DismissEvent> for Notification {} impl EventEmitter<DismissEvent> for Notification {}
impl FluentBuilder for Notification {} impl FluentBuilder for Notification {}
impl Render for Notification { impl Render for Notification {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let closing = self.closing; let closing = self.closing;
@@ -209,15 +212,15 @@ impl Render for Notification {
Some(icon) => icon, Some(icon) => icon,
None => match self.type_ { None => match self.type_ {
NotificationType::Info => { 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 => { 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) 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) 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("") .group("")
.occlude() .occlude()
.relative() .relative()
.w_96() .w_72()
.border_1() .border_1()
.border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE)) .border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE))
.bg(cx.theme().background) .bg(cx.theme().background)
.rounded_md() .rounded(px(cx.theme().radius))
.shadow_md() .shadow_md()
.py_2() .p_2()
.px_4()
.gap_3() .gap_3()
.child(div().absolute().top_3().left_4().child(icon)) .child(div().absolute().top_3().left_2().child(icon))
.child( .child(
v_flex() v_flex()
.pl_6() .pl_6()