, child: Stateful) -> Div {
+ div()
+ .w_6()
+ .bg(cx.theme().colors().element_background)
+ .hover(|style| style.bg(cx.theme().colors().element_hover).rounded_md())
+ .child(child)
+ }
+
fn render_popover_buttons(
&self,
cx: &ViewContext,
message_id: Option,
can_delete_message: bool,
+ can_edit_message: bool,
) -> Div {
- div()
+ h_flex()
.absolute()
- .child(
- div()
- .absolute()
- .right_8()
- .w_6()
- .rounded_tl_md()
- .rounded_bl_md()
- .border_l_1()
- .border_t_1()
- .border_b_1()
- .border_color(cx.theme().colors().element_selected)
- .bg(cx.theme().colors().element_background)
- .hover(|style| style.bg(cx.theme().colors().element_hover))
- .when(!self.has_open_menu(message_id), |el| {
- el.visible_on_hover("")
- })
- .when_some(message_id, |el, message_id| {
- el.child(
+ .right_2()
+ .overflow_hidden()
+ .rounded_md()
+ .border_color(cx.theme().colors().element_selected)
+ .border_1()
+ .when(!self.has_open_menu(message_id), |el| {
+ el.visible_on_hover("")
+ })
+ .bg(cx.theme().colors().element_background)
+ .when_some(message_id, |el, message_id| {
+ el.child(
+ self.render_popover_button(
+ cx,
+ div()
+ .id("reply")
+ .child(
+ IconButton::new(("reply", message_id), IconName::ReplyArrowRight)
+ .on_click(cx.listener(move |this, _, cx| {
+ this.message_editor.update(cx, |editor, cx| {
+ editor.set_reply_to_message_id(message_id);
+ editor.focus_handle(cx).focus(cx);
+ })
+ })),
+ )
+ .tooltip(|cx| Tooltip::text("Reply", cx)),
+ ),
+ )
+ })
+ .when_some(message_id, |el, message_id| {
+ el.when(can_edit_message, |el| {
+ el.child(
+ self.render_popover_button(
+ cx,
div()
- .id("reply")
+ .id("edit")
.child(
- IconButton::new(
- ("reply", message_id),
- IconName::ReplyArrowLeft,
- )
- .on_click(cx.listener(
- move |this, _, cx| {
- this.selected_message_to_reply_id = Some(message_id);
+ IconButton::new(("edit", message_id), IconName::Pencil)
+ .on_click(cx.listener(move |this, _, cx| {
this.message_editor.update(cx, |editor, cx| {
- editor.set_reply_to_message_id(message_id);
- editor.focus_handle(cx).focus(cx);
- })
- },
- )),
- )
- .tooltip(|cx| Tooltip::text("Reply", cx)),
- )
- }),
- )
- .child(
- div()
- .absolute()
- .right_2()
- .w_6()
- .rounded_tr_md()
- .rounded_br_md()
- .border_r_1()
- .border_t_1()
- .border_b_1()
- .border_color(cx.theme().colors().element_selected)
- .bg(cx.theme().colors().element_background)
- .hover(|style| style.bg(cx.theme().colors().element_hover))
- .when(!self.has_open_menu(message_id), |el| {
- el.visible_on_hover("")
- })
- .when_some(message_id, |el, message_id| {
- let this = cx.view().clone();
+ let message = this
+ .active_chat()
+ .and_then(|active_chat| {
+ active_chat
+ .read(cx)
+ .find_loaded_message(message_id)
+ })
+ .cloned();
- el.child(
- div()
- .id("more")
- .child(
- popover_menu(("menu", message_id))
- .trigger(IconButton::new(
- ("trigger", message_id),
- IconName::Ellipsis,
- ))
- .menu(move |cx| {
- Some(Self::render_message_menu(
- &this,
- message_id,
- can_delete_message,
- cx,
- ))
- }),
+ if let Some(message) = message {
+ let buffer = editor
+ .editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .as_singleton()
+ .expect("message editor must be singleton");
+
+ buffer.update(cx, |buffer, cx| {
+ buffer.set_text(message.body.clone(), cx)
+ });
+
+ editor.set_edit_message_id(message_id);
+ editor.focus_handle(cx).focus(cx);
+ }
+ })
+ })),
)
- .tooltip(|cx| Tooltip::text("More", cx)),
- )
- }),
- )
+ .tooltip(|cx| Tooltip::text("Edit", cx)),
+ ),
+ )
+ })
+ })
+ .when_some(message_id, |el, message_id| {
+ let this = cx.view().clone();
+
+ el.child(
+ self.render_popover_button(
+ cx,
+ div()
+ .child(
+ popover_menu(("menu", message_id))
+ .trigger(IconButton::new(
+ ("trigger", message_id),
+ IconName::Ellipsis,
+ ))
+ .menu(move |cx| {
+ Some(Self::render_message_menu(
+ &this,
+ message_id,
+ can_delete_message,
+ cx,
+ ))
+ }),
+ )
+ .id("more")
+ .tooltip(|cx| Tooltip::text("More", cx)),
+ ),
+ )
+ })
}
fn render_message_menu(
@@ -670,18 +707,6 @@ impl ChatPanel {
let menu = {
ContextMenu::build(cx, move |menu, cx| {
menu.entry(
- "Reply to message",
- None,
- cx.handler_for(&this, move |this, cx| {
- this.selected_message_to_reply_id = Some(message_id);
-
- this.message_editor.update(cx, |editor, cx| {
- editor.set_reply_to_message_id(message_id);
- editor.focus_handle(cx).focus(cx);
- })
- }),
- )
- .entry(
"Copy message text",
None,
cx.handler_for(&this, move |this, cx| {
@@ -693,7 +718,7 @@ impl ChatPanel {
}
}),
)
- .when(can_delete_message, move |menu| {
+ .when(can_delete_message, |menu| {
menu.entry(
"Delete message",
None,
@@ -725,22 +750,52 @@ impl ChatPanel {
})
.collect::>();
- rich_text::render_rich_text(message.body.clone(), &mentions, language_registry, None)
+ const MESSAGE_UPDATED: &str = " (edited)";
+
+ let mut body = message.body.clone();
+
+ if message.edited_at.is_some() {
+ body.push_str(MESSAGE_UPDATED);
+ }
+
+ let mut rich_text = rich_text::render_rich_text(body, &mentions, language_registry, None);
+
+ if message.edited_at.is_some() {
+ rich_text.highlights.push((
+ message.body.len()..(message.body.len() + MESSAGE_UPDATED.len()),
+ Highlight::Highlight(HighlightStyle {
+ fade_out: Some(0.8),
+ ..Default::default()
+ }),
+ ));
+ }
+ rich_text
}
fn send(&mut self, _: &Confirm, cx: &mut ViewContext) {
- self.selected_message_to_reply_id = None;
-
if let Some((chat, _)) = self.active_chat.as_ref() {
let message = self
.message_editor
.update(cx, |editor, cx| editor.take_message(cx));
- if let Some(task) = chat
- .update(cx, |chat, cx| chat.send_message(message, cx))
- .log_err()
- {
- task.detach();
+ if let Some(id) = self.message_editor.read(cx).edit_message_id() {
+ self.message_editor.update(cx, |editor, _| {
+ editor.clear_edit_message_id();
+ });
+
+ if let Some(task) = chat
+ .update(cx, |chat, cx| chat.update_message(id, message, cx))
+ .log_err()
+ {
+ task.detach();
+ }
+ } else {
+ if let Some(task) = chat
+ .update(cx, |chat, cx| chat.send_message(message, cx))
+ .log_err()
+ {
+ task.detach();
+ }
}
}
}
@@ -825,16 +880,39 @@ impl ChatPanel {
})
}
- fn close_reply_preview(&mut self, _: &CloseReplyPreview, cx: &mut ViewContext) {
- self.selected_message_to_reply_id = None;
+ fn close_reply_preview(&mut self, cx: &mut ViewContext) {
self.message_editor
.update(cx, |editor, _| editor.clear_reply_to_message_id());
}
+
+ fn cancel_edit_message(&mut self, cx: &mut ViewContext) {
+ self.message_editor.update(cx, |editor, cx| {
+ // only clear the editor input if we were editing a message
+ if editor.edit_message_id().is_none() {
+ return;
+ }
+
+ editor.clear_edit_message_id();
+
+ let buffer = editor
+ .editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .as_singleton()
+ .expect("message editor must be singleton");
+
+ buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
+ });
+ }
}
impl Render for ChatPanel {
fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement {
- let reply_to_message_id = self.message_editor.read(cx).reply_to_message_id();
+ let message_editor = self.message_editor.read(cx);
+
+ let reply_to_message_id = message_editor.reply_to_message_id();
+ let edit_message_id = message_editor.edit_message_id();
v_flex()
.key_context("ChatPanel")
@@ -890,13 +968,36 @@ impl Render for ChatPanel {
)
}
}))
+ .when(!self.is_scrolled_to_bottom, |el| {
+ el.child(div().border_t_1().border_color(cx.theme().colors().border))
+ })
+ .when_some(edit_message_id, |el, _| {
+ el.child(
+ h_flex()
+ .px_2()
+ .text_ui_xs()
+ .justify_between()
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .bg(cx.theme().colors().background)
+ .child("Editing message")
+ .child(
+ IconButton::new("cancel-edit-message", IconName::Close)
+ .shape(ui::IconButtonShape::Square)
+ .tooltip(|cx| Tooltip::text("Cancel edit message", cx))
+ .on_click(cx.listener(move |this, _, cx| {
+ this.cancel_edit_message(cx);
+ })),
+ ),
+ )
+ })
.when_some(reply_to_message_id, |el, reply_to_message_id| {
let reply_message = self
.active_chat()
.and_then(|active_chat| {
- active_chat.read(cx).messages().iter().find(|message| {
- message.id == ChannelMessageId::Saved(reply_to_message_id)
- })
+ active_chat
+ .read(cx)
+ .find_loaded_message(reply_to_message_id)
})
.cloned();
@@ -932,13 +1033,9 @@ impl Render for ChatPanel {
.child(
IconButton::new("close-reply-preview", IconName::Close)
.shape(ui::IconButtonShape::Square)
- .tooltip(|cx| {
- Tooltip::for_action("Close reply", &CloseReplyPreview, cx)
- })
+ .tooltip(|cx| Tooltip::text("Close reply", cx))
.on_click(cx.listener(move |this, _, cx| {
- this.selected_message_to_reply_id = None;
-
- cx.dispatch_action(CloseReplyPreview.boxed_clone())
+ this.close_reply_preview(cx);
})),
),
)
@@ -947,13 +1044,11 @@ impl Render for ChatPanel {
.children(
Some(
h_flex()
- .key_context("MessageEditor")
- .on_action(cx.listener(ChatPanel::close_reply_preview))
- .when(
- !self.is_scrolled_to_bottom && reply_to_message_id.is_none(),
- |el| el.border_t_1().border_color(cx.theme().colors().border),
- )
.p_2()
+ .on_action(cx.listener(|this, _: &actions::Cancel, cx| {
+ this.cancel_edit_message(cx);
+ this.close_reply_preview(cx);
+ }))
.map(|el| el.child(self.message_editor.clone())),
)
.filter(|_| self.active_chat.is_some()),
@@ -1056,6 +1151,7 @@ mod tests {
nonce: 5,
mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
reply_to_message_id: None,
+ edited_at: None,
};
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
@@ -1103,6 +1199,7 @@ mod tests {
nonce: 5,
mentions: Vec::new(),
reply_to_message_id: None,
+ edited_at: None,
};
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
@@ -1143,6 +1240,7 @@ mod tests {
nonce: 5,
mentions: Vec::new(),
reply_to_message_id: None,
+ edited_at: None,
};
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs
index cf77a77d47..fbdded1cba 100644
--- a/crates/collab_ui/src/chat_panel/message_editor.rs
+++ b/crates/collab_ui/src/chat_panel/message_editor.rs
@@ -37,6 +37,7 @@ pub struct MessageEditor {
mentions_task: Option>,
channel_id: Option,
reply_to_message_id: Option,
+ edit_message_id: Option,
}
struct MessageEditorCompletionProvider(WeakView);
@@ -131,6 +132,7 @@ impl MessageEditor {
mentions: Vec::new(),
mentions_task: None,
reply_to_message_id: None,
+ edit_message_id: None,
}
}
@@ -146,6 +148,18 @@ impl MessageEditor {
self.reply_to_message_id = None;
}
+ pub fn edit_message_id(&self) -> Option {
+ self.edit_message_id
+ }
+
+ pub fn set_edit_message_id(&mut self, edit_message_id: u64) {
+ self.edit_message_id = Some(edit_message_id);
+ }
+
+ pub fn clear_edit_message_id(&mut self) {
+ self.edit_message_id = None;
+ }
+
pub fn set_channel(
&mut self,
channel_id: ChannelId,
diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto
index 0d67d62d90..b69c7bbae7 100644
--- a/crates/rpc/proto/zed.proto
+++ b/crates/rpc/proto/zed.proto
@@ -203,7 +203,9 @@ message Envelope {
CompleteWithLanguageModel complete_with_language_model = 166;
LanguageModelResponse language_model_response = 167;
CountTokensWithLanguageModel count_tokens_with_language_model = 168;
- CountTokensResponse count_tokens_response = 169; // current max
+ CountTokensResponse count_tokens_response = 169;
+ UpdateChannelMessage update_channel_message = 170;
+ ChannelMessageUpdate channel_message_update = 171; // current max
}
reserved 158 to 161;
@@ -1184,6 +1186,14 @@ message RemoveChannelMessage {
uint64 message_id = 2;
}
+message UpdateChannelMessage {
+ uint64 channel_id = 1;
+ uint64 message_id = 2;
+ Nonce nonce = 4;
+ string body = 5;
+ repeated ChatMention mentions = 6;
+}
+
message AckChannelMessage {
uint64 channel_id = 1;
uint64 message_id = 2;
@@ -1198,6 +1208,11 @@ message ChannelMessageSent {
ChannelMessage message = 2;
}
+message ChannelMessageUpdate {
+ uint64 channel_id = 1;
+ ChannelMessage message = 2;
+}
+
message GetChannelMessages {
uint64 channel_id = 1;
uint64 before_message_id = 2;
@@ -1229,6 +1244,7 @@ message ChannelMessage {
Nonce nonce = 5;
repeated ChatMention mentions = 6;
optional uint64 reply_to_message_id = 7;
+ optional uint64 edited_at = 8;
}
message ChatMention {
diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs
index b25b01a798..429452d3e1 100644
--- a/crates/rpc/src/proto.rs
+++ b/crates/rpc/src/proto.rs
@@ -149,6 +149,7 @@ messages!(
(CallCanceled, Foreground),
(CancelCall, Foreground),
(ChannelMessageSent, Foreground),
+ (ChannelMessageUpdate, Foreground),
(CompleteWithLanguageModel, Background),
(CopyProjectEntry, Foreground),
(CountTokensWithLanguageModel, Background),
@@ -244,6 +245,7 @@ messages!(
(ReloadBuffersResponse, Foreground),
(RemoveChannelMember, Foreground),
(RemoveChannelMessage, Foreground),
+ (UpdateChannelMessage, Foreground),
(RemoveContact, Foreground),
(RemoveProjectCollaborator, Foreground),
(RenameChannel, Foreground),
@@ -358,6 +360,7 @@ request_messages!(
(ReloadBuffers, ReloadBuffersResponse),
(RemoveChannelMember, Ack),
(RemoveChannelMessage, Ack),
+ (UpdateChannelMessage, Ack),
(RemoveContact, Ack),
(RenameChannel, RenameChannelResponse),
(RenameProjectEntry, ProjectEntryResponse),
@@ -442,7 +445,9 @@ entity_messages!(
entity_messages!(
{channel_id, Channel},
ChannelMessageSent,
+ ChannelMessageUpdate,
RemoveChannelMessage,
+ UpdateChannelMessage,
UpdateChannelBuffer,
UpdateChannelBufferCollaborators,
);
diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs
index 9a0326bdcd..e637c64b8c 100644
--- a/crates/ui/src/components/icon.rs
+++ b/crates/ui/src/components/icon.rs
@@ -93,6 +93,7 @@ pub enum IconName {
Option,
PageDown,
PageUp,
+ Pencil,
Play,
Plus,
Public,
@@ -188,6 +189,7 @@ impl IconName {
IconName::Option => "icons/option.svg",
IconName::PageDown => "icons/page_down.svg",
IconName::PageUp => "icons/page_up.svg",
+ IconName::Pencil => "icons/pencil.svg",
IconName::Play => "icons/play.svg",
IconName::Plus => "icons/plus.svg",
IconName::Public => "icons/public.svg",