From dc52e7595cca06d0a2da7d11b3c88cb2f7ce529c Mon Sep 17 00:00:00 2001 From: fiaxh Date: Fri, 6 Jan 2023 13:19:42 +0100 Subject: Add support for XEP-0461 replies (with fallback) --- main/src/ui/chat_input/chat_input_controller.vala | 37 ++++++++++- main/src/ui/chat_input/view.vala | 15 ++++- .../conversation_item_skeleton.vala | 22 ++++--- .../conversation_view.vala | 6 -- .../conversation_content_view/message_widget.vala | 29 +++++++-- .../ui/conversation_content_view/quote_widget.vala | 73 ++++++++++++++++++++++ .../reactions_widget.vala | 3 - .../conversation_selector_row.vala | 2 +- main/src/ui/main_window_controller.vala | 15 +++++ 9 files changed, 176 insertions(+), 26 deletions(-) create mode 100644 main/src/ui/conversation_content_view/quote_widget.vala (limited to 'main/src') diff --git a/main/src/ui/chat_input/chat_input_controller.vala b/main/src/ui/chat_input/chat_input_controller.vala index 7b260695..de75cdf8 100644 --- a/main/src/ui/chat_input/chat_input_controller.vala +++ b/main/src/ui/chat_input/chat_input_controller.vala @@ -24,6 +24,8 @@ public class ChatInputController : Object { private Plugins.InputFieldStatus input_field_status; private ChatTextViewController chat_text_view_controller; + private ContentItem? quoted_content_item = null; + public ChatInputController(ChatInput.View chat_input, StreamInteractor stream_interactor) { this.chat_input = chat_input; this.status_description_label = chat_input.chat_input_status; @@ -58,12 +60,34 @@ public class ChatInputController : Object { } return true; }); + + SimpleAction quote_action = new SimpleAction("quote", new VariantType.tuple(new VariantType[]{VariantType.INT32, VariantType.INT32})); + quote_action.activate.connect((variant) => { + int conversation_id = variant.get_child_value(0).get_int32(); + Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_by_id(conversation_id); + if (conversation == null || !this.conversation.equals(conversation)) return; + + int content_item_id = variant.get_child_value(1).get_int32(); + ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, content_item_id); + if (content_item == null) return; + + quoted_content_item = content_item; + var quote_model = new Quote.Model.from_content_item(content_item, conversation, stream_interactor) { can_abort = true }; + quote_model.aborted.connect(() => { + content_item = null; + chat_input.unset_quoted_message(); + }); + chat_input.set_quoted_message(Quote.get_widget(quote_model)); + }); + GLib.Application.get_default().add_action(quote_action); } public void set_conversation(Conversation conversation) { - this.conversation = conversation; - + this.quoted_content_item = null; reset_input_field_status(); + chat_input.unset_quoted_message(); + + this.conversation = conversation; chat_input.encryption_widget.set_conversation(conversation); @@ -111,7 +135,10 @@ public class ChatInputController : Object { } string text = chat_input.chat_text_view.text_view.buffer.text; + chat_input.chat_text_view.text_view.buffer.text = ""; + chat_input.unset_quoted_message(); + if (text.has_prefix("/")) { string[] token = text.split(" ", 2); switch(token[0]) { @@ -164,7 +191,11 @@ public class ChatInputController : Object { break; } } - stream_interactor.get_module(MessageProcessor.IDENTITY).send_text(text, conversation); + Message out_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(text, conversation); + if (quoted_content_item != null) { + stream_interactor.get_module(Replies.IDENTITY).set_message_is_reply_to(out_message, quoted_content_item); + } + stream_interactor.get_module(MessageProcessor.IDENTITY).send_message(out_message, conversation); } private void on_text_input_changed() { diff --git a/main/src/ui/chat_input/view.vala b/main/src/ui/chat_input/view.vala index 4be4455b..e16b4085 100644 --- a/main/src/ui/chat_input/view.vala +++ b/main/src/ui/chat_input/view.vala @@ -20,8 +20,8 @@ public class View : Box { private HashMap entry_cache = new HashMap(Conversation.hash_func, Conversation.equals_func); [GtkChild] public unowned Frame frame; + [GtkChild] public unowned Box quote_box; [GtkChild] public unowned ChatTextView chat_text_view; - [GtkChild] public unowned Box outer_box; [GtkChild] public unowned Button file_button; [GtkChild] public unowned MenuButton emoji_button; [GtkChild] public unowned MenuButton encryption_button; @@ -94,6 +94,19 @@ public class View : Box { }); } + public void set_quoted_message(Widget quote_widget) { + Widget? quote_box_child = quote_box.get_first_child(); + if (quote_box_child != null) quote_box.remove(quote_box_child); + quote_box.append(quote_widget); + quote_box.visible = true; + } + + public void unset_quoted_message() { + Widget? quote_box_child = quote_box.get_first_child(); + if (quote_box_child != null) quote_box.remove(quote_box_child); + quote_box.visible = false; + } + public void do_focus() { chat_text_view.text_view.grab_focus(); } diff --git a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala index 7113b3b7..00c88db3 100644 --- a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala +++ b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala @@ -16,7 +16,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, public Image encryption_image { get; set; } public Image received_image { get; set; } - public Widget? content_widget = null; + private HashMap content_widgets = new HashMap(); private bool show_skeleton_ = false; public bool show_skeleton { @@ -58,7 +58,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, widget = item.get_widget(this, Plugins.WidgetType.GTK4) as Widget; if (widget != null) { widget.valign = Align.END; - set_widget(widget, Plugins.WidgetType.GTK4); + set_widget(widget, Plugins.WidgetType.GTK4, 2); } if (item.requires_header) { @@ -72,7 +72,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, if (content_meta_item != null) { reactions_controller = new ReactionsController(conversation, content_meta_item.content_item, stream_interactor); reactions_controller.box_activated.connect((widget) => { - main_grid.attach(widget, 1, 2, 4, 1); + set_widget(widget, Plugins.WidgetType.GTK4, 3); }); reactions_controller.init(); } @@ -103,12 +103,18 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, update_received_mark(); } - public void set_widget(Object object, Plugins.WidgetType type) { - if (content_widget != null) content_widget.unparent(); + public void set_widget(Object object, Plugins.WidgetType type, int priority) { + foreach (var content_widget in content_widgets.values) { + content_widget.unparent(); + } - Widget widget = (Widget) object; - content_widget = widget; - main_grid.attach(widget, 1, 1, 4, 1); + content_widgets[priority] = (Widget) object; + int row_no = 1; + for (int i = 0; i < 5; i++) { + if (!content_widgets.has_key(i)) continue; + main_grid.attach(content_widgets[i], 1, row_no, 4, 1); + row_no++; + } } private void update_margin() { diff --git a/main/src/ui/conversation_content_view/conversation_view.vala b/main/src/ui/conversation_content_view/conversation_view.vala index f0f6e118..70115512 100644 --- a/main/src/ui/conversation_content_view/conversation_view.vala +++ b/main/src/ui/conversation_content_view/conversation_view.vala @@ -18,7 +18,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug [GtkChild] private unowned Box notifications; [GtkChild] private unowned Box main; [GtkChild] private unowned Box main_wrap_box; - [GtkChild] private unowned Stack stack; private ArrayList action_buttons = new ArrayList(); private Gee.List? message_actions = null; @@ -208,7 +207,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug Button button = new Button(); button.icon_name = message_action.icon_name; button.clicked.connect(() => { - print(@"$(current_meta_item.jid) skdfj \n"); message_action.callback(button, current_meta_item, currently_highlighted); }); action_buttons.add(button); @@ -233,15 +231,12 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug }); firstLoad = false; } - stack.set_visible_child_name("void"); clear(); initialize_for_conversation_(conversation); display_latest(); - stack.set_visible_child_name("main"); } public void initialize_around_message(Conversation conversation, ContentItem content_item) { - stack.set_visible_child_name("void"); clear(); initialize_for_conversation_(conversation); Gee.List before_items = content_populator.populate_before(conversation, content_item, 40); @@ -277,7 +272,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug scrolled.vadjustment.value = h - scrolled.vadjustment.page_size * 1/3; w.add_css_class("highlight-once"); reload_messages = true; - stack.set_visible_child_name("main"); return false; }); } diff --git a/main/src/ui/conversation_content_view/message_widget.vala b/main/src/ui/conversation_content_view/message_widget.vala index 1f027c89..f4e1d22c 100644 --- a/main/src/ui/conversation_content_view/message_widget.vala +++ b/main/src/ui/conversation_content_view/message_widget.vala @@ -82,7 +82,8 @@ public class MessageMetaItem : ContentMetaItem { bool theme_dependent = false; - string markup_text = message.body; + string markup_text = Dino.message_body_without_reply_fallback(message); + if (markup_text.length > 10000) { markup_text = markup_text.substring(0, 10000) + " [" + _("Message too long") + "]"; } @@ -169,7 +170,7 @@ public class MessageMetaItem : ContentMetaItem { edit_mode.cancelled.connect(() => { in_edit_mode = false; - outer.set_widget(label, Plugins.WidgetType.GTK4); + outer.set_widget(label, Plugins.WidgetType.GTK4, 2); }); edit_mode.send.connect(() => { if (((MessageItem) content_item).message.body != edit_mode.chat_text_view.text_view.buffer.text) { @@ -178,18 +179,31 @@ public class MessageMetaItem : ContentMetaItem { // edit_cancelled(); } in_edit_mode = false; - outer.set_widget(label, Plugins.WidgetType.GTK4); + outer.set_widget(label, Plugins.WidgetType.GTK4, 2); }); edit_mode.chat_text_view.text_view.buffer.text = message.body; - outer.set_widget(edit_mode, Plugins.WidgetType.GTK4); + outer.set_widget(edit_mode, Plugins.WidgetType.GTK4, 2); edit_mode.chat_text_view.text_view.grab_focus(); } else { this.in_edit_mode = false; } }); + outer.set_widget(label, Plugins.WidgetType.GTK4, 2); + + if (message_item.message.quoted_item_id > 0) { + var quoted_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(message_item.conversation, message_item.message.quoted_item_id); + if (quoted_content_item != null) { + var quote_model = new Quote.Model.from_content_item(quoted_content_item, message_item.conversation, stream_interactor); + quote_model.jump_to.connect(() => { + GLib.Application.get_default().activate_action("jump-to-conversation-message", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(message_item.conversation.id), new GLib.Variant.int32(message_item.id) })); + }); + var quote_widget = Quote.get_widget(quote_model); + outer.set_widget(quote_widget, Plugins.WidgetType.GTK4, 1); + } + } return label; } @@ -209,6 +223,13 @@ public class MessageMetaItem : ContentMetaItem { actions.add(action1); } + Plugins.MessageAction reply_action = new Plugins.MessageAction(); + reply_action.icon_name = "mail-reply-sender-symbolic"; + reply_action.callback = (button, content_meta_item_activated, widget) => { + GLib.Application.get_default().activate_action("quote", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(message_item.conversation.id), new GLib.Variant.int32(message_item.id) })); + }; + actions.add(reply_action); + if (supports_reaction) { Plugins.MessageAction action2 = new Plugins.MessageAction(); action2.icon_name = "dino-emoticon-add-symbolic"; diff --git a/main/src/ui/conversation_content_view/quote_widget.vala b/main/src/ui/conversation_content_view/quote_widget.vala new file mode 100644 index 00000000..f627c852 --- /dev/null +++ b/main/src/ui/conversation_content_view/quote_widget.vala @@ -0,0 +1,73 @@ +using Dino.Ui.ConversationSummary; +using Gee; +using Gtk; +using Xmpp; + +using Dino.Entities; + +namespace Dino.Ui.Quote { + + public class Model : Object { + public signal void aborted(); + public signal void jump_to(); + + public string display_name { get; set; } + public string message { get; set; } + public DateTime message_time { get; set; } + + public StreamInteractor stream_interactor { get; set; } + public Conversation conversation { get; set; } + public Jid author_jid { get; set; } + + public bool can_abort { get; set; default=false; } + + public Model.from_content_item(ContentItem content_item, Conversation conversation, StreamInteractor stream_interactor) { + this.display_name = Util.get_participant_display_name(stream_interactor, conversation, content_item.jid, true); + if (content_item.type_ == MessageItem.TYPE) { + var message = ((MessageItem) content_item).message; + this.message = Dino.message_body_without_reply_fallback(message); + } else if (content_item.type_ == FileItem.TYPE) { + this.message = "[File]"; + } + this.message_time = content_item.time; + + this.stream_interactor = stream_interactor; + this.conversation = conversation; + this.author_jid = content_item.jid; + } + } + + public Widget get_widget(Model model) { + Builder builder = new Builder.from_resource("/im/dino/Dino/quote.ui"); + AvatarImage avatar = (AvatarImage) builder.get_object("avatar"); + Label author = (Label) builder.get_object("author"); + Label time = (Label) builder.get_object("time"); + Label message = (Label) builder.get_object("message"); + Button abort_button = (Button) builder.get_object("abort-button"); + + avatar.set_conversation_participant(model.stream_interactor, model.conversation, model.author_jid); + model.bind_property("display-name", author, "label", BindingFlags.SYNC_CREATE); + model.bind_property("message-time", time, "label", BindingFlags.SYNC_CREATE, (_, from_value, ref to_value) => { + DateTime message_time = (DateTime) from_value; + to_value = ConversationItemSkeleton.get_relative_time(message_time); + return true; + }); + model.bind_property("message", message, "label", BindingFlags.SYNC_CREATE); + model.bind_property("can-abort", abort_button, "visible", BindingFlags.SYNC_CREATE); + + abort_button.clicked.connect(() => { + model.aborted(); + }); + + Widget outer = builder.get_object("outer") as Widget; + + GestureClick gesture_click_controller = new GestureClick(); + outer.add_controller(gesture_click_controller); + gesture_click_controller.pressed.connect(() => { + model.jump_to(); + }); + + return outer; + } +} + diff --git a/main/src/ui/conversation_content_view/reactions_widget.vala b/main/src/ui/conversation_content_view/reactions_widget.vala index c9f93f66..890c1206 100644 --- a/main/src/ui/conversation_content_view/reactions_widget.vala +++ b/main/src/ui/conversation_content_view/reactions_widget.vala @@ -27,9 +27,6 @@ public class ReactionsController : Object { public void init() { Gee.List reactions = stream_interactor.get_module(Reactions.IDENTITY).get_item_reactions(conversation, content_item); - if (reactions.size > 0) { - initialize_widget(); - } foreach (ReactionUsers reaction_users in reactions) { foreach (Jid jid in reaction_users.jids) { reaction_added(reaction_users.reaction, jid); diff --git a/main/src/ui/conversation_selector/conversation_selector_row.vala b/main/src/ui/conversation_selector/conversation_selector_row.vala index a2588d9a..bd2b0747 100644 --- a/main/src/ui/conversation_selector/conversation_selector_row.vala +++ b/main/src/ui/conversation_selector/conversation_selector_row.vala @@ -149,7 +149,7 @@ public class ConversationSelectorRow : ListBoxRow { MessageItem message_item = last_content_item as MessageItem; Message last_message = message_item.message; - string body = last_message.body; + string body = Dino.message_body_without_reply_fallback(last_message); bool me_command = body.has_prefix("/me "); /* If we have a /me command, we always show the display diff --git a/main/src/ui/main_window_controller.vala b/main/src/ui/main_window_controller.vala index 38ebcc9c..9e7e8ce7 100644 --- a/main/src/ui/main_window_controller.vala +++ b/main/src/ui/main_window_controller.vala @@ -23,6 +23,21 @@ public class MainWindowController : Object { stream_interactor.get_module(ConversationManager.IDENTITY).conversation_deactivated.connect(check_unset_conversation); stream_interactor.account_removed.connect(check_unset_conversation); + + SimpleAction jump_to_conversatio_message_action = new SimpleAction("jump-to-conversation-message", new VariantType.tuple(new VariantType[]{VariantType.INT32, VariantType.INT32})); + jump_to_conversatio_message_action.activate.connect((variant) => { + int conversation_id = variant.get_child_value(0).get_int32(); + Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_by_id(conversation_id); + if (conversation == null || !this.conversation.equals(conversation)) return; + + int item_id = variant.get_child_value(1).get_int32(); + ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, item_id); + + select_conversation(conversation, false, false); + window.conversation_view.conversation_frame.initialize_around_message(conversation, content_item); + }); + app.add_action(jump_to_conversatio_message_action); + } public void set_window(MainWindow window) { -- cgit v1.2.3-70-g09d2