feat: improve message list
This commit is contained in:
3
assets/icons/info.svg
Normal file
3
assets/icons/info.svg
Normal 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 |
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user