From 3ab53f7cb601c62ede22f1927db5aa3baf00a6e9 Mon Sep 17 00:00:00 2001 From: frosty Date: Thu, 27 Aug 2026 18:56:06 -0400 Subject: feat: improve message actions and add copy option --- .../code_block_widget.vala | 1 + .../conversation_view.vala | 21 +++++++++++- .../conversation_content_view/message_widget.vala | 40 ++++++++++++++++++++-- 3 files changed, 58 insertions(+), 4 deletions(-) (limited to 'main/src/ui') diff --git a/main/src/ui/conversation_content_view/code_block_widget.vala b/main/src/ui/conversation_content_view/code_block_widget.vala index dc1ecc1f..3b24c944 100644 --- a/main/src/ui/conversation_content_view/code_block_widget.vala +++ b/main/src/ui/conversation_content_view/code_block_widget.vala @@ -129,6 +129,7 @@ public class CodeBlockWidget : Box { yalign=0, wrap=false }; + code_label.add_css_class("message-text"); code_label.add_css_class("code-block-content"); append(code_label); diff --git a/main/src/ui/conversation_content_view/conversation_view.vala b/main/src/ui/conversation_content_view/conversation_view.vala index 6a686f1b..83e86827 100644 --- a/main/src/ui/conversation_content_view/conversation_view.vala +++ b/main/src/ui/conversation_content_view/conversation_view.vala @@ -55,6 +55,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug Widget currently_highlighted = null; ContentMetaItem? current_meta_item = null; double last_y = -1; + private bool message_text_selectable = true; private Button create_action_button(string action_name) { var button = new Button() { name=action_name }; @@ -82,6 +83,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug create_action_button("reply"); create_action_button("delete"); + create_action_button("copy"); var menu_button = new MenuButton() { name="menu", tooltip_text=_("More actions") }; action_buttons["menu"] = menu_button; @@ -127,7 +129,10 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug // reaches the overlapping part of a button. EventControllerMotion main_motion_events = new EventControllerMotion(); main.add_controller(main_motion_events); - main_motion_events.motion.connect(update_highlight); + main_motion_events.motion.connect((x, y) => { + set_message_text_selectable(true); + update_highlight(x, y); + }); // Process touch events and capture phase to allow highlighting a message without cursor GestureClick click_controller = new GestureClick(); @@ -135,12 +140,23 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug click_controller.propagation_phase = Gtk.PropagationPhase.CAPTURE; main_wrap_box.add_controller(click_controller); click_controller.pressed.connect_after((n, x, y) => { + set_message_text_selectable(false); update_highlight(x, y); }); return this; } + private void set_message_text_selectable(bool selectable) { + if (message_text_selectable == selectable) return; + + message_text_selectable = selectable; + foreach (Plugins.MetaConversationItem item in meta_items) { + MessageMetaItem? message_item = item as MessageMetaItem; + if (message_item != null) message_item.set_text_selectable(selectable); + } + } + public void activate_last_message_correction() { Gee.BidirIterator iter = content_items.bidir_iterator(); iter.last(); @@ -578,6 +594,9 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug private Widget insert_new(Plugins.MetaConversationItem item) { Plugins.MetaConversationItem? lower_item = meta_items.lower(item); + MessageMetaItem? message_item = item as MessageMetaItem; + if (message_item != null) message_item.set_text_selectable(message_text_selectable); + // Fill datastructure ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation, item); item_item_skeletons[item] = item_skeleton; diff --git a/main/src/ui/conversation_content_view/message_widget.vala b/main/src/ui/conversation_content_view/message_widget.vala index f8db1a45..c5a9c145 100644 --- a/main/src/ui/conversation_content_view/message_widget.vala +++ b/main/src/ui/conversation_content_view/message_widget.vala @@ -31,12 +31,15 @@ public class MessageMetaItem : ContentMetaItem { public Label label = new Label("") { use_markup=true, xalign=0, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=true, vexpand=true }; private Widget? content_widget; + private bool text_selectable = true; public MessageMetaItem(ContentItem content_item, StreamInteractor stream_interactor) { base(content_item); message_item = content_item as MessageItem; this.stream_interactor = stream_interactor; + label.add_css_class("message-text"); + stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.connect(on_updated_item); stream_interactor.get_module(MessageDeletion.IDENTITY).item_deleted.connect(on_updated_item); @@ -111,12 +114,12 @@ public class MessageMetaItem : ContentMetaItem { return markups; } - private string prepare_message(ArrayList markups, out int action_name_length) { + private string prepare_message(ArrayList markups, out int action_name_length, bool truncate = true) { Message message = message_item.message; string text = message.body ?? ""; action_name_length = 0; - if (text.char_count() > 10000) { + if (truncate && text.char_count() > 10000) { text = text.substring(0, text.index_of_nth_char(10000)) + " [" + _("Message too long") + "]"; } @@ -141,15 +144,32 @@ public class MessageMetaItem : ContentMetaItem { var text_label = new Label("") { use_markup=true, xalign=0, - selectable=true, + selectable=text_selectable, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=true }; + text_label.add_css_class("message-text"); text_label.activate_link.connect(on_label_activate_link); return text_label; } + public void set_text_selectable(bool selectable) { + text_selectable = selectable; + if (content_widget != null) set_text_selectable_recursive(content_widget, selectable); + } + + private void set_text_selectable_recursive(Widget widget, bool selectable) { + Label? text_label = widget as Label; + if (text_label != null && text_label.has_css_class("message-text")) { + text_label.selectable = selectable; + } + + for (Widget? child = widget.get_first_child(); child != null; child = child.get_next_sibling()) { + set_text_selectable_recursive(child, selectable); + } + } + private void add_text_attribute(AttrList attributes, Attribute attribute, string text, int source_start, int span_start, int span_end) { int source_end = source_start + text.char_count(); int start = int.max(source_start, span_start); @@ -286,6 +306,7 @@ public class MessageMetaItem : ContentMetaItem { generate_markup_text(label, text, markups, action_name_length); content_widget = label; } + set_text_selectable_recursive(content_widget, text_selectable); if (outer != null && !in_edit_mode) { outer.set_widget(content_widget, Plugins.WidgetType.GTK4, 2); @@ -331,6 +352,19 @@ public class MessageMetaItem : ContentMetaItem { actions.add(action1); } + Plugins.MessageAction copy_action = new Plugins.MessageAction(); + copy_action.name = "copy"; + copy_action.icon_name = "dino-edit-copy-symbolic"; + copy_action.tooltip = _("Copy message"); + copy_action.callback = () => { + var markups = copy_markups(message_item.message); + int action_name_length; + string text = prepare_message(markups, out action_name_length, false); + Gdk.Display? display = Gdk.Display.get_default(); + if (display != null) display.get_clipboard().set_text(text); + }; + actions.add(copy_action); + actions.add(get_reply_action(content_item, message_item.conversation, stream_interactor)); actions.add(get_reaction_action(content_item, message_item.conversation, stream_interactor)); -- cgit v1.3