From 871ff33ac79f3d17b0260b8bfcd27780038edd6d Mon Sep 17 00:00:00 2001 From: fiaxh Date: Fri, 3 Apr 2020 22:49:59 +0200 Subject: Add support for last message correction --- main/src/ui/chat_input/chat_input_controller.vala | 18 +- main/src/ui/chat_input/chat_text_view.vala | 91 +++++++++ main/src/ui/chat_input/edit_history.vala | 4 +- .../src/ui/chat_input/occupants_tab_completer.vala | 2 +- main/src/ui/chat_input/smiley_converter.vala | 2 +- main/src/ui/chat_input/view.vala | 52 +---- .../chat_state_populator.vala | 6 +- .../content_item_widget_factory.vala | 114 ----------- .../content_populator.vala | 39 ++-- .../conversation_item_skeleton.vala | 20 +- .../conversation_view.vala | 63 +++--- .../date_separator_populator.vala | 6 +- .../ui/conversation_content_view/file_widget.vala | 18 ++ .../ui/conversation_content_view/message_item.vala | 0 .../conversation_content_view/message_widget.vala | 211 +++++++++++++++++++++ .../conversation_selector_row.vala | 6 + main/src/ui/conversation_view_controller.vala | 13 +- main/src/ui/util/size_request_box.vala | 12 +- 18 files changed, 445 insertions(+), 232 deletions(-) create mode 100644 main/src/ui/chat_input/chat_text_view.vala delete mode 100644 main/src/ui/conversation_content_view/content_item_widget_factory.vala delete mode 100644 main/src/ui/conversation_content_view/message_item.vala create mode 100644 main/src/ui/conversation_content_view/message_widget.vala (limited to 'main/src/ui') diff --git a/main/src/ui/chat_input/chat_input_controller.vala b/main/src/ui/chat_input/chat_input_controller.vala index f65da1e8..c0878c36 100644 --- a/main/src/ui/chat_input/chat_input_controller.vala +++ b/main/src/ui/chat_input/chat_input_controller.vala @@ -17,18 +17,20 @@ public class ChatInputController : Object { private StreamInteractor stream_interactor; private Plugins.InputFieldStatus input_field_status; + private ChatTextViewController chat_text_view_controller; public ChatInputController(ChatInput.View chat_input, StreamInteractor stream_interactor) { this.chat_input = chat_input; this.status_description_label = chat_input.chat_input_status; this.stream_interactor = stream_interactor; + this.chat_text_view_controller = new ChatTextViewController(chat_input.chat_text_view, stream_interactor); chat_input.init(stream_interactor); reset_input_field_status(); - chat_input.text_input.buffer.changed.connect(on_text_input_changed); - chat_input.send_text.connect(send_text); + chat_input.chat_text_view.text_view.buffer.changed.connect(on_text_input_changed); + chat_text_view_controller.send_text.connect(send_text); chat_input.encryption_widget.encryption_changed.connect(on_encryption_changed); @@ -40,10 +42,10 @@ public class ChatInputController : Object { reset_input_field_status(); - chat_input.initialize_for_conversation(conversation); - chat_input.occupants_tab_completor.initialize_for_conversation(conversation); - chat_input.edit_history.initialize_for_conversation(conversation); chat_input.encryption_widget.set_conversation(conversation); + + chat_input.initialize_for_conversation(conversation); + chat_text_view_controller.initialize_for_conversation(conversation); } private void on_encryption_changed(Plugins.EncryptionListEntry? encryption_entry) { @@ -81,8 +83,8 @@ public class ChatInputController : Object { return; } - string text = chat_input.text_input.buffer.text; - chat_input.text_input.buffer.text = ""; + string text = chat_input.chat_text_view.text_view.buffer.text; + chat_input.chat_text_view.text_view.buffer.text = ""; if (text.has_prefix("/")) { string[] token = text.split(" ", 2); switch(token[0]) { @@ -137,7 +139,7 @@ public class ChatInputController : Object { } private void on_text_input_changed() { - if (chat_input.text_input.buffer.text != "") { + if (chat_input.chat_text_view.text_view.buffer.text != "") { stream_interactor.get_module(ChatInteraction.IDENTITY).on_message_entered(conversation); } else { stream_interactor.get_module(ChatInteraction.IDENTITY).on_message_cleared(conversation); diff --git a/main/src/ui/chat_input/chat_text_view.vala b/main/src/ui/chat_input/chat_text_view.vala new file mode 100644 index 00000000..1155d21d --- /dev/null +++ b/main/src/ui/chat_input/chat_text_view.vala @@ -0,0 +1,91 @@ +using Gdk; +using Gee; +using Gtk; + +using Dino.Entities; +using Xmpp; + +namespace Dino.Ui { + +public class ChatTextViewController : Object { + + public signal void send_text(); + + public OccupantsTabCompletor occupants_tab_completor; + + private ChatTextView widget; + + public ChatTextViewController(ChatTextView widget, StreamInteractor stream_interactor) { + this.widget = widget; + occupants_tab_completor = new OccupantsTabCompletor(stream_interactor, widget.text_view); + + widget.send_text.connect(() => { + send_text(); + }); + } + + public void initialize_for_conversation(Conversation conversation) { + occupants_tab_completor.initialize_for_conversation(conversation); + widget.initialize_for_conversation(conversation); + } +} + +public class ChatTextView : ScrolledWindow { + + public signal void send_text(); + public signal void cancel_input(); + + public TextView text_view = new TextView() { can_focus=true, hexpand=true, margin=8, wrap_mode=Gtk.WrapMode.WORD_CHAR, valign=Align.CENTER, visible=true }; + private int vscrollbar_min_height; + private SmileyConverter smiley_converter; + public EditHistory edit_history; + + construct { + max_content_height = 300; + propagate_natural_height = true; + this.add(text_view); + + smiley_converter = new SmileyConverter(text_view); + edit_history = new EditHistory(text_view); + + this.get_vscrollbar().get_preferred_height(out vscrollbar_min_height, null); + this.vadjustment.notify["upper"].connect_after(on_upper_notify); + text_view.key_press_event.connect(on_text_input_key_press); + + Gtk.drag_dest_unset(text_view); + } + + public void initialize_for_conversation(Conversation conversation) { + edit_history.initialize_for_conversation(conversation); + } + + public override void get_preferred_height(out int min_height, out int nat_height) { + base.get_preferred_height(out min_height, out nat_height); + min_height = nat_height; + } + + private void on_upper_notify() { + this.vadjustment.value = this.vadjustment.upper - this.vadjustment.page_size; + + // hack for vscrollbar not requiring space and making textview higher //TODO doesn't resize immediately + this.get_vscrollbar().visible = (this.vadjustment.upper > this.max_content_height - 2 * this.vscrollbar_min_height); + } + + private bool on_text_input_key_press(EventKey event) { + if (event.keyval in new uint[]{Key.Return, Key.KP_Enter}) { + if ((event.state & ModifierType.SHIFT_MASK) > 0) { + text_view.buffer.insert_at_cursor("\n", 1); + } else if (text_view.buffer.text != "") { + send_text(); + edit_history.reset_history(); + } + return true; + } + if (event.keyval == Key.Escape) { + cancel_input(); + } + return false; + } +} + +} diff --git a/main/src/ui/chat_input/edit_history.vala b/main/src/ui/chat_input/edit_history.vala index 1d179bb7..70f6d400 100644 --- a/main/src/ui/chat_input/edit_history.vala +++ b/main/src/ui/chat_input/edit_history.vala @@ -4,7 +4,7 @@ using Gtk; using Dino.Entities; -namespace Dino.Ui.ChatInput { +namespace Dino.Ui { public class EditHistory { @@ -14,7 +14,7 @@ public class EditHistory { private HashMap> histories = new HashMap>(Conversation.hash_func, Conversation.equals_func); private HashMap indices = new HashMap(Conversation.hash_func, Conversation.equals_func); - public EditHistory(TextView text_input, GLib.Application application) { + public EditHistory(TextView text_input) { this.text_input = text_input; text_input.key_press_event.connect(on_text_input_key_press); diff --git a/main/src/ui/chat_input/occupants_tab_completer.vala b/main/src/ui/chat_input/occupants_tab_completer.vala index 87db8986..ab1b75e0 100644 --- a/main/src/ui/chat_input/occupants_tab_completer.vala +++ b/main/src/ui/chat_input/occupants_tab_completer.vala @@ -5,7 +5,7 @@ using Gtk; using Dino.Entities; using Xmpp; -namespace Dino.Ui.ChatInput { +namespace Dino.Ui { /** * - With given prefix: Complete from occupant list (sorted lexicographically) diff --git a/main/src/ui/chat_input/smiley_converter.vala b/main/src/ui/chat_input/smiley_converter.vala index a25076ba..8f4dee9a 100644 --- a/main/src/ui/chat_input/smiley_converter.vala +++ b/main/src/ui/chat_input/smiley_converter.vala @@ -4,7 +4,7 @@ using Gtk; using Dino.Entities; -namespace Dino.Ui.ChatInput { +namespace Dino.Ui { class SmileyConverter { diff --git a/main/src/ui/chat_input/view.vala b/main/src/ui/chat_input/view.vala index 960e4e14..166ead2e 100644 --- a/main/src/ui/chat_input/view.vala +++ b/main/src/ui/chat_input/view.vala @@ -10,25 +10,17 @@ namespace Dino.Ui.ChatInput { [GtkTemplate (ui = "/im/dino/Dino/chat_input.ui")] public class View : Box { - public signal void send_text(); - public string text { - owned get { return text_input.buffer.text; } - set { text_input.buffer.text = value; } + owned get { return chat_text_view.text_view.buffer.text; } + set { chat_text_view.text_view.buffer.text = value; } } private StreamInteractor stream_interactor; private Conversation? conversation; private HashMap entry_cache = new HashMap(Conversation.hash_func, Conversation.equals_func); - private int vscrollbar_min_height; - - public OccupantsTabCompletor occupants_tab_completor; - private SmileyConverter smiley_converter; - public EditHistory edit_history; [GtkChild] public Frame frame; - [GtkChild] public ScrolledWindow scrolled; - [GtkChild] public TextView text_input; + [GtkChild] public ChatTextView chat_text_view; [GtkChild] public Box outer_box; [GtkChild] public Button file_button; [GtkChild] public Separator file_separator; @@ -39,9 +31,6 @@ public class View : Box { public View init(StreamInteractor stream_interactor) { this.stream_interactor = stream_interactor; - occupants_tab_completor = new OccupantsTabCompletor(stream_interactor, text_input); - smiley_converter = new SmileyConverter(text_input); - edit_history = new EditHistory(text_input, GLib.Application.get_default()); encryption_widget = new EncryptionButton(stream_interactor) { relief=ReliefStyle.NONE, margin_top=3, valign=Align.START, visible=true }; file_button.clicked.connect(() => { @@ -53,9 +42,6 @@ public class View : Box { }); file_button.get_style_context().add_class("dino-attach-button"); - scrolled.get_vscrollbar().get_preferred_height(out vscrollbar_min_height, null); - scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify); - encryption_widget.get_style_context().add_class("dino-chatinput-button"); encryption_widget.encryption_changed.connect(update_file_transfer_availability); @@ -68,7 +54,7 @@ public class View : Box { EmojiChooser chooser = new EmojiChooser(); chooser.emoji_picked.connect((emoji) => { - text_input.buffer.insert_at_cursor(emoji, emoji.data.length); + chat_text_view.text_view.buffer.insert_at_cursor(emoji, emoji.data.length); }); emoji_button.set_popover(chooser); @@ -77,8 +63,6 @@ public class View : Box { outer_box.add(encryption_widget); - text_input.key_press_event.connect(on_text_input_key_press); - Util.force_css(frame, "* { border-radius: 3px; }"); return this; @@ -91,17 +75,17 @@ public class View : Box { } public void initialize_for_conversation(Conversation conversation) { - if (this.conversation != null) entry_cache[this.conversation] = text_input.buffer.text; + if (this.conversation != null) entry_cache[this.conversation] = chat_text_view.text_view.buffer.text; this.conversation = conversation; update_file_transfer_availability(); - text_input.buffer.text = ""; + chat_text_view.text_view.buffer.text = ""; if (entry_cache.has_key(conversation)) { - text_input.buffer.text = entry_cache[conversation]; + chat_text_view.text_view.buffer.text = entry_cache[conversation]; } - text_input.grab_focus(); + chat_text_view.text_view.grab_focus(); } public void set_input_state(Plugins.InputFieldStatus.MessageType message_type) { @@ -132,26 +116,6 @@ public class View : Box { return false; }); } - - private bool on_text_input_key_press(EventKey event) { - if (event.keyval in new uint[]{Key.Return, Key.KP_Enter}) { - if ((event.state & ModifierType.SHIFT_MASK) > 0) { - text_input.buffer.insert_at_cursor("\n", 1); - } else if (this.text != "") { - send_text(); - edit_history.reset_history(); - } - return true; - } - return false; - } - - private void on_upper_notify() { - scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; - - // hack for vscrollbar not requiring space and making textview higher //TODO doesn't resize immediately - scrolled.get_vscrollbar().visible = (scrolled.vadjustment.upper > scrolled.max_content_height - 2 * vscrollbar_min_height); - } } } diff --git a/main/src/ui/conversation_content_view/chat_state_populator.vala b/main/src/ui/conversation_content_view/chat_state_populator.vala index 0438e014..545d2c6d 100644 --- a/main/src/ui/conversation_content_view/chat_state_populator.vala +++ b/main/src/ui/conversation_content_view/chat_state_populator.vala @@ -64,10 +64,6 @@ class ChatStatePopulator : Plugins.ConversationItemPopulator, Plugins.Conversati private class MetaChatStateItem : Plugins.MetaConversationItem { public override DateTime sort_time { get; set; default=new DateTime.now_utc().add_years(10); } - public override bool can_merge { get; set; default=false; } - public override bool requires_avatar { get; set; default=false; } - public override bool requires_header { get; set; default=false; } - private StreamInteractor stream_interactor; private Conversation conversation; private Gee.List jids = new ArrayList(); @@ -93,6 +89,8 @@ private class MetaChatStateItem : Plugins.MetaConversationItem { return image_content_box; } + public override Gee.List? get_item_actions(Plugins.WidgetType type) { return null; } + public void set_new(Gee.List jids) { this.jids = jids; update(); diff --git a/main/src/ui/conversation_content_view/content_item_widget_factory.vala b/main/src/ui/conversation_content_view/content_item_widget_factory.vala deleted file mode 100644 index da092e34..00000000 --- a/main/src/ui/conversation_content_view/content_item_widget_factory.vala +++ /dev/null @@ -1,114 +0,0 @@ -using Gee; -using Gdk; -using Gtk; -using Pango; -using Xmpp; - -using Dino.Entities; - -namespace Dino.Ui.ConversationSummary { - -public class ContentItemWidgetFactory : Object { - - private StreamInteractor stream_interactor; - private HashMap generators = new HashMap(); - - public ContentItemWidgetFactory(StreamInteractor stream_interactor) { - this.stream_interactor = stream_interactor; - - generators[MessageItem.TYPE] = new MessageItemWidgetGenerator(stream_interactor); - generators[FileItem.TYPE] = new FileItemWidgetGenerator(stream_interactor); - } - - public Widget? get_widget(ContentItem item) { - WidgetGenerator? generator = generators[item.type_]; - if (generator != null) { - return (Widget?) generator.get_widget(item); - } - return null; - } - - public void register_widget_generator(WidgetGenerator generator) { - generators[generator.handles_type] = generator; - } -} - -public interface WidgetGenerator : Object { - public abstract string handles_type { get; set; } - public abstract Object get_widget(ContentItem item); -} - -public class MessageItemWidgetGenerator : WidgetGenerator, Object { - - public string handles_type { get; set; default=MessageItem.TYPE; } - - private StreamInteractor stream_interactor; - - public MessageItemWidgetGenerator(StreamInteractor stream_interactor) { - this.stream_interactor = stream_interactor; - } - - public Object get_widget(ContentItem item) { - MessageItem message_item = item as MessageItem; - Conversation conversation = message_item.conversation; - Message message = message_item.message; - - Label label = new Label("") { use_markup=true, xalign=0, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, vexpand=true, visible=true }; - string markup_text = message.body; - if (markup_text.length > 10000) { - markup_text = markup_text.substring(0, 10000) + " [" + _("Message too long") + "]"; - } - if (message_item.message.body.has_prefix("/me")) { - markup_text = markup_text.substring(3); - } - - if (conversation.type_ == Conversation.Type.GROUPCHAT) { - markup_text = Util.parse_add_markup(markup_text, conversation.nickname, true, true); - } else { - markup_text = Util.parse_add_markup(markup_text, null, true, true); - } - - if (message_item.message.body.has_prefix("/me")) { - string display_name = Util.get_participant_display_name(stream_interactor, conversation, message.from); - update_me_style(stream_interactor, message.real_jid ?? message.from, display_name, conversation.account, label, markup_text); - label.realize.connect(() => update_me_style(stream_interactor, message.real_jid ?? message.from, display_name, conversation.account, label, markup_text)); - label.style_updated.connect(() => update_me_style(stream_interactor, message.real_jid ?? message.from, display_name, conversation.account, label, markup_text)); - } - - int only_emoji_count = Util.get_only_emoji_count(markup_text); - if (only_emoji_count != -1) { - string size_str = only_emoji_count < 5 ? "xx-large" : "large"; - markup_text = @"" + markup_text + ""; - } - - label.label = markup_text; - return label; - } - - public static void update_me_style(StreamInteractor stream_interactor, Jid jid, string display_name, Account account, Label label, string action_text) { - string color = Util.get_name_hex_color(stream_interactor, account, jid, Util.is_dark_theme(label)); - label.label = @"$(Markup.escape_text(display_name))" + action_text; - } -} - -public class FileItemWidgetGenerator : WidgetGenerator, Object { - - public StreamInteractor stream_interactor; - public string handles_type { get; set; default=FileItem.TYPE; } - - private const int MAX_HEIGHT = 300; - private const int MAX_WIDTH = 600; - - public FileItemWidgetGenerator(StreamInteractor stream_interactor) { - this.stream_interactor = stream_interactor; - } - - public Object get_widget(ContentItem item) { - FileItem file_item = item as FileItem; - FileTransfer transfer = file_item.file_transfer; - - return new FileWidget(stream_interactor, transfer) { visible=true }; - } -} - -} diff --git a/main/src/ui/conversation_content_view/content_populator.vala b/main/src/ui/conversation_content_view/content_populator.vala index e8eee06c..2a0f8ac1 100644 --- a/main/src/ui/conversation_content_view/content_populator.vala +++ b/main/src/ui/conversation_content_view/content_populator.vala @@ -9,13 +9,11 @@ namespace Dino.Ui.ConversationSummary { public class ContentProvider : ContentItemCollection, Object { private StreamInteractor stream_interactor; - private ContentItemWidgetFactory widget_factory; private Conversation? current_conversation; private Plugins.ConversationItemCollection? item_collection; public ContentProvider(StreamInteractor stream_interactor) { this.stream_interactor = stream_interactor; - this.widget_factory = new ContentItemWidgetFactory(stream_interactor); } public void init(Plugins.ConversationItemCollection item_collection, Conversation conversation, Plugins.WidgetType type) { @@ -28,7 +26,7 @@ public class ContentProvider : ContentItemCollection, Object { } public void insert_item(ContentItem item) { - item_collection.insert_item(new ContentMetaItem(item, widget_factory)); + item_collection.insert_item(create_content_meta_item(item)); } public void remove_item(ContentItem item) { } @@ -38,7 +36,7 @@ public class ContentProvider : ContentItemCollection, Object { Gee.List items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_n_latest(conversation, n); Gee.List ret = new ArrayList(); foreach (ContentItem item in items) { - ret.add(new ContentMetaItem(item, widget_factory)); + ret.add(create_content_meta_item(item)); } return ret; } @@ -47,7 +45,7 @@ public class ContentProvider : ContentItemCollection, Object { Gee.List ret = new ArrayList(); Gee.List items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_before(conversation, before_item, n); foreach (ContentItem item in items) { - ret.add(new ContentMetaItem(item, widget_factory)); + ret.add(create_content_meta_item(item)); } return ret; } @@ -56,26 +54,30 @@ public class ContentProvider : ContentItemCollection, Object { Gee.List ret = new ArrayList(); Gee.List items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_after(conversation, after_item, n); foreach (ContentItem item in items) { - ret.add(new ContentMetaItem(item, widget_factory)); + ret.add(create_content_meta_item(item)); } return ret; } public ContentMetaItem get_content_meta_item(ContentItem content_item) { - return new ContentMetaItem(content_item, widget_factory); + return create_content_meta_item(content_item); + } + + private ContentMetaItem create_content_meta_item(ContentItem content_item) { + if (content_item.type_ == MessageItem.TYPE) { + return new MessageMetaItem(content_item, stream_interactor); + } else if (content_item.type_ == FileItem.TYPE) { + return new FileMetaItem(content_item, stream_interactor); + } + return null; } } -public class ContentMetaItem : Plugins.MetaConversationItem { - public override Jid? jid { get; set; } - public override DateTime sort_time { get; set; } - public override DateTime? display_time { get; set; } - public override Encryption encryption { get; set; } +public abstract class ContentMetaItem : Plugins.MetaConversationItem { public ContentItem content_item; - private ContentItemWidgetFactory widget_factory; - public ContentMetaItem(ContentItem content_item, ContentItemWidgetFactory widget_factory) { + protected ContentMetaItem(ContentItem content_item) { this.jid = content_item.jid; this.sort_time = content_item.sort_time; this.seccondary_sort_indicator = (long) content_item.display_time.to_unix(); @@ -96,15 +98,6 @@ public class ContentMetaItem : Plugins.MetaConversationItem { this.requires_header = true; this.content_item = content_item; - this.widget_factory = widget_factory; - } - - public override bool can_merge { get; set; default=true; } - public override bool requires_avatar { get; set; default=true; } - public override bool requires_header { get; set; default=true; } - - public override Object? get_widget(Plugins.WidgetType type) { - return widget_factory.get_widget(content_item); } } 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 8c59dde7..fe6c2dee 100644 --- a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala +++ b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala @@ -15,6 +15,8 @@ public class ConversationItemSkeleton : EventBox { public StreamInteractor stream_interactor; public Conversation conversation { get; set; } public Plugins.MetaConversationItem item; + public ContentMetaItem? content_meta_item = null; + public Widget? widget = null; private Box image_content_box = new Box(Orientation.HORIZONTAL, 8) { visible=true }; private Box header_content_box = new Box(Orientation.VERTICAL, 0) { visible=true }; @@ -25,9 +27,18 @@ public class ConversationItemSkeleton : EventBox { this.stream_interactor = stream_interactor; this.conversation = conversation; this.item = item; + this.content_meta_item = item as ContentMetaItem; this.get_style_context().add_class("message-box"); - Widget? widget = item.get_widget(Plugins.WidgetType.GTK) as Widget; + item.notify["in-edit-mode"].connect(() => { + if (item.in_edit_mode) { + this.get_style_context().add_class("edit-mode"); + } else { + this.get_style_context().remove_class("edit-mode"); + } + }); + + widget = item.get_widget(Plugins.WidgetType.GTK) as Widget; if (widget != null) { widget.valign = Align.END; header_content_box.add(widget); @@ -51,7 +62,12 @@ public class ConversationItemSkeleton : EventBox { update_margin(); } - public void update_margin() { + public void set_edit_mode() { + if (content_meta_item == null) return; + + } + + private void update_margin() { if (item.requires_header && show_skeleton && metadata_header == null) { metadata_header = new ItemMetaDataHeader(stream_interactor, conversation, item) { visible=true }; header_content_box.add(metadata_header); diff --git a/main/src/ui/conversation_content_view/conversation_view.vala b/main/src/ui/conversation_content_view/conversation_view.vala index ac6df1fc..808c6cad 100644 --- a/main/src/ui/conversation_content_view/conversation_view.vala +++ b/main/src/ui/conversation_content_view/conversation_view.vala @@ -15,6 +15,8 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins [GtkChild] public ScrolledWindow scrolled; [GtkChild] private Revealer notification_revealer; [GtkChild] private Box message_menu_box; + [GtkChild] private Button button1; + [GtkChild] private Image button1_icon; [GtkChild] private Box notifications; [GtkChild] private Box main; [GtkChild] private EventBox main_event_box; @@ -29,7 +31,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins private Gee.List item_skeletons = new Gee.ArrayList(); private ContentProvider content_populator; private SubscriptionNotitication subscription_notification; - private bool enable_menu_box = false; private double? was_value; private double? was_upper; @@ -40,8 +41,8 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins private bool firstLoad = true; private bool at_current_content = true; private bool reload_messages = true; - Widget currently_highlighted = null; - ContentItem current_highlighted_item = null; + ConversationItemSkeleton currently_highlighted = null; + ContentMetaItem? current_meta_item = null; bool mouse_inside = false; int last_y_root = -1; @@ -67,6 +68,11 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins main_event_box.events = EventMask.POINTER_MOTION_MASK; main_event_box.motion_notify_event.connect(on_motion_notify_event); + button1.clicked.connect(() => { + current_meta_item.get_item_actions(Plugins.WidgetType.GTK)[0].callback(button1, current_meta_item, currently_highlighted.widget); + update_message_menu(); + }); + return this; } @@ -95,7 +101,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins } last_y_root = y_root; - message_menu_box.visible = enable_menu_box; // Get pointer location in main int geometry_x, geometry_y, geometry_width, geometry_height, dest_x, dest_y; @@ -106,21 +111,24 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins // Get widget under pointer int h = 0; bool @break = false; - Widget? w = null; + ConversationItemSkeleton? w = null; main.@foreach((widget) => { if (break) return; h += widget.get_allocated_height(); - w = widget; + w = widget as ConversationItemSkeleton; if (h >= dest_y) { @break = true; return; } }); + if (currently_highlighted != null) currently_highlighted.unset_state_flags(StateFlags.PRELIGHT); + if (w == null) { - if (currently_highlighted != null) currently_highlighted.unset_state_flags(StateFlags.PRELIGHT); currently_highlighted = null; + current_meta_item = null; + update_message_menu(); return; } @@ -129,23 +137,36 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins w.translate_coordinates(main, 0, 0, out widget_x, out widget_y); // Get MessageItem - var iter = widgets.map_iterator(); - while (iter.next()) { - if (iter.get_value() == w) { - Plugins.MetaConversationItem meta_item = iter.get_key(); - var meta_content_item = meta_item as ContentMetaItem; - if (meta_content_item == null) return; - current_highlighted_item = meta_content_item.content_item; + foreach (Plugins.MetaConversationItem item in item_item_skeletons.keys) { + if (item_item_skeletons[item] == w) { + current_meta_item = item as ContentMetaItem; } } - // Highlight widget - if (currently_highlighted != null) currently_highlighted.unset_state_flags(StateFlags.PRELIGHT); - w.set_state_flags(StateFlags.PRELIGHT, true); - currently_highlighted = w; + update_message_menu(); + + if (current_meta_item != null) { + // Highlight widget + w.set_state_flags(StateFlags.PRELIGHT, true); + currently_highlighted = w; + + // Move message menu + message_menu_box.margin_top = widget_y - 10; + } + } - // Move message menu - message_menu_box.margin_top = widget_y - 10; + private void update_message_menu() { + if (current_meta_item == null) { + message_menu_box.visible = false; + return; + } + + var actions = current_meta_item.get_item_actions(Plugins.WidgetType.GTK); + message_menu_box.visible = actions != null && actions.size > 0; + if (actions != null && actions.size == 1) { + button1.visible = true; + button1_icon.set_from_icon_name(actions[0].icon_name, IconSize.SMALL_TOOLBAR); + } } public void initialize_for_conversation(Conversation? conversation) { @@ -163,8 +184,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins initialize_for_conversation_(conversation); display_latest(); stack.set_visible_child_name("main"); - - enable_menu_box = false; } public void initialize_around_message(Conversation conversation, ContentItem content_item) { diff --git a/main/src/ui/conversation_content_view/date_separator_populator.vala b/main/src/ui/conversation_content_view/date_separator_populator.vala index 3ddb0d9a..91485f25 100644 --- a/main/src/ui/conversation_content_view/date_separator_populator.vala +++ b/main/src/ui/conversation_content_view/date_separator_populator.vala @@ -54,10 +54,6 @@ class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Plugins.Conver public class MetaDateItem : Plugins.MetaConversationItem { public override DateTime sort_time { get; set; } - public override bool can_merge { get; set; default=false; } - public override bool requires_avatar { get; set; default=false; } - public override bool requires_header { get; set; default=false; } - private DateTime date; public MetaDateItem(DateTime date) { @@ -76,6 +72,8 @@ public class MetaDateItem : Plugins.MetaConversationItem { return box; } + public override Gee.List? get_item_actions(Plugins.WidgetType type) { return null; } + private static string get_relative_time(DateTime time) { DateTime time_local = time.to_local(); DateTime now_local = new DateTime.now_local(); diff --git a/main/src/ui/conversation_content_view/file_widget.vala b/main/src/ui/conversation_content_view/file_widget.vala index f5ba08e3..ee14af7a 100644 --- a/main/src/ui/conversation_content_view/file_widget.vala +++ b/main/src/ui/conversation_content_view/file_widget.vala @@ -7,6 +7,24 @@ using Dino.Entities; namespace Dino.Ui.ConversationSummary { +public class FileMetaItem : ContentMetaItem { + + private StreamInteractor stream_interactor; + + public FileMetaItem(ContentItem content_item, StreamInteractor stream_interactor) { + base(content_item); + this.stream_interactor = stream_interactor; + } + + public override Object? get_widget(Plugins.WidgetType type) { + FileItem file_item = content_item as FileItem; + FileTransfer transfer = file_item.file_transfer; + return new FileWidget(stream_interactor, transfer) { visible=true }; + } + + public override Gee.List? get_item_actions(Plugins.WidgetType type) { return null; } +} + public class FileWidget : Box { enum State { diff --git a/main/src/ui/conversation_content_view/message_item.vala b/main/src/ui/conversation_content_view/message_item.vala deleted file mode 100644 index e69de29b..00000000 diff --git a/main/src/ui/conversation_content_view/message_widget.vala b/main/src/ui/conversation_content_view/message_widget.vala new file mode 100644 index 00000000..71094c71 --- /dev/null +++ b/main/src/ui/conversation_content_view/message_widget.vala @@ -0,0 +1,211 @@ +using Gee; +using Gdk; +using Gtk; +using Pango; +using Xmpp; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +public class MessageMetaItem : ContentMetaItem { + + private StreamInteractor stream_interactor; + private MessageItemWidget message_item_widget; + private MessageItem message_item; + + public MessageMetaItem(ContentItem content_item, StreamInteractor stream_interactor) { + base(content_item); + message_item = content_item as MessageItem; + this.stream_interactor = stream_interactor; + } + + public override Object? get_widget(Plugins.WidgetType type) { + message_item_widget = new MessageItemWidget(stream_interactor, content_item) { visible=true }; + + message_item_widget.edit_cancelled.connect(() => { this.in_edit_mode = false; }); + message_item_widget.edit_sent.connect(on_edit_send); + + stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.connect(on_received_correction); + + return message_item_widget; + } + + public override Gee.List? get_item_actions(Plugins.WidgetType type) { + if (content_item as FileItem != null) return null; + + bool allowed = stream_interactor.get_module(MessageCorrection.IDENTITY).is_own_correction_allowed(message_item.conversation, message_item.message); + Gee.List actions = new ArrayList(); + if (allowed && !in_edit_mode) { + Plugins.MessageAction action1 = new Plugins.MessageAction(); + action1.icon_name = "document-edit-symbolic"; + action1.callback = (button, content_meta_item_activated, widget) => { + message_item_widget.set_edit_mode(); + this.in_edit_mode = true; + }; + actions.add(action1); + } + return actions; + } + + private void on_edit_send(string text) { + stream_interactor.get_module(MessageCorrection.IDENTITY).send_correction(message_item.conversation, message_item.message, text); + this.in_edit_mode = false; + } + + private void on_received_correction(ContentItem content_item) { + if (this.content_item.id == content_item.id) { + this.content_item = content_item; + message_item = content_item as MessageItem; + message_item_widget.content_item = content_item; + message_item_widget.update_label(); + } + } +} + +public class MessageItemWidget : SizeRequestBin { + + public signal void edit_cancelled(); + public signal void edit_sent(string text); + + StreamInteractor stream_interactor; + public ContentItem content_item; + + Label label = new Label("") { use_markup=true, xalign=0, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, vexpand=true, visible=true }; + MessageItemEditMode? edit_mode = null; + ChatTextViewController? controller = null; + + ulong realize_id = -1; + ulong style_updated_id = -1; + + construct { + this.add(label); + this.size_request_mode = SizeRequestMode.HEIGHT_FOR_WIDTH; + } + + public MessageItemWidget(StreamInteractor stream_interactor, ContentItem content_item) { + this.stream_interactor = stream_interactor; + this.content_item = content_item; + + update_label(); + } + + public void set_edit_mode() { + + MessageItem message_item = content_item as MessageItem; + Message message = message_item.message; + + if (edit_mode == null) { + edit_mode = new MessageItemEditMode(); + controller = new ChatTextViewController(edit_mode.chat_text_view, stream_interactor); + Conversation conversation = message_item.conversation; + controller.initialize_for_conversation(conversation); + + edit_mode.cancelled.connect(() => { + edit_cancelled(); + unset_edit_mode(); + }); + edit_mode.send.connect(() => { + edit_sent(edit_mode.chat_text_view.text_view.buffer.text); + unset_edit_mode(); + }); + } + + edit_mode.chat_text_view.text_view.buffer.text = message.body; + + this.remove(label); + this.add(edit_mode); + + edit_mode.chat_text_view.text_view.grab_focus(); + } + + public void unset_edit_mode() { + this.remove(edit_mode); + this.add(label); + } + + public void update_label() { + label.label = generate_markup_text(content_item); + } + + private string generate_markup_text(ContentItem item) { + MessageItem message_item = item as MessageItem; + Conversation conversation = message_item.conversation; + Message message = message_item.message; + + bool theme_dependent = false; + + string markup_text = message.body; + if (markup_text.length > 10000) { + markup_text = markup_text.substring(0, 10000) + " [" + _("Message too long") + "]"; + } + if (message.body.has_prefix("/me")) { + markup_text = markup_text.substring(3); + } + + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + markup_text = Util.parse_add_markup(markup_text, conversation.nickname, true, true); + } else { + markup_text = Util.parse_add_markup(markup_text, null, true, true); + } + + if (message.body.has_prefix("/me")) { + string display_name = Util.get_participant_display_name(stream_interactor, conversation, message.from); + string color = Util.get_name_hex_color(stream_interactor, conversation.account, message.real_jid ?? message.from, Util.is_dark_theme(label)); + markup_text = @"$(Markup.escape_text(display_name))" + markup_text; + theme_dependent = true; + } + + int only_emoji_count = Util.get_only_emoji_count(markup_text); + if (only_emoji_count != -1) { + string size_str = only_emoji_count < 5 ? "xx-large" : "large"; + markup_text = @"" + markup_text + ""; + } + + if (message.edit_to != null) { + string color = Util.is_dark_theme(label) ? "#808080" : "#909090"; + markup_text += " (%s)".printf(color, _("edited")); + theme_dependent = true; + } + + if (theme_dependent && realize_id == -1) { + realize_id = label.realize.connect(update_label); + style_updated_id = label.style_updated.connect(update_label); + } else if (!theme_dependent && realize_id != -1) { + label.disconnect(realize_id); + label.disconnect(style_updated_id); + } + return markup_text; + } +} + +[GtkTemplate (ui = "/im/dino/Dino/message_item_widget_edit_mode.ui")] +public class MessageItemEditMode : Box { + + public signal void cancelled(); + public signal void send(); + + [GtkChild] public MenuButton emoji_button; + [GtkChild] public ChatTextView chat_text_view; + [GtkChild] public Button cancel_button; + [GtkChild] public Button send_button; + [GtkChild] public Frame frame; + + construct { + Util.force_css(frame, "* { border-radius: 3px; }"); + + EmojiChooser chooser = new EmojiChooser(); + chooser.emoji_picked.connect((emoji) => { + chat_text_view.text_view.buffer.insert_at_cursor(emoji, emoji.data.length); + }); + emoji_button.set_popover(chooser); + + cancel_button.clicked.connect(() => cancelled()); + send_button.clicked.connect(() => send()); + chat_text_view.cancel_input.connect(() => cancelled()); + chat_text_view.send_text.connect(() => send()); + + } +} + +} diff --git a/main/src/ui/conversation_selector/conversation_selector_row.vala b/main/src/ui/conversation_selector/conversation_selector_row.vala index 6234e9c8..6dc953df 100644 --- a/main/src/ui/conversation_selector/conversation_selector_row.vala +++ b/main/src/ui/conversation_selector/conversation_selector_row.vala @@ -86,6 +86,12 @@ public class ConversationSelectorRow : ListBoxRow { content_item_received(item); } }); + stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.connect((item) => { + if (last_content_item != null && last_content_item.id == item.id) { + content_item_received(item); + } + }); + last_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_latest(conversation); x_button.clicked.connect(() => { diff --git a/main/src/ui/conversation_view_controller.vala b/main/src/ui/conversation_view_controller.vala index abb8ab57..2e03de8f 100644 --- a/main/src/ui/conversation_view_controller.vala +++ b/main/src/ui/conversation_view_controller.vala @@ -40,7 +40,6 @@ public class ConversationViewController : Object { view.conversation_frame.init(stream_interactor); // drag 'n drop file upload - Gtk.drag_dest_unset(view.chat_input.text_input); Gtk.drag_dest_set(view, DestDefaults.ALL, target_list, Gdk.DragAction.COPY); view.drag_data_received.connect(this.on_drag_data_received); @@ -52,7 +51,9 @@ public class ConversationViewController : Object { // goto-end floating button var vadjustment = view.conversation_frame.scrolled.vadjustment; vadjustment.notify["value"].connect(() => { - view.goto_end_revealer.reveal_child = vadjustment.value < vadjustment.upper - vadjustment.page_size; + bool button_active = vadjustment.value < vadjustment.upper - vadjustment.page_size; + view.goto_end_revealer.reveal_child = button_active; + view.goto_end_revealer.visible = button_active; }); view.goto_end_button.clicked.connect(() => { view.conversation_frame.initialize_for_conversation(conversation); @@ -158,9 +159,11 @@ public class ConversationViewController : Object { if ((event.state & ModifierType.CONTROL_MASK) > 0) { return false; } - view.chat_input.text_input.key_press_event(event); - view.chat_input.text_input.grab_focus(); - return true; + if (view.chat_input.chat_text_view.text_view.key_press_event(event)) { + view.chat_input.chat_text_view.text_view.grab_focus(); + return true; + } + return false; } } } diff --git a/main/src/ui/util/size_request_box.vala b/main/src/ui/util/size_request_box.vala index c9adcb70..a2828262 100644 --- a/main/src/ui/util/size_request_box.vala +++ b/main/src/ui/util/size_request_box.vala @@ -1,11 +1,19 @@ using Gtk; namespace Dino.Ui { -class SizeRequestBox : Box { +public class SizeRequestBox : Box { public SizeRequestMode size_request_mode { get; set; default = SizeRequestMode.CONSTANT_SIZE; } public override Gtk.SizeRequestMode get_request_mode() { return size_request_mode; } } -} \ No newline at end of file + +public class SizeRequestBin : Bin { + public SizeRequestMode size_request_mode { get; set; default = SizeRequestMode.CONSTANT_SIZE; } + + public override Gtk.SizeRequestMode get_request_mode() { + return size_request_mode; + } +} +} -- cgit v1.2.3-54-g00ecf