feat: Reply or Reference a specific message (#39)
* add reply to when send message * show reply message * refactor * multiple quote
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use async_utility::task::spawn;
|
||||
use chats::{
|
||||
message::{Message, RoomMessage},
|
||||
room::Room,
|
||||
message::Message,
|
||||
room::{Room, SendError},
|
||||
ChatRegistry,
|
||||
};
|
||||
use common::{nip96_upload, profile::SharedProfile};
|
||||
@@ -35,8 +35,6 @@ use ui::{
|
||||
|
||||
use crate::views::subject;
|
||||
|
||||
const DESC: &str = "This conversation is private. Only members can see each other's messages.";
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||
pub struct ChangeSubject(pub String);
|
||||
|
||||
@@ -56,11 +54,12 @@ pub struct Chat {
|
||||
focus_handle: FocusHandle,
|
||||
// Chat Room
|
||||
room: Entity<Room>,
|
||||
messages: Entity<Vec<RoomMessage>>,
|
||||
messages: Entity<Vec<Rc<RefCell<Message>>>>,
|
||||
text_data: HashMap<EventId, RichText>,
|
||||
list_state: ListState,
|
||||
// New Message
|
||||
input: Entity<InputState>,
|
||||
replies_to: Entity<Option<Vec<Message>>>,
|
||||
// Media Attachment
|
||||
attaches: Entity<Option<Vec<Url>>>,
|
||||
uploading: bool,
|
||||
@@ -70,17 +69,30 @@ pub struct Chat {
|
||||
|
||||
impl Chat {
|
||||
pub fn new(id: &u64, room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let messages = cx.new(|_| vec![RoomMessage::announcement()]);
|
||||
let attaches = cx.new(|_| None);
|
||||
let replies_to = cx.new(|_| None);
|
||||
|
||||
let messages = cx.new(|_| {
|
||||
let message = Message::builder()
|
||||
.content(
|
||||
"This conversation is private. Only members can see each other's messages."
|
||||
.into(),
|
||||
)
|
||||
.build_rc()
|
||||
.unwrap();
|
||||
|
||||
vec![message]
|
||||
});
|
||||
|
||||
let input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("Message...")
|
||||
.multi_line()
|
||||
.prevent_new_line_on_enter()
|
||||
.rows(1)
|
||||
.max_rows(20)
|
||||
.auto_grow()
|
||||
.clean_on_escape()
|
||||
.max_rows(20)
|
||||
});
|
||||
|
||||
cx.new(|cx| {
|
||||
@@ -102,25 +114,13 @@ impl Chat {
|
||||
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(&room, window, move |this, _, incoming, _w, cx| {
|
||||
let created_at = &incoming.0.created_at.to_string()[..5];
|
||||
let content = incoming.0.content.as_str();
|
||||
let author = incoming.0.author.public_key();
|
||||
|
||||
// Check if the incoming message is the same as the new message created by optimistic update
|
||||
if this.messages.read(cx).iter().any(|msg| {
|
||||
if let RoomMessage::User(m) = msg {
|
||||
created_at == &m.created_at.to_string()[..5]
|
||||
&& m.content == content
|
||||
&& m.author.public_key() == author
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
if this.prevent_duplicate_message(&incoming.0, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
let old_len = this.messages.read(cx).len();
|
||||
let message = RoomMessage::user(incoming.0.clone());
|
||||
let message = incoming.0.clone().into_rc();
|
||||
|
||||
cx.update_entity(&this.messages, |this, cx| {
|
||||
this.extend(vec![message]);
|
||||
@@ -152,6 +152,7 @@ impl Chat {
|
||||
messages,
|
||||
list_state,
|
||||
input,
|
||||
replies_to,
|
||||
attaches,
|
||||
subscriptions,
|
||||
}
|
||||
@@ -161,18 +162,18 @@ impl Chat {
|
||||
/// Load all messages belonging to this room
|
||||
pub(crate) fn load_messages(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let room = self.room.read(cx);
|
||||
let task = room.load_messages(cx);
|
||||
let load_messages = room.load_messages(cx);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match task.await {
|
||||
Ok(events) => {
|
||||
match load_messages.await {
|
||||
Ok(messages) => {
|
||||
this.update(cx, |this, cx| {
|
||||
let old_len = this.messages.read(cx).len();
|
||||
let new_len = events.len();
|
||||
let new_len = messages.len();
|
||||
|
||||
// Extend the messages list with the new events
|
||||
this.messages.update(cx, |this, cx| {
|
||||
this.extend(events);
|
||||
this.extend(messages.into_iter().map(|e| e.into_rc()));
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
@@ -216,21 +217,42 @@ impl Chat {
|
||||
content
|
||||
}
|
||||
|
||||
fn prevent_duplicate_message(&self, new_msg: &Message, cx: &Context<Self>) -> bool {
|
||||
let min_timestamp = new_msg.created_at.as_u64().saturating_sub(2);
|
||||
|
||||
self.messages.read(cx).iter().any(|existing| {
|
||||
let existing = existing.borrow();
|
||||
// Check if messages are within the time window
|
||||
(existing.created_at.as_u64() >= min_timestamp) &&
|
||||
// Compare content and author
|
||||
(existing.content == new_msg.content) &&
|
||||
(existing.author == new_msg.author)
|
||||
})
|
||||
}
|
||||
|
||||
fn send_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.input.update(cx, |this, cx| {
|
||||
this.set_loading(true, cx);
|
||||
this.set_disabled(true, cx);
|
||||
});
|
||||
|
||||
// Get the message which includes all attachments
|
||||
let content = self.message(cx);
|
||||
// Get replies_to if it's present
|
||||
let replies = self.replies_to.read(cx).as_ref();
|
||||
// Get the current room entity
|
||||
let room = self.room.read(cx);
|
||||
let temp_message = room.create_temp_message(&content, cx);
|
||||
let send_message = room.send_in_background(&content, cx);
|
||||
// Create a temporary message for optimistic update
|
||||
let temp_message = room.create_temp_message(&content, replies, cx);
|
||||
// Create a task for sending the message in the background
|
||||
let send_message = room.send_in_background(&content, replies, cx);
|
||||
|
||||
if let Some(message) = temp_message {
|
||||
let id = message.id;
|
||||
// Optimistically update message list
|
||||
self.push_user_message(message, cx);
|
||||
self.insert_message(message, cx);
|
||||
// Remove all replies
|
||||
self.remove_all_replies(cx);
|
||||
|
||||
// Reset the input state
|
||||
self.input.update(cx, |this, cx| {
|
||||
@@ -245,16 +267,10 @@ impl Chat {
|
||||
if !reports.is_empty() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.messages.update(cx, |this, cx| {
|
||||
if let Some(msg) = this.iter_mut().find(|msg| {
|
||||
if let RoomMessage::User(m) = msg {
|
||||
m.id == id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
if let Some(msg) = id.and_then(|id| {
|
||||
this.iter().find(|msg| msg.borrow().id == Some(id)).cloned()
|
||||
}) {
|
||||
if let RoomMessage::User(this) = msg {
|
||||
this.errors = Some(reports)
|
||||
}
|
||||
msg.borrow_mut().errors = Some(reports);
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
@@ -267,9 +283,9 @@ impl Chat {
|
||||
}
|
||||
}
|
||||
|
||||
fn push_user_message(&self, message: Message, cx: &mut Context<Self>) {
|
||||
fn insert_message(&self, message: Message, cx: &mut Context<Self>) {
|
||||
let old_len = self.messages.read(cx).len();
|
||||
let message = RoomMessage::user(message);
|
||||
let message = message.into_rc();
|
||||
|
||||
cx.update_entity(&self.messages, |this, cx| {
|
||||
this.extend(vec![message]);
|
||||
@@ -279,17 +295,44 @@ impl Chat {
|
||||
self.list_state.splice(old_len..old_len, 1);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn push_system_message(&self, content: String, cx: &mut Context<Self>) {
|
||||
let old_len = self.messages.read(cx).len();
|
||||
let message = RoomMessage::system(content.into());
|
||||
fn scroll_to(&self, id: EventId, cx: &Context<Self>) {
|
||||
if let Some(ix) = self
|
||||
.messages
|
||||
.read(cx)
|
||||
.iter()
|
||||
.position(|m| m.borrow().id == Some(id))
|
||||
{
|
||||
self.list_state.scroll_to_reveal_item(ix);
|
||||
}
|
||||
}
|
||||
|
||||
cx.update_entity(&self.messages, |this, cx| {
|
||||
this.extend(vec![message]);
|
||||
fn reply(&mut self, message: Message, cx: &mut Context<Self>) {
|
||||
self.replies_to.update(cx, |this, cx| {
|
||||
if let Some(replies) = this {
|
||||
replies.push(message);
|
||||
} else {
|
||||
*this = Some(vec![message])
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
self.list_state.splice(old_len..old_len, 1);
|
||||
fn remove_reply(&mut self, id: EventId, cx: &mut Context<Self>) {
|
||||
self.replies_to.update(cx, |this, cx| {
|
||||
if let Some(replies) = this {
|
||||
if let Some(ix) = replies.iter().position(|m| m.id == Some(id)) {
|
||||
replies.remove(ix);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn remove_all_replies(&mut self, cx: &mut Context<Self>) {
|
||||
self.replies_to.update(cx, |this, cx| {
|
||||
*this = None;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
fn upload_media(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -380,6 +423,90 @@ impl Chat {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_attach(&mut self, url: &Url, cx: &Context<Self>) -> impl IntoElement {
|
||||
let url = url.clone();
|
||||
let path: SharedString = url.to_string().into();
|
||||
|
||||
div()
|
||||
.id(path.clone())
|
||||
.relative()
|
||||
.w_16()
|
||||
.child(
|
||||
img(format!(
|
||||
"{}/?url={}&w=128&h=128&fit=cover&n=-1",
|
||||
IMAGE_SERVICE, path
|
||||
))
|
||||
.size_16()
|
||||
.shadow_lg()
|
||||
.rounded(cx.theme().radius)
|
||||
.object_fit(ObjectFit::ScaleDown),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_neg_2()
|
||||
.right_neg_2()
|
||||
.size_4()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded_full()
|
||||
.bg(red())
|
||||
.child(Icon::new(IconName::Close).size_2().text_color(white())),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.remove_media(&url, window, cx);
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_reply(&mut self, message: &Message, cx: &Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.w_full()
|
||||
.pl_2()
|
||||
.border_l_2()
|
||||
.border_color(cx.theme().element_active)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_baseline()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("Replying to:")
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_accent)
|
||||
.child(message.author.as_ref().unwrap().shared_name()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("remove-reply")
|
||||
.icon(IconName::Close)
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.on_click({
|
||||
let id = message.id.unwrap();
|
||||
cx.listener(move |this, _, _, cx| {
|
||||
this.remove_reply(id, cx);
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.text_sm()
|
||||
.text_ellipsis()
|
||||
.line_clamp(1)
|
||||
.child(message.content.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_message(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
@@ -387,160 +514,182 @@ impl Chat {
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let Some(message) = self.messages.read(cx).get(ix) else {
|
||||
return div().into_element();
|
||||
return div().id(ix);
|
||||
};
|
||||
|
||||
match message {
|
||||
RoomMessage::User(item) => self.render_user_msg(item, window, cx),
|
||||
RoomMessage::System(content) => self.render_system_msg(content, cx),
|
||||
RoomMessage::Announcement => self.render_announcement_msg(cx),
|
||||
}
|
||||
}
|
||||
let message = message.borrow();
|
||||
|
||||
// Message without ID, Author probably the placeholder
|
||||
let (Some(id), Some(author)) = (message.id, message.author.as_ref()) else {
|
||||
return div()
|
||||
.id(ix)
|
||||
.group("")
|
||||
.w_full()
|
||||
.relative()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.w_full()
|
||||
.h_32()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.line_height(relative(1.3))
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_10()
|
||||
.text_color(cx.theme().elevated_surface_background),
|
||||
)
|
||||
.child(message.content.clone());
|
||||
};
|
||||
|
||||
fn render_user_msg(&mut self, item: &Message, window: &mut Window, cx: &Context<Self>) -> Div {
|
||||
let texts = self
|
||||
.text_data
|
||||
.entry(item.id)
|
||||
.or_insert_with(|| RichText::new(item.content.to_owned(), &item.mentions));
|
||||
.entry(id)
|
||||
.or_insert_with(|| RichText::new(message.content.to_string(), &message.mentions));
|
||||
|
||||
div()
|
||||
.id(ix)
|
||||
.group("")
|
||||
.w_full()
|
||||
.relative()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.py_1()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.hover(|this| this.bg(cx.theme().surface_background))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.left_0()
|
||||
.top_0()
|
||||
.w(px(2.))
|
||||
.h_full()
|
||||
.bg(cx.theme().border_transparent)
|
||||
.group_hover("", |this| this.bg(cx.theme().element_active)),
|
||||
)
|
||||
.child(img(item.author.shared_avatar()).size_8().flex_shrink_0())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.flex_initial()
|
||||
.overflow_hidden()
|
||||
.gap_3()
|
||||
.child(img(author.shared_avatar()).size_8().flex_shrink_0())
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.items_baseline()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.flex_col()
|
||||
.flex_initial()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
div()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text)
|
||||
.child(item.author.shared_name()),
|
||||
.flex()
|
||||
.items_baseline()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.child(
|
||||
div()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text)
|
||||
.child(author.shared_name()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(message.ago()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(item.ago()),
|
||||
),
|
||||
)
|
||||
.child(texts.element("body".into(), window, cx))
|
||||
.when_some(item.errors.clone(), |this, errors| {
|
||||
this.child(
|
||||
div()
|
||||
.id("")
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.text_color(gpui::red())
|
||||
.text_xs()
|
||||
.italic()
|
||||
.child(Icon::new(IconName::Info).small())
|
||||
.child("Failed to send message. Click to see details.")
|
||||
.on_click(move |_, window, cx| {
|
||||
let errors = errors.clone();
|
||||
.when_some(message.replies_to.as_ref(), |this, replies| {
|
||||
this.w_full().children({
|
||||
let mut items = vec![];
|
||||
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
this.title("Error Logs").child(
|
||||
div().flex().flex_col().gap_2().px_3().pb_3().children(
|
||||
errors.clone().into_iter().map(|error| {
|
||||
div()
|
||||
.text_sm()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_baseline()
|
||||
.gap_1()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("Send to:")
|
||||
.child(error.profile.shared_name()),
|
||||
)
|
||||
.child(error.message)
|
||||
}),
|
||||
),
|
||||
)
|
||||
});
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
for (ix, id) in replies.iter().enumerate() {
|
||||
if let Some(message) = self
|
||||
.messages
|
||||
.read(cx)
|
||||
.iter()
|
||||
.find(|msg| msg.borrow().id == Some(*id))
|
||||
.cloned()
|
||||
{
|
||||
let message = message.borrow();
|
||||
|
||||
fn render_system_msg(&mut self, content: &SharedString, cx: &Context<Self>) -> Div {
|
||||
div()
|
||||
.group("")
|
||||
.w_full()
|
||||
.relative()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.left_0()
|
||||
.top_0()
|
||||
.w(px(2.))
|
||||
.h_full()
|
||||
.bg(cx.theme().border_transparent)
|
||||
.group_hover("", |this| this.bg(red())),
|
||||
)
|
||||
.child(img("brand/avatar.png").size_8().flex_shrink_0())
|
||||
.text_sm()
|
||||
.text_color(red())
|
||||
.child(content.clone())
|
||||
}
|
||||
items.push(
|
||||
div()
|
||||
.id(ix)
|
||||
.w_full()
|
||||
.px_2()
|
||||
.border_l_2()
|
||||
.border_color(cx.theme().element_selected)
|
||||
.text_sm()
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_accent)
|
||||
.child(
|
||||
message
|
||||
.author
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.shared_name(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.text_ellipsis()
|
||||
.line_clamp(1)
|
||||
.child(message.content.clone()),
|
||||
)
|
||||
.hover(|this| {
|
||||
this.bg(cx
|
||||
.theme()
|
||||
.elevated_surface_background)
|
||||
})
|
||||
.on_click({
|
||||
let id = message.id.unwrap();
|
||||
cx.listener(move |this, _, _, cx| {
|
||||
this.scroll_to(id, cx)
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_announcement_msg(&mut self, cx: &Context<Self>) -> Div {
|
||||
div()
|
||||
.group("")
|
||||
.w_full()
|
||||
.relative()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.w_full()
|
||||
.h_32()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.line_height(relative(1.3))
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_10()
|
||||
.text_color(cx.theme().elevated_surface_background),
|
||||
items
|
||||
})
|
||||
})
|
||||
.child(texts.element("body".into(), window, cx))
|
||||
.when_some(message.errors.clone(), |this, errors| {
|
||||
this.child(
|
||||
div()
|
||||
.id("")
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.text_color(gpui::red())
|
||||
.text_xs()
|
||||
.italic()
|
||||
.child(Icon::new(IconName::Info).small())
|
||||
.child("Failed to send message. Click to see details.")
|
||||
.on_click(move |_, window, cx| {
|
||||
let errors = errors.clone();
|
||||
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
this.title("Error Logs")
|
||||
.child(message_errors(errors.clone(), cx))
|
||||
});
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(DESC)
|
||||
.child(message_border(cx))
|
||||
.child(message_actions(
|
||||
vec![Button::new("reply")
|
||||
.icon(IconName::Reply)
|
||||
.tooltip("Reply")
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click({
|
||||
let message = message.clone();
|
||||
cx.listener(move |this, _, _, cx| {
|
||||
this.reply(message.clone(), cx);
|
||||
})
|
||||
})],
|
||||
cx,
|
||||
))
|
||||
.hover(|this| this.bg(cx.theme().surface_background))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,83 +756,115 @@ impl Render for Chat {
|
||||
.size_full()
|
||||
.child(list(self.list_state.clone()).flex_1())
|
||||
.child(
|
||||
div().flex_shrink_0().px_3().py_2().child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.when_some(self.attaches.read(cx).as_ref(), |this, attaches| {
|
||||
this.gap_1p5().children(attaches.iter().map(|url| {
|
||||
let url = url.clone();
|
||||
let path: SharedString = url.to_string().into();
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.w_full()
|
||||
.relative()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.when_some(self.attaches.read(cx).as_ref(), |this, urls| {
|
||||
this.gap_1p5()
|
||||
.children(urls.iter().map(|url| self.render_attach(url, cx)))
|
||||
})
|
||||
.when_some(self.replies_to.read(cx).as_ref(), |this, messages| {
|
||||
this.gap_1p5().children({
|
||||
let mut items = vec![];
|
||||
|
||||
for message in messages.iter() {
|
||||
items.push(self.render_reply(message, cx));
|
||||
}
|
||||
|
||||
items
|
||||
})
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.id(path.clone())
|
||||
.relative()
|
||||
.w_16()
|
||||
.child(
|
||||
img(format!(
|
||||
"{}/?url={}&w=128&h=128&fit=cover&n=-1",
|
||||
IMAGE_SERVICE, path
|
||||
))
|
||||
.size_16()
|
||||
.shadow_lg()
|
||||
.rounded(cx.theme().radius)
|
||||
.object_fit(ObjectFit::ScaleDown),
|
||||
)
|
||||
.w_full()
|
||||
.flex()
|
||||
.items_end()
|
||||
.gap_2p5()
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_neg_2()
|
||||
.right_neg_2()
|
||||
.size_4()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded_full()
|
||||
.bg(red())
|
||||
.gap_1()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(
|
||||
Icon::new(IconName::Close)
|
||||
.size_2()
|
||||
.text_color(white()),
|
||||
Button::new("upload")
|
||||
.icon(Icon::new(IconName::Upload))
|
||||
.ghost()
|
||||
.disabled(self.uploading)
|
||||
.loading(self.uploading)
|
||||
.on_click(cx.listener(
|
||||
move |this, _, window, cx| {
|
||||
this.upload_media(window, cx);
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
EmojiPicker::new(self.input.downgrade())
|
||||
.icon(IconName::EmojiFill),
|
||||
),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.remove_media(&url, window, cx);
|
||||
}))
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.items_end()
|
||||
.gap_2p5()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(
|
||||
Button::new("upload")
|
||||
.icon(Icon::new(IconName::Upload))
|
||||
.ghost()
|
||||
.disabled(self.uploading)
|
||||
.loading(self.uploading)
|
||||
.on_click(cx.listener(
|
||||
move |this, _, window, cx| {
|
||||
this.upload_media(window, cx);
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
EmojiPicker::new(self.input.downgrade())
|
||||
.icon(IconName::EmojiFill),
|
||||
),
|
||||
)
|
||||
.child(TextInput::new(&self.input)),
|
||||
),
|
||||
),
|
||||
.child(TextInput::new(&self.input)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn message_border(cx: &App) -> Div {
|
||||
div()
|
||||
.group_hover("", |this| this.bg(cx.theme().element_active))
|
||||
.absolute()
|
||||
.left_0()
|
||||
.top_0()
|
||||
.w(px(2.))
|
||||
.h_full()
|
||||
.bg(cx.theme().border_transparent)
|
||||
}
|
||||
|
||||
fn message_errors(errors: Vec<SendError>, cx: &App) -> Div {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.px_3()
|
||||
.pb_3()
|
||||
.children(errors.into_iter().map(|error| {
|
||||
div()
|
||||
.text_sm()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_baseline()
|
||||
.gap_1()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("Send to:")
|
||||
.child(error.profile.shared_name()),
|
||||
)
|
||||
.child(error.message)
|
||||
}))
|
||||
}
|
||||
|
||||
fn message_actions(buttons: Vec<Button>, cx: &App) -> Div {
|
||||
div()
|
||||
.group_hover("", |this| this.visible())
|
||||
.invisible()
|
||||
.absolute()
|
||||
.right_4()
|
||||
.top_neg_2()
|
||||
.shadow_sm()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().background)
|
||||
.p_0p5()
|
||||
.flex()
|
||||
.gap_0p5()
|
||||
.children(buttons)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user