From 4ed6204fc2879c52fe88caa5711dea37cd4ae201 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Fri, 21 Feb 2020 02:49:53 +0100 Subject: Rename folders/files conversation_summary -> conversation_content_view --- main/CMakeLists.txt | 24 +- .../conversation_content_view/image_toolbar.ui | 41 +++ .../item_metadata_header.ui | 55 +++ main/data/conversation_content_view/view.ui | 84 +++++ main/data/conversation_summary/image_toolbar.ui | 41 --- .../conversation_summary/item_metadata_header.ui | 55 --- main/data/conversation_summary/view.ui | 84 ----- main/src/ui/chat_input/chat_input_controller.vala | 150 +++++++++ main/src/ui/chat_input_controller.vala | 150 --------- .../chat_state_populator.vala | 126 +++++++ .../content_item_widget_factory.vala | 114 +++++++ .../content_populator.vala | 111 ++++++ .../conversation_item_skeleton.vala | 221 ++++++++++++ .../conversation_view.vala | 374 +++++++++++++++++++++ .../date_separator_populator.vala | 105 ++++++ .../ui/conversation_content_view/file_widget.vala | 338 +++++++++++++++++++ .../ui/conversation_content_view/message_item.vala | 0 .../subscription_notification.vala | 55 +++ .../conversation_summary/chat_state_populator.vala | 126 ------- .../content_item_widget_factory.vala | 114 ------- .../ui/conversation_summary/content_populator.vala | 111 ------ .../conversation_item_skeleton.vala | 221 ------------ .../ui/conversation_summary/conversation_view.vala | 374 --------------------- .../date_separator_populator.vala | 105 ------ main/src/ui/conversation_summary/file_widget.vala | 338 ------------------- main/src/ui/conversation_summary/message_item.vala | 0 .../subscription_notification.vala | 55 --- 27 files changed, 1786 insertions(+), 1786 deletions(-) create mode 100644 main/data/conversation_content_view/image_toolbar.ui create mode 100644 main/data/conversation_content_view/item_metadata_header.ui create mode 100644 main/data/conversation_content_view/view.ui delete mode 100644 main/data/conversation_summary/image_toolbar.ui delete mode 100644 main/data/conversation_summary/item_metadata_header.ui delete mode 100644 main/data/conversation_summary/view.ui create mode 100644 main/src/ui/chat_input/chat_input_controller.vala delete mode 100644 main/src/ui/chat_input_controller.vala create mode 100644 main/src/ui/conversation_content_view/chat_state_populator.vala create mode 100644 main/src/ui/conversation_content_view/content_item_widget_factory.vala create mode 100644 main/src/ui/conversation_content_view/content_populator.vala create mode 100644 main/src/ui/conversation_content_view/conversation_item_skeleton.vala create mode 100644 main/src/ui/conversation_content_view/conversation_view.vala create mode 100644 main/src/ui/conversation_content_view/date_separator_populator.vala create mode 100644 main/src/ui/conversation_content_view/file_widget.vala create mode 100644 main/src/ui/conversation_content_view/message_item.vala create mode 100644 main/src/ui/conversation_content_view/subscription_notification.vala delete mode 100644 main/src/ui/conversation_summary/chat_state_populator.vala delete mode 100644 main/src/ui/conversation_summary/content_item_widget_factory.vala delete mode 100644 main/src/ui/conversation_summary/content_populator.vala delete mode 100644 main/src/ui/conversation_summary/conversation_item_skeleton.vala delete mode 100644 main/src/ui/conversation_summary/conversation_view.vala delete mode 100644 main/src/ui/conversation_summary/date_separator_populator.vala delete mode 100644 main/src/ui/conversation_summary/file_widget.vala delete mode 100644 main/src/ui/conversation_summary/message_item.vala delete mode 100644 main/src/ui/conversation_summary/subscription_notification.vala (limited to 'main') diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 61fbc374..c6d92fa4 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -48,9 +48,9 @@ set(RESOURCE_LIST global_search.ui conversation_selector/chat_row_tooltip.ui conversation_selector/conversation_row.ui - conversation_summary/image_toolbar.ui - conversation_summary/item_metadata_header.ui - conversation_summary/view.ui + conversation_content_view/image_toolbar.ui + conversation_content_view/item_metadata_header.ui + conversation_content_view/view.ui manage_accounts/account_row.ui manage_accounts/add_account_dialog.ui manage_accounts/dialog.ui @@ -95,7 +95,6 @@ SOURCES src/ui/application.vala src/ui/avatar_drawer.vala src/ui/avatar_image.vala - src/ui/chat_input_controller.vala src/ui/conversation_list_titlebar.vala src/ui/conversation_view.vala src/ui/conversation_view_controller.vala @@ -115,6 +114,7 @@ SOURCES src/ui/add_conversation/select_contact_dialog.vala src/ui/add_conversation/select_jid_fragment.vala + src/ui/chat_input/chat_input_controller.vala src/ui/chat_input/edit_history.vala src/ui/chat_input/encryption_button.vala src/ui/chat_input/occupants_tab_completer.vala @@ -129,14 +129,14 @@ SOURCES src/ui/conversation_selector/conversation_selector_row.vala src/ui/conversation_selector/conversation_selector.vala - src/ui/conversation_summary/chat_state_populator.vala - src/ui/conversation_summary/content_item_widget_factory.vala - src/ui/conversation_summary/content_populator.vala - src/ui/conversation_summary/conversation_item_skeleton.vala - src/ui/conversation_summary/conversation_view.vala - src/ui/conversation_summary/date_separator_populator.vala - src/ui/conversation_summary/file_widget.vala - src/ui/conversation_summary/subscription_notification.vala + src/ui/conversation_content_view/chat_state_populator.vala + src/ui/conversation_content_view/content_item_widget_factory.vala + src/ui/conversation_content_view/content_populator.vala + src/ui/conversation_content_view/conversation_item_skeleton.vala + src/ui/conversation_content_view/conversation_view.vala + src/ui/conversation_content_view/date_separator_populator.vala + src/ui/conversation_content_view/file_widget.vala + src/ui/conversation_content_view/subscription_notification.vala src/ui/conversation_titlebar/menu_entry.vala src/ui/conversation_titlebar/occupants_entry.vala diff --git a/main/data/conversation_content_view/image_toolbar.ui b/main/data/conversation_content_view/image_toolbar.ui new file mode 100644 index 00000000..562f944b --- /dev/null +++ b/main/data/conversation_content_view/image_toolbar.ui @@ -0,0 +1,41 @@ + + + + end + end + 10 + True + + + True + + + True + + + 25 + middle + True + 5 + True + + + + + none + True + + + view-fullscreen-symbolic + 1 + True + + + + + + + + + + diff --git a/main/data/conversation_content_view/item_metadata_header.ui b/main/data/conversation_content_view/item_metadata_header.ui new file mode 100644 index 00000000..93940c9a --- /dev/null +++ b/main/data/conversation_content_view/item_metadata_header.ui @@ -0,0 +1,55 @@ + + + + diff --git a/main/data/conversation_content_view/view.ui b/main/data/conversation_content_view/view.ui new file mode 100644 index 00000000..51133890 --- /dev/null +++ b/main/data/conversation_content_view/view.ui @@ -0,0 +1,84 @@ + + + + diff --git a/main/data/conversation_summary/image_toolbar.ui b/main/data/conversation_summary/image_toolbar.ui deleted file mode 100644 index 562f944b..00000000 --- a/main/data/conversation_summary/image_toolbar.ui +++ /dev/null @@ -1,41 +0,0 @@ - - - - end - end - 10 - True - - - True - - - True - - - 25 - middle - True - 5 - True - - - - - none - True - - - view-fullscreen-symbolic - 1 - True - - - - - - - - - - diff --git a/main/data/conversation_summary/item_metadata_header.ui b/main/data/conversation_summary/item_metadata_header.ui deleted file mode 100644 index 93940c9a..00000000 --- a/main/data/conversation_summary/item_metadata_header.ui +++ /dev/null @@ -1,55 +0,0 @@ - - - - diff --git a/main/data/conversation_summary/view.ui b/main/data/conversation_summary/view.ui deleted file mode 100644 index 51133890..00000000 --- a/main/data/conversation_summary/view.ui +++ /dev/null @@ -1,84 +0,0 @@ - - - - diff --git a/main/src/ui/chat_input/chat_input_controller.vala b/main/src/ui/chat_input/chat_input_controller.vala new file mode 100644 index 00000000..59a2abb4 --- /dev/null +++ b/main/src/ui/chat_input/chat_input_controller.vala @@ -0,0 +1,150 @@ +using Gee; +using Gdk; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui { + +public class ChatInputController : Object { + + public new string? conversation_display_name { get; set; } + public string? conversation_topic { get; set; } + + private Conversation? conversation; + private ChatInput.View chat_input; + private Label status_description_label; + + private StreamInteractor stream_interactor; + private Plugins.InputFieldStatus input_field_status; + + 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; + + 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.encryption_widget.encryption_changed.connect(on_encryption_changed); + + stream_interactor.get_module(FileManager.IDENTITY).upload_available.connect(on_upload_available); + } + + + + public void set_conversation(Conversation conversation) { + this.conversation = conversation; + + 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); + } + + private void on_encryption_changed(Plugins.EncryptionListEntry? encryption_entry) { + reset_input_field_status(); + + if (encryption_entry == null) return; + + encryption_entry.encryption_activated(conversation, set_input_field_status); + } + + private void set_input_field_status(Plugins.InputFieldStatus? status) { + input_field_status = status; + + chat_input.set_input_state(status.message_type); + status_description_label.label = status.message; + + chat_input.file_button.sensitive = status.input_state == Plugins.InputFieldStatus.InputState.NORMAL; + } + + private void reset_input_field_status() { + set_input_field_status(new Plugins.InputFieldStatus("", Plugins.InputFieldStatus.MessageType.NONE, Plugins.InputFieldStatus.InputState.NORMAL)); + } + + private void on_upload_available(Account account) { + if (conversation != null && conversation.account.equals(account)) { + chat_input.file_button.visible = true; + chat_input.file_separator.visible = true; + } + } + + private void send_text() { + // Don't do anything if we're in a NO_SEND state. Don't clear the chat input, don't send. + if (input_field_status.input_state == Plugins.InputFieldStatus.InputState.NO_SEND) { + chat_input.highlight_state_description(); + return; + } + + string text = chat_input.text_input.buffer.text; + chat_input.text_input.buffer.text = ""; + if (text.has_prefix("/")) { + string[] token = text.split(" ", 2); + switch(token[0]) { + case "/me": + // Just send as is. + break; + case "/say": + if (token.length == 1) return; + text = token[1]; + break; + case "/kick": + stream_interactor.get_module(MucManager.IDENTITY).kick(conversation.account, conversation.counterpart, token[1]); + return; + case "/affiliate": + if (token.length > 1) { + string[] user_role = token[1].split(" ", 2); + if (user_role.length == 2) { + stream_interactor.get_module(MucManager.IDENTITY).change_affiliation(conversation.account, conversation.counterpart, user_role[0].strip(), user_role[1].strip()); + } + } + return; + case "/nick": + stream_interactor.get_module(MucManager.IDENTITY).change_nick(conversation.account, conversation.counterpart, token[1]); + return; + case "/ping": + Xmpp.XmppStream? stream = stream_interactor.get_stream(conversation.account); + try { + stream.get_module(Xmpp.Xep.Ping.Module.IDENTITY).send_ping(stream, conversation.counterpart.with_resource(token[1]), null); + } catch (Xmpp.InvalidJidError e) { + warning("Could not ping invalid Jid: %s", e.message); + } + return; + case "/topic": + stream_interactor.get_module(MucManager.IDENTITY).change_subject(conversation.account, conversation.counterpart, token[1]); + return; + default: + if (token[0].has_prefix("//")) { + text = text.substring(1); + } else { + string cmd_name = token[0].substring(1); + Dino.Application app = GLib.Application.get_default() as Dino.Application; + if (app != null && app.plugin_registry.text_commands.has_key(cmd_name)) { + string? new_text = app.plugin_registry.text_commands[cmd_name].handle_command(token[1], conversation); + if (new_text == null) return; + text = (!)new_text; + } + } + break; + } + } + stream_interactor.get_module(MessageProcessor.IDENTITY).send_text(text, conversation); + } + + private void on_text_input_changed() { + if (chat_input.text_input.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_controller.vala b/main/src/ui/chat_input_controller.vala deleted file mode 100644 index 59a2abb4..00000000 --- a/main/src/ui/chat_input_controller.vala +++ /dev/null @@ -1,150 +0,0 @@ -using Gee; -using Gdk; -using Gtk; - -using Dino.Entities; - -namespace Dino.Ui { - -public class ChatInputController : Object { - - public new string? conversation_display_name { get; set; } - public string? conversation_topic { get; set; } - - private Conversation? conversation; - private ChatInput.View chat_input; - private Label status_description_label; - - private StreamInteractor stream_interactor; - private Plugins.InputFieldStatus input_field_status; - - 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; - - 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.encryption_widget.encryption_changed.connect(on_encryption_changed); - - stream_interactor.get_module(FileManager.IDENTITY).upload_available.connect(on_upload_available); - } - - - - public void set_conversation(Conversation conversation) { - this.conversation = conversation; - - 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); - } - - private void on_encryption_changed(Plugins.EncryptionListEntry? encryption_entry) { - reset_input_field_status(); - - if (encryption_entry == null) return; - - encryption_entry.encryption_activated(conversation, set_input_field_status); - } - - private void set_input_field_status(Plugins.InputFieldStatus? status) { - input_field_status = status; - - chat_input.set_input_state(status.message_type); - status_description_label.label = status.message; - - chat_input.file_button.sensitive = status.input_state == Plugins.InputFieldStatus.InputState.NORMAL; - } - - private void reset_input_field_status() { - set_input_field_status(new Plugins.InputFieldStatus("", Plugins.InputFieldStatus.MessageType.NONE, Plugins.InputFieldStatus.InputState.NORMAL)); - } - - private void on_upload_available(Account account) { - if (conversation != null && conversation.account.equals(account)) { - chat_input.file_button.visible = true; - chat_input.file_separator.visible = true; - } - } - - private void send_text() { - // Don't do anything if we're in a NO_SEND state. Don't clear the chat input, don't send. - if (input_field_status.input_state == Plugins.InputFieldStatus.InputState.NO_SEND) { - chat_input.highlight_state_description(); - return; - } - - string text = chat_input.text_input.buffer.text; - chat_input.text_input.buffer.text = ""; - if (text.has_prefix("/")) { - string[] token = text.split(" ", 2); - switch(token[0]) { - case "/me": - // Just send as is. - break; - case "/say": - if (token.length == 1) return; - text = token[1]; - break; - case "/kick": - stream_interactor.get_module(MucManager.IDENTITY).kick(conversation.account, conversation.counterpart, token[1]); - return; - case "/affiliate": - if (token.length > 1) { - string[] user_role = token[1].split(" ", 2); - if (user_role.length == 2) { - stream_interactor.get_module(MucManager.IDENTITY).change_affiliation(conversation.account, conversation.counterpart, user_role[0].strip(), user_role[1].strip()); - } - } - return; - case "/nick": - stream_interactor.get_module(MucManager.IDENTITY).change_nick(conversation.account, conversation.counterpart, token[1]); - return; - case "/ping": - Xmpp.XmppStream? stream = stream_interactor.get_stream(conversation.account); - try { - stream.get_module(Xmpp.Xep.Ping.Module.IDENTITY).send_ping(stream, conversation.counterpart.with_resource(token[1]), null); - } catch (Xmpp.InvalidJidError e) { - warning("Could not ping invalid Jid: %s", e.message); - } - return; - case "/topic": - stream_interactor.get_module(MucManager.IDENTITY).change_subject(conversation.account, conversation.counterpart, token[1]); - return; - default: - if (token[0].has_prefix("//")) { - text = text.substring(1); - } else { - string cmd_name = token[0].substring(1); - Dino.Application app = GLib.Application.get_default() as Dino.Application; - if (app != null && app.plugin_registry.text_commands.has_key(cmd_name)) { - string? new_text = app.plugin_registry.text_commands[cmd_name].handle_command(token[1], conversation); - if (new_text == null) return; - text = (!)new_text; - } - } - break; - } - } - stream_interactor.get_module(MessageProcessor.IDENTITY).send_text(text, conversation); - } - - private void on_text_input_changed() { - if (chat_input.text_input.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/conversation_content_view/chat_state_populator.vala b/main/src/ui/conversation_content_view/chat_state_populator.vala new file mode 100644 index 00000000..54b41b7d --- /dev/null +++ b/main/src/ui/conversation_content_view/chat_state_populator.vala @@ -0,0 +1,126 @@ +using Gee; +using Gtk; + +using Dino.Entities; +using Xmpp; + +namespace Dino.Ui.ConversationSummary { + +class ChatStatePopulator : Plugins.ConversationItemPopulator, Plugins.ConversationAdditionPopulator, Object { + + public string id { get { return "chat_state"; } } + + private StreamInteractor? stream_interactor; + private Conversation? current_conversation; + private Plugins.ConversationItemCollection? item_collection; + + private MetaChatStateItem? meta_item; + + public ChatStatePopulator(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).received_state.connect((conversation, state) => { + if (current_conversation != null && current_conversation.equals(conversation)) { + update_chat_state(); + } + }); + stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect((message, conversation) => { + if (conversation.equals(current_conversation)) { + update_chat_state(); + } + }); + } + + public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection, Plugins.WidgetType type) { + current_conversation = conversation; + this.item_collection = item_collection; + this.meta_item = null; + + update_chat_state(); + } + + public void close(Conversation conversation) { } + + public void populate_timespan(Conversation conversation, DateTime from, DateTime to) { } + + private void update_chat_state() { + Gee.List? typing_jids = stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).get_typing_jids(current_conversation); + + if (meta_item != null && typing_jids == null) { + // Remove state (stoped typing) + item_collection.remove_item(meta_item); + meta_item = null; + } else if (meta_item != null && typing_jids != null) { + // Update state (other people typing in MUC) + meta_item.set_new(typing_jids); + } else if (typing_jids != null) { + // New state (started typing) + meta_item = new MetaChatStateItem(stream_interactor, current_conversation, typing_jids); + item_collection.insert_item(meta_item); + } + } +} + +private class MetaChatStateItem : Plugins.MetaConversationItem { + public override bool dim { get; set; default=true; } + 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(); + private Label label; + private AvatarImage image; + + public MetaChatStateItem(StreamInteractor stream_interactor, Conversation conversation, Gee.List jids) { + this.stream_interactor = stream_interactor; + this.conversation = conversation; + this.jids = jids; + } + + public override Object? get_widget(Plugins.WidgetType widget_type) { + label = new Label("") { xalign=0, vexpand=true, visible=true }; + label.get_style_context().add_class("dim-label"); + image = new AvatarImage() { margin_top=2, valign=Align.START, visible=true }; + + Box image_content_box = new Box(Orientation.HORIZONTAL, 8) { visible=true }; + image_content_box.add(image); + image_content_box.add(label); + + update(); + return image_content_box; + } + + public void set_new(Gee.List jids) { + this.jids = jids; + update(); + } + + private void update() { + if (image == null || label == null) return; + + image.set_conversation_participants(stream_interactor, conversation, jids.to_array()); + + Gee.List display_names = new ArrayList(); + foreach (Jid jid in jids) { + display_names.add(Util.get_participant_display_name(stream_interactor, conversation, jid)); + } + string new_text = ""; + if (jids.size > 3) { + new_text = _("%s, %s and %i others").printf(display_names[0], display_names[1], jids.size - 2); + } else if (jids.size == 3) { + new_text = _("%s, %s and %s are typing…").printf(display_names[0], display_names[1], display_names[2]); + } else if (jids.size == 2) { + new_text =_("%s and %s are typing…").printf(display_names[0], display_names[1]); + } else { + new_text = "%s is typing…".printf(display_names[0]); + } + + label.label = new_text; + } +} + +} 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 new file mode 100644 index 00000000..54283e75 --- /dev/null +++ b/main/src/ui/conversation_content_view/content_item_widget_factory.vala @@ -0,0 +1,114 @@ +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=FileItem.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 new file mode 100644 index 00000000..e8eee06c --- /dev/null +++ b/main/src/ui/conversation_content_view/content_populator.vala @@ -0,0 +1,111 @@ +using Gee; +using Gtk; + +using Xmpp; +using Dino.Entities; + +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) { + if (current_conversation != null) { + stream_interactor.get_module(ContentItemStore.IDENTITY).uninit(current_conversation, this); + } + current_conversation = conversation; + this.item_collection = item_collection; + stream_interactor.get_module(ContentItemStore.IDENTITY).init(conversation, this); + } + + public void insert_item(ContentItem item) { + item_collection.insert_item(new ContentMetaItem(item, widget_factory)); + } + + public void remove_item(ContentItem item) { } + + + public Gee.List populate_latest(Conversation conversation, int n) { + 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)); + } + return ret; + } + + public Gee.List populate_before(Conversation conversation, ContentItem before_item, int n) { + 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)); + } + return ret; + } + + public Gee.List populate_after(Conversation conversation, ContentItem after_item, int n) { + 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)); + } + return ret; + } + + public ContentMetaItem get_content_meta_item(ContentItem content_item) { + return new ContentMetaItem(content_item, widget_factory); + } +} + +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 ContentItem content_item; + private ContentItemWidgetFactory widget_factory; + + public ContentMetaItem(ContentItem content_item, ContentItemWidgetFactory widget_factory) { + this.jid = content_item.jid; + this.sort_time = content_item.sort_time; + this.seccondary_sort_indicator = (long) content_item.display_time.to_unix(); + this.tertiary_sort_indicator = content_item.id; + this.display_time = content_item.display_time; + this.encryption = content_item.encryption; + this.mark = content_item.mark; + + WeakRef weak_item = WeakRef(content_item); + content_item.notify["mark"].connect(() => { + ContentItem? ci = weak_item.get() as ContentItem; + if (ci == null) return; + this.mark = ci.mark; + }); + + this.can_merge = true; + this.requires_avatar = true; + 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 new file mode 100644 index 00000000..dbba2276 --- /dev/null +++ b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala @@ -0,0 +1,221 @@ +using Gee; +using Gdk; +using Gtk; +using Markup; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +public class ConversationItemSkeleton : EventBox { + + private AvatarImage image = new AvatarImage() { margin_top=2, valign=Align.START, visible=true, allow_gray = false }; + + public bool show_skeleton { get; set; } + public bool last_group_item { get; set; } + + public StreamInteractor stream_interactor; + public Conversation conversation { get; set; } + public Plugins.MetaConversationItem item; + + private Box image_content_box = new Box(Orientation.HORIZONTAL, 8) { visible=true }; + private Box header_content_box = new Box(Orientation.VERTICAL, 0) { visible=true }; + private ItemMetaDataHeader metadata_header; + + public ConversationItemSkeleton(StreamInteractor stream_interactor, Conversation conversation, Plugins.MetaConversationItem item) { + this.stream_interactor = stream_interactor; + this.conversation = conversation; + this.item = item; + this.get_style_context().add_class("message-box"); + + if (item.requires_avatar) { + image.set_conversation_participant(stream_interactor, conversation, item.jid); + image_content_box.add(image); + } + if (item.requires_header) { + metadata_header = new ItemMetaDataHeader(stream_interactor, conversation, item) { visible=true }; + header_content_box.add(metadata_header); + } + + Widget? widget = item.get_widget(Plugins.WidgetType.GTK) as Widget; + if (widget != null) { + widget.valign = Align.END; + header_content_box.add(widget); + } + + image_content_box.add(header_content_box); + this.add(image_content_box); + + if (item.get_type().is_a(typeof(ContentMetaItem))) { + this.motion_notify_event.connect((event) => { + this.set_state_flags(StateFlags.PRELIGHT, false); + return false; + }); + this.enter_notify_event.connect((event) => { + this.set_state_flags(StateFlags.PRELIGHT, false); + return false; + }); + this.leave_notify_event.connect((event) => { + this.unset_state_flags(StateFlags.PRELIGHT); + return false; + }); + } + + this.notify["show-skeleton"].connect(update_margin); + this.notify["last-group-item"].connect(update_margin); + + this.show_skeleton = true; + this.last_group_item = true; + update_margin(); + this.notify["show-skeleton"].connect(update_margin); + } + + public void update_time() { + if (metadata_header != null) { + metadata_header.update_time(); + } + } + + public void update_margin() { + image.visible = this.show_skeleton; + if (metadata_header != null) { + metadata_header.visible = this.show_skeleton; + } + image_content_box.margin_start = this.show_skeleton ? 15 : 58; + image_content_box.margin_end = 15; + + if (this.show_skeleton && this.last_group_item) { + image_content_box.margin_top = 8; + image_content_box.margin_bottom = 8; + } else { + image_content_box.margin_top = 4; + image_content_box.margin_bottom = 4; + } + } +} + +[GtkTemplate (ui = "/im/dino/Dino/conversation_content_view/item_metadata_header.ui")] +public class ItemMetaDataHeader : Box { + [GtkChild] public Label name_label; + [GtkChild] public Label dot_label; + [GtkChild] public Label time_label; + [GtkChild] public Image encryption_image; + [GtkChild] public Image received_image; + + public static IconSize ICON_SIZE_HEADER = Gtk.icon_size_register("im.dino.Dino.HEADER_ICON", 17, 12); + + private StreamInteractor stream_interactor; + private Conversation conversation; + private Plugins.MetaConversationItem item; + private ArrayList items = new ArrayList(); + + public ItemMetaDataHeader(StreamInteractor stream_interactor, Conversation conversation, Plugins.MetaConversationItem item) { + this.stream_interactor = stream_interactor; + this.conversation = conversation; + this.item = item; + items.add(item); + + update_name_label(); + name_label.style_updated.connect(update_name_label); + if (item.encryption != Encryption.NONE) { + encryption_image.visible = true; + encryption_image.set_from_icon_name("dino-changes-prevent-symbolic", ICON_SIZE_HEADER); + } + update_time(); + + item.notify["mark"].connect_after(update_received_mark); + update_received_mark(); + } + + public void update_time() { + if (item.display_time != null) { + time_label.label = get_relative_time(item.display_time.to_local()).to_string(); + } + } + + private void update_name_label() { + string display_name = Markup.escape_text(Util.get_participant_display_name(stream_interactor, conversation, item.jid)); + string color = Util.get_name_hex_color(stream_interactor, conversation.account, item.jid, Util.is_dark_theme(name_label)); + name_label.label = @"$display_name"; + } + + private void update_received_mark() { + bool all_received = true; + bool all_read = true; + bool all_sent = true; + foreach (Plugins.MetaConversationItem item in items) { + if (item.mark == Message.Marked.WONTSEND) { + received_image.visible = true; + received_image.set_from_icon_name("dialog-warning-symbolic", ICON_SIZE_HEADER); + Util.force_error_color(received_image); + Util.force_error_color(encryption_image); + Util.force_error_color(time_label); + string error_text = _("Unable to send message"); + received_image.tooltip_text = error_text; + encryption_image.tooltip_text = error_text; + time_label.tooltip_text = error_text; + return; + } else if (item.mark != Message.Marked.READ) { + all_read = false; + if (item.mark != Message.Marked.RECEIVED) { + all_received = false; + if (item.mark == Message.Marked.UNSENT) { + all_sent = false; + } + } + } + } + if (all_read) { + received_image.visible = true; + received_image.set_from_icon_name("dino-double-tick-symbolic", ICON_SIZE_HEADER); + } else if (all_received) { + received_image.visible = true; + received_image.set_from_icon_name("dino-tick-symbolic", ICON_SIZE_HEADER); + } else if (!all_sent) { + received_image.visible = true; + received_image.set_from_icon_name("image-loading-symbolic", ICON_SIZE_HEADER); + } else if (received_image.visible) { + received_image.set_from_icon_name("image-loading-symbolic", ICON_SIZE_HEADER); + + } + } + + public static string format_time(DateTime datetime, string format_24h, string format_12h) { + string format = Util.is_24h_format() ? format_24h : format_12h; + if (!get_charset(null)) { + // No UTF-8 support, use simple colon for time instead + format = format.replace("∶", ":"); + } + return datetime.format(format); + } + + public static string get_relative_time(DateTime datetime) { + DateTime now = new DateTime.now_local(); + TimeSpan timespan = now.difference(datetime); + if (timespan > 365 * TimeSpan.DAY) { + return format_time(datetime, + /* xgettext:no-c-format */ /* Date + time in 24h format (w/o seconds) */ _("%x, %H∶%M"), + /* xgettext:no-c-format */ /* Date + time in 12h format (w/o seconds)*/ _("%x, %l∶%M %p")); + } else if (timespan > 7 * TimeSpan.DAY) { + return format_time(datetime, + /* xgettext:no-c-format */ /* Month, day and time in 24h format (w/o seconds) */ _("%b %d, %H∶%M"), + /* xgettext:no-c-format */ /* Month, day and time in 12h format (w/o seconds) */ _("%b %d, %l∶%M %p")); + } else if (datetime.get_day_of_month() != now.get_day_of_month()) { + return format_time(datetime, + /* xgettext:no-c-format */ /* Day of week and time in 24h format (w/o seconds) */ _("%a, %H∶%M"), + /* xgettext:no-c-format */ /* Day of week and time in 12h format (w/o seconds) */_("%a, %l∶%M %p")); + } else if (timespan > 9 * TimeSpan.MINUTE) { + return format_time(datetime, + /* xgettext:no-c-format */ /* Time in 24h format (w/o seconds) */ _("%H∶%M"), + /* xgettext:no-c-format */ /* Time in 12h format (w/o seconds) */ _("%l∶%M %p")); + } else if (timespan > TimeSpan.MINUTE) { + ulong mins = (ulong) (timespan.abs() / TimeSpan.MINUTE); + /* xgettext:this is the beginning of a sentence. */ + return n("%i min ago", "%i mins ago", mins).printf(mins); + } else { + return _("Just now"); + } + } +} + +} diff --git a/main/src/ui/conversation_content_view/conversation_view.vala b/main/src/ui/conversation_content_view/conversation_view.vala new file mode 100644 index 00000000..6c286fc0 --- /dev/null +++ b/main/src/ui/conversation_content_view/conversation_view.vala @@ -0,0 +1,374 @@ +using Gee; +using Gtk; +using Pango; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +[GtkTemplate (ui = "/im/dino/Dino/conversation_content_view/view.ui")] +public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins.NotificationCollection { + + public Conversation? conversation { get; private set; } + + [GtkChild] public ScrolledWindow scrolled; + [GtkChild] private Revealer notification_revealer; + [GtkChild] private Box notifications; + [GtkChild] private Box main; + [GtkChild] private Stack stack; + + private StreamInteractor stream_interactor; + private Gee.TreeSet content_items = new Gee.TreeSet(compare_meta_items); + private Gee.TreeSet meta_items = new TreeSet(compare_meta_items); + private Gee.HashMap item_item_skeletons = new Gee.HashMap(); + private Gee.HashMap widgets = new Gee.HashMap(); + private Gee.List item_skeletons = new Gee.ArrayList(); + private ContentProvider content_populator; + private SubscriptionNotitication subscription_notification; + + private double? was_value; + private double? was_upper; + private double? was_page_size; + + private Mutex reloading_mutex = Mutex(); + private bool animate = false; + private bool firstLoad = true; + private bool at_current_content = true; + private bool reload_messages = true; + + public ConversationView init(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify); + scrolled.vadjustment.notify["value"].connect(on_value_notify); + + content_populator = new ContentProvider(stream_interactor); + subscription_notification = new SubscriptionNotitication(stream_interactor); + + add_meta_notification.connect(on_add_meta_notification); + remove_meta_notification.connect(on_remove_meta_notification); + + Application app = GLib.Application.get_default() as Application; + app.plugin_registry.register_conversation_addition_populator(new ChatStatePopulator(stream_interactor)); + app.plugin_registry.register_conversation_addition_populator(new DateSeparatorPopulator(stream_interactor)); + + Timeout.add_seconds(60, () => { + foreach (ConversationItemSkeleton item_skeleton in item_skeletons) { + item_skeleton.update_time(); + } + return true; + }); + return this; + } + + public void initialize_for_conversation(Conversation? conversation) { + // Workaround for rendering issues + if (firstLoad) { + main.visible = false; + Idle.add(() => { + main.visible=true; + return false; + }); + 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); + foreach (ContentMetaItem item in before_items) { + do_insert_item(item); + } + ContentMetaItem meta_item = content_populator.get_content_meta_item(content_item); + meta_item.can_merge = false; + Widget w = insert_new(meta_item); + content_items.add(meta_item); + meta_items.add(meta_item); + + Gee.List after_items = content_populator.populate_after(conversation, content_item, 40); + foreach (ContentMetaItem item in after_items) { + do_insert_item(item); + } + if (after_items.size == 40) { + at_current_content = false; + } + + // Compute where to jump to for centered message, jump, highlight. + reload_messages = false; + Timeout.add(700, () => { + int h = 0, i = 0; + bool @break = false; + main.@foreach((widget) => { + if (widget == w || @break) { + @break = true; + return; + } + h += widget.get_allocated_height(); + i++; + }); + scrolled.vadjustment.value = h - scrolled.vadjustment.page_size * 1/3; + w.get_style_context().add_class("highlight-once"); + reload_messages = true; + stack.set_visible_child_name("main"); + return false; + }); + } + + private void initialize_for_conversation_(Conversation? conversation) { + // Deinitialize old conversation + Dino.Application app = Dino.Application.get_default(); + if (this.conversation != null) { + foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) { + populator.close(conversation); + } + foreach (Plugins.NotificationPopulator populator in app.plugin_registry.notification_populators) { + populator.close(conversation); + } + } + + // Clear data structures + clear_notifications(); + this.conversation = conversation; + + // Init for new conversation + foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) { + populator.init(conversation, this, Plugins.WidgetType.GTK); + } + content_populator.init(this, conversation, Plugins.WidgetType.GTK); + subscription_notification.init(conversation, this); + + animate = false; + Timeout.add(20, () => { animate = true; return false; }); + } + + private void display_latest() { + Gee.List items = content_populator.populate_latest(conversation, 40); + foreach (ContentMetaItem item in items) { + do_insert_item(item); + } + Application app = GLib.Application.get_default() as Application; + foreach (Plugins.NotificationPopulator populator in app.plugin_registry.notification_populators) { + populator.init(conversation, this, Plugins.WidgetType.GTK); + } + Idle.add(() => { on_value_notify(); return false; }); + } + + public void insert_item(Plugins.MetaConversationItem item) { + if (meta_items.size > 0) { + bool after_last = meta_items.last().sort_time.compare(item.sort_time) <= 0; + bool within_range = meta_items.last().sort_time.compare(item.sort_time) > 0 && meta_items.first().sort_time.compare(item.sort_time) < 0; + bool accept = within_range || (at_current_content && after_last); + if (!accept) { + return; + } + } + do_insert_item(item); + } + + public void do_insert_item(Plugins.MetaConversationItem item) { + lock (meta_items) { + insert_new(item); + if (item as ContentMetaItem != null) { + content_items.add(item); + } + meta_items.add(item); + } + + inserted_item(item); + } + + private void remove_item(Plugins.MetaConversationItem item) { + ConversationItemSkeleton? skeleton = item_item_skeletons[item]; + if (skeleton != null) { + widgets[item].destroy(); + widgets.unset(item); + skeleton.destroy(); + item_skeletons.remove(skeleton); + item_item_skeletons.unset(item); + + content_items.remove(item); + meta_items.remove(item); + } + + removed_item(item); + } + + public void on_add_meta_notification(Plugins.MetaConversationNotification notification) { + Widget? widget = (Widget) notification.get_widget(Plugins.WidgetType.GTK); + if (widget != null) { + add_notification(widget); + } + } + + public void on_remove_meta_notification(Plugins.MetaConversationNotification notification){ + Widget? widget = (Widget) notification.get_widget(Plugins.WidgetType.GTK); + if (widget != null) { + remove_notification(widget); + } + } + + public void add_notification(Widget widget) { + notifications.add(widget); + Timeout.add(20, () => { + notification_revealer.transition_duration = 200; + notification_revealer.reveal_child = true; + return false; + }); + } + + public void remove_notification(Widget widget) { + notification_revealer.reveal_child = false; + widget.destroy(); + } + + private Widget insert_new(Plugins.MetaConversationItem item) { + Plugins.MetaConversationItem? lower_item = meta_items.lower(item); + + // Fill datastructure + ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation, item) { visible=true }; + item_item_skeletons[item] = item_skeleton; + int index = lower_item != null ? item_skeletons.index_of(item_item_skeletons[lower_item]) + 1 : 0; + item_skeletons.insert(index, item_skeleton); + + // Insert widget + Widget insert = item_skeleton; + if (animate) { + Revealer revealer = new Revealer() {transition_duration = 200, transition_type = RevealerTransitionType.SLIDE_UP, visible = true}; + revealer.add(item_skeleton); + insert = revealer; + main.add(insert); + revealer.reveal_child = true; + } else { + main.add(insert); + } + widgets[item] = insert; + main.reorder_child(insert, index); + + if (lower_item != null) { + if (can_merge(item, lower_item)) { + ConversationItemSkeleton lower_skeleton = item_item_skeletons[lower_item]; + item_skeleton.show_skeleton = false; + lower_skeleton.last_group_item = false; + } + } + + Plugins.MetaConversationItem? upper_item = meta_items.higher(item); + if (upper_item != null) { + if (!can_merge(upper_item, item)) { + ConversationItemSkeleton upper_skeleton = item_item_skeletons[upper_item]; + upper_skeleton.show_skeleton = true; + } + } + + // If an item from the past was added, add everything between that item and the (post-)first present item + if (index == 0) { + Dino.Application app = Dino.Application.get_default(); + if (item_skeletons.size == 1) { + foreach (Plugins.ConversationAdditionPopulator populator in app.plugin_registry.conversation_addition_populators) { + populator.populate_timespan(conversation, item.sort_time, new DateTime.now_utc()); + } + } else { + foreach (Plugins.ConversationAdditionPopulator populator in app.plugin_registry.conversation_addition_populators) { + populator.populate_timespan(conversation, item.sort_time, meta_items.higher(item).sort_time); + } + } + } + return insert; + } + + private bool can_merge(Plugins.MetaConversationItem upper_item /*more recent, displayed below*/, Plugins.MetaConversationItem lower_item /*less recent, displayed above*/) { + return upper_item.display_time != null && lower_item.display_time != null && + upper_item.display_time.difference(lower_item.display_time) < TimeSpan.MINUTE && + upper_item.jid.equals(lower_item.jid) && + upper_item.encryption == lower_item.encryption && + (upper_item.mark == Message.Marked.WONTSEND) == (lower_item.mark == Message.Marked.WONTSEND); + } + + private void on_upper_notify() { + if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size + if (at_current_content) { + scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down + } + } else if (scrolled.vadjustment.value < scrolled.vadjustment.upper - scrolled.vadjustment.page_size - 1) { + scrolled.vadjustment.value = scrolled.vadjustment.upper - was_upper + scrolled.vadjustment.value; // stay at same content + } + was_upper = scrolled.vadjustment.upper; + was_page_size = scrolled.vadjustment.page_size; + was_value = scrolled.vadjustment.value; + reloading_mutex.trylock(); + reloading_mutex.unlock(); + } + + private void on_value_notify() { + if (scrolled.vadjustment.value < 400) { + load_earlier_messages(); + } else if (scrolled.vadjustment.upper - (scrolled.vadjustment.value + scrolled.vadjustment.page_size) < 400) { + load_later_messages(); + } + } + + private void load_earlier_messages() { + was_value = scrolled.vadjustment.value; + if (!reloading_mutex.trylock()) return; + if (meta_items.size > 0) { + Gee.List items = content_populator.populate_before(conversation, (content_items.first() as ContentMetaItem).content_item, 20); + foreach (ContentMetaItem item in items) { + do_insert_item(item); + } + } else { + reloading_mutex.unlock(); + } + } + + private void load_later_messages() { + if (!reloading_mutex.trylock()) return; + if (meta_items.size > 0 && !at_current_content) { + Gee.List items = content_populator.populate_after(conversation, (content_items.last() as ContentMetaItem).content_item, 20); + if (items.size == 0) { + at_current_content = true; + } + foreach (ContentMetaItem item in items) { + do_insert_item(item); + } + } else { + reloading_mutex.unlock(); + } + } + + private static int compare_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) { + int cmp1 = a.sort_time.compare(b.sort_time); + if (cmp1 == 0) { + double cmp2 = a.seccondary_sort_indicator - b.seccondary_sort_indicator; + if (cmp2 == 0) { + return (int) (a.tertiary_sort_indicator - b.tertiary_sort_indicator); + } + return (int) cmp2; + } + return cmp1; + } + + private void clear() { + was_upper = null; + was_page_size = null; + content_items.clear(); + meta_items.clear(); + item_skeletons.clear(); + item_item_skeletons.clear(); + widgets.clear(); + main.@foreach((widget) => { widget.destroy(); }); + } + + private void clear_notifications() { + notifications.@foreach((widget) => { widget.destroy(); }); + notification_revealer.transition_duration = 0; + notification_revealer.set_reveal_child(false); + } +} + +} diff --git a/main/src/ui/conversation_content_view/date_separator_populator.vala b/main/src/ui/conversation_content_view/date_separator_populator.vala new file mode 100644 index 00000000..3ddb0d9a --- /dev/null +++ b/main/src/ui/conversation_content_view/date_separator_populator.vala @@ -0,0 +1,105 @@ +using Gee; +using Gtk; + +using Dino.Entities; +using Xmpp; + +namespace Dino.Ui.ConversationSummary { + +class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Plugins.ConversationAdditionPopulator, Object { + + public string id { get { return "date_separator"; } } + + private StreamInteractor stream_interactor; + private Conversation? current_conversation; + private Plugins.ConversationItemCollection? item_collection; + private Gee.TreeSet insert_times; + + + public DateSeparatorPopulator(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection, Plugins.WidgetType type) { + current_conversation = conversation; + this.item_collection = item_collection; + item_collection.inserted_item.connect(on_inserted_item); + this.insert_times = new TreeSet((a, b) => { + return a.compare(b); + }); + } + + public void close(Conversation conversation) { + item_collection.inserted_item.disconnect(on_inserted_item); + } + + public void populate_timespan(Conversation conversation, DateTime after, DateTime before) { } + + private void on_inserted_item(Plugins.MetaConversationItem item) { + if (!(item is ContentMetaItem)) return; + + DateTime time = item.sort_time.to_local(); + DateTime msg_date = new DateTime.local(time.get_year(), time.get_month(), time.get_day_of_month(), 0, 0, 0); + if (!insert_times.contains(msg_date)) { + if (insert_times.lower(msg_date) != null) { + item_collection.insert_item(new MetaDateItem(msg_date.to_utc())); + } else if (insert_times.size > 0) { + item_collection.insert_item(new MetaDateItem(insert_times.first().to_utc())); + } + insert_times.add(msg_date); + } + } +} + +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) { + this.date = date; + this.sort_time = date; + } + + public override Object? get_widget(Plugins.WidgetType widget_type) { + Box box = new Box(Orientation.HORIZONTAL, 10) { width_request=300, halign=Align.CENTER, visible=true }; + box.add(new Separator(Orientation.HORIZONTAL) { valign=Align.CENTER, hexpand=true, visible=true }); + string date_str = get_relative_time(date); + Label label = new Label(@"$date_str") { use_markup=true, halign=Align.CENTER, hexpand=false, visible=true }; + label.get_style_context().add_class("dim-label"); + box.add(label); + box.add(new Separator(Orientation.HORIZONTAL) { valign=Align.CENTER, hexpand=true, visible=true }); + return box; + } + + private static string get_relative_time(DateTime time) { + DateTime time_local = time.to_local(); + DateTime now_local = new DateTime.now_local(); + if (time_local.get_year() == now_local.get_year() && + time_local.get_month() == now_local.get_month() && + time_local.get_day_of_month() == now_local.get_day_of_month()) { + return _("Today"); + } + DateTime now_local_minus = now_local.add_days(-1); + if (time_local.get_year() == now_local_minus.get_year() && + time_local.get_month() == now_local_minus.get_month() && + time_local.get_day_of_month() == now_local_minus.get_day_of_month()) { + return _("Yesterday"); + } + if (time_local.get_year() != now_local.get_year()) { + return time_local.format("%x"); + } + TimeSpan timespan = now_local.difference(time_local); + if (timespan < 7 * TimeSpan.DAY) { + return time_local.format(_("%a, %b %d")); + } else { + return time_local.format(_("%b %d")); + } + } +} + +} diff --git a/main/src/ui/conversation_content_view/file_widget.vala b/main/src/ui/conversation_content_view/file_widget.vala new file mode 100644 index 00000000..dd28b385 --- /dev/null +++ b/main/src/ui/conversation_content_view/file_widget.vala @@ -0,0 +1,338 @@ +using Gee; +using Gdk; +using Gtk; +using Pango; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +public class FileWidget : Box { + + enum State { + IMAGE, + DEFAULT + } + + private const int MAX_HEIGHT = 300; + private const int MAX_WIDTH = 600; + + private StreamInteractor stream_interactor; + private FileTransfer file_transfer; + private State state; + + // default box + private Box main_box; + private Image content_type_image; + private Image download_image; + private Spinner spinner; + private Label mime_label; + private Stack image_stack; + + private Widget content; + + private bool pointer_inside = false; + + public FileWidget(StreamInteractor stream_interactor, FileTransfer file_transfer) { + this.stream_interactor = stream_interactor; + this.file_transfer = file_transfer; + + load_widget.begin(); + } + + private async void load_widget() { + if (show_image()) { + content = yield get_image_widget(file_transfer); + if (content != null) { + this.state = State.IMAGE; + this.add(content); + return; + } + } + content = get_default_widget(file_transfer); + this.state = State.DEFAULT; + this.add(content); + } + + private async Widget? get_image_widget(FileTransfer file_transfer) { + // Load and prepare image in tread + Thread thread = new Thread (null, () => { + Image image = new Image() { halign=Align.START, visible = true }; + + Gdk.Pixbuf pixbuf; + try { + pixbuf = new Gdk.Pixbuf.from_file(file_transfer.get_file().get_path()); + } catch (Error error) { + warning("Can't load picture %s - %s", file_transfer.get_file().get_path(), error.message); + Idle.add(get_image_widget.callback); + return null; + } + + pixbuf = pixbuf.apply_embedded_orientation(); + + int max_scaled_height = MAX_HEIGHT * image.scale_factor; + if (pixbuf.height > max_scaled_height) { + pixbuf = pixbuf.scale_simple((int) ((double) max_scaled_height / pixbuf.height * pixbuf.width), max_scaled_height, Gdk.InterpType.BILINEAR); + } + int max_scaled_width = MAX_WIDTH * image.scale_factor; + if (pixbuf.width > max_scaled_width) { + pixbuf = pixbuf.scale_simple(max_scaled_width, (int) ((double) max_scaled_width / pixbuf.width * pixbuf.height), Gdk.InterpType.BILINEAR); + } + pixbuf = crop_corners(pixbuf, 3 * image.get_scale_factor()); + Util.image_set_from_scaled_pixbuf(image, pixbuf); + + Idle.add(get_image_widget.callback); + return image; + }); + yield; + Image image = thread.join(); + if (image == null) return null; + + Util.force_css(image, "* { box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.1); margin: 2px; border-radius: 3px; }"); + + Builder builder = new Builder.from_resource("/im/dino/Dino/conversation_content_view/image_toolbar.ui"); + Widget toolbar = builder.get_object("main") as Widget; + Util.force_background(toolbar, "rgba(0, 0, 0, 0.5)"); + Util.force_css(toolbar, "* { padding: 3px; border-radius: 3px; }"); + + Label url_label = builder.get_object("url_label") as Label; + Util.force_color(url_label, "#eee"); + + if (file_transfer.file_name != null && file_transfer.file_name != "") { + string caption = file_transfer.file_name; + url_label.label = caption; + } else { + url_label.visible = false; + } + + Image open_image = builder.get_object("open_image") as Image; + Util.force_css(open_image, "*:not(:hover) { color: #eee; }"); + Button open_button = builder.get_object("open_button") as Button; + Util.force_css(open_button, "*:hover { background-color: rgba(255,255,255,0.3); border-color: transparent; }"); + open_button.clicked.connect(() => { + try{ + AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null); + } catch (Error err) { + info("Could not to open file://%s: %s", file_transfer.get_file().get_path(), err.message); + } + }); + + Revealer toolbar_revealer = new Revealer() { transition_type=RevealerTransitionType.CROSSFADE, transition_duration=400, visible=true }; + toolbar_revealer.add(toolbar); + + Grid grid = new Grid() { visible=true }; + grid.attach(toolbar_revealer, 0, 0, 1, 1); + grid.attach(image, 0, 0, 1, 1); + + EventBox event_box = new EventBox() { margin_top=5, halign=Align.START, visible=true }; + event_box.events = EventMask.POINTER_MOTION_MASK; + event_box.add(grid); + event_box.enter_notify_event.connect(() => { toolbar_revealer.reveal_child = true; return false; }); + event_box.leave_notify_event.connect(() => { toolbar_revealer.reveal_child = false; return false; }); + + return event_box; + } + + private static Gdk.Pixbuf crop_corners(Gdk.Pixbuf pixbuf, double radius = 3) { + Cairo.Context ctx = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.ARGB32, pixbuf.width, pixbuf.height)); + Gdk.cairo_set_source_pixbuf(ctx, pixbuf, 0, 0); + double degrees = Math.PI / 180.0; + ctx.new_sub_path(); + ctx.arc(pixbuf.width - radius, radius, radius, -90 * degrees, 0 * degrees); + ctx.arc(pixbuf.width - radius, pixbuf.height - radius, radius, 0 * degrees, 90 * degrees); + ctx.arc(radius, pixbuf.height - radius, radius, 90 * degrees, 180 * degrees); + ctx.arc(radius, radius, radius, 180 * degrees, 270 * degrees); + ctx.close_path(); + ctx.clip(); + ctx.paint(); + return Gdk.pixbuf_get_from_surface(ctx.get_target(), 0, 0, pixbuf.width, pixbuf.height); + } + + private Widget get_default_widget(FileTransfer file_transfer) { + string icon_name = get_file_icon_name(file_transfer.mime_type); + + main_box = new Box(Orientation.HORIZONTAL, 10) { halign=Align.FILL, hexpand=true, visible=true }; + content_type_image = new Image.from_icon_name(icon_name, IconSize.DND) { opacity=0.5, visible=true }; + download_image = new Image.from_icon_name("dino-file-download-symbolic", IconSize.DND) { opacity=0.7, visible=true }; + spinner = new Spinner() { visible=true }; + + EventBox stack_event_box = new EventBox() { visible=true }; + image_stack = new Stack() { transition_type = StackTransitionType.CROSSFADE, transition_duration=50, valign=Align.CENTER, visible=true }; + image_stack.add_named(download_image, "download_image"); + image_stack.add_named(spinner, "spinner"); + image_stack.add_named(content_type_image, "content_type_image"); + stack_event_box.add(image_stack); + + main_box.add(stack_event_box); + + Box right_box = new Box(Orientation.VERTICAL, 0) { hexpand=true, visible=true }; + Label name_label = new Label(file_transfer.file_name) { ellipsize=EllipsizeMode.MIDDLE, max_width_chars=1, hexpand=true, xalign=0, yalign=0, visible=true}; + right_box.add(name_label); + + EventBox mime_label_event_box = new EventBox() { visible=true }; + mime_label = new Label("") { use_markup=true, xalign=0, yalign=1, visible=true}; + + mime_label_event_box.add(mime_label); + mime_label.get_style_context().add_class("dim-label"); + + right_box.add(mime_label_event_box); + main_box.add(right_box); + + EventBox event_box = new EventBox() { margin_top=5, width_request=500, halign=Align.START, visible=true }; + event_box.get_style_context().add_class("file-box-outer"); + event_box.add(main_box); + main_box.get_style_context().add_class("file-box"); + + event_box.enter_notify_event.connect((event) => { + pointer_inside = true; + Timeout.add(20, () => { + if (pointer_inside) { + event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.HAND2)); + content_type_image.opacity = 0.7; + if (file_transfer.state == FileTransfer.State.NOT_STARTED) { + image_stack.set_visible_child_name("download_image"); + } + } + return false; + }); + return false; + }); + stack_event_box.enter_notify_event.connect((event) => { pointer_inside = true; return false; }); + mime_label_event_box.enter_notify_event.connect((event) => { pointer_inside = true; return false; }); + mime_label.enter_notify_event.connect((event) => { pointer_inside = true; return false; }); + event_box.leave_notify_event.connect((event) => { + pointer_inside = false; + Timeout.add(20, () => { + if (!pointer_inside) { + event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.XTERM)); + content_type_image.opacity = 0.5; + if (file_transfer.state == FileTransfer.State.NOT_STARTED) { + image_stack.set_visible_child_name("content_type_image"); + } + } + return false; + }); + return false; + }); + stack_event_box.leave_notify_event.connect((event) => { pointer_inside = true; return false; }); + mime_label_event_box.leave_notify_event.connect((event) => { pointer_inside = true; return false; }); + mime_label.leave_notify_event.connect((event) => { pointer_inside = true; return false; }); + event_box.button_release_event.connect((event_button) => { + switch (file_transfer.state) { + case FileTransfer.State.COMPLETE: + if (event_button.button == 1) { + try{ + AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null); + } catch (Error err) { + print("Tried to open " + file_transfer.get_file().get_path()); + } + } + break; + case FileTransfer.State.NOT_STARTED: + stream_interactor.get_module(FileManager.IDENTITY).download_file.begin(file_transfer); + break; + } + return false; + }); + + main_box.events = EventMask.POINTER_MOTION_MASK; + content_type_image.events = EventMask.POINTER_MOTION_MASK; + download_image.events = EventMask.POINTER_MOTION_MASK; + spinner.events = EventMask.POINTER_MOTION_MASK; + image_stack.events = EventMask.POINTER_MOTION_MASK; + right_box.events = EventMask.POINTER_MOTION_MASK; + name_label.events = EventMask.POINTER_MOTION_MASK; + mime_label.events = EventMask.POINTER_MOTION_MASK; + event_box.events = EventMask.POINTER_MOTION_MASK; + mime_label.events = EventMask.POINTER_MOTION_MASK; + mime_label_event_box.events = EventMask.POINTER_MOTION_MASK; + + file_transfer.notify["path"].connect(update_file_info); + file_transfer.notify["state"].connect(update_file_info); + file_transfer.notify["mime-type"].connect(update_file_info); + update_file_info.begin(); + + return event_box; + } + + private async void update_file_info() { + if (file_transfer.state == FileTransfer.State.COMPLETE && show_image() && state != State.IMAGE) { + this.remove(content); + this.add(yield get_image_widget(file_transfer)); + state = State.IMAGE; + } + + spinner.active = false; // A hidden spinning spinner still uses CPU. Deactivate asap + + string? mime_description = file_transfer.mime_type != null ? ContentType.get_description(file_transfer.mime_type) : null; + + switch (file_transfer.state) { + case FileTransfer.State.COMPLETE: + mime_label.label = "" + mime_description + ""; + image_stack.set_visible_child_name("content_type_image"); + break; + case FileTransfer.State.IN_PROGRESS: + mime_label.label = "" + _("Downloading %s…").printf(get_size_string(file_transfer.size)) + ""; + spinner.active = true; + image_stack.set_visible_child_name("spinner"); + break; + case FileTransfer.State.NOT_STARTED: + if (mime_description != null) { + mime_label.label = "" + _("%s offered: %s").printf(mime_description, get_size_string(file_transfer.size)) + ""; + } else if (file_transfer.size != -1) { + mime_label.label = "" + _("File offered: %s").printf(get_size_string(file_transfer.size)) + ""; + } else { + mime_label.label = "" + _("File offered") + ""; + } + image_stack.set_visible_child_name("content_type_image"); + break; + case FileTransfer.State.FAILED: + mime_label.label = "" + _("File transfer failed") + ""; + image_stack.set_visible_child_name("content_type_image"); + break; + } + } + + private static string get_file_icon_name(string? mime_type) { + if (mime_type == null) return "dino-file-symbolic"; + + string generic_icon_name = ContentType.get_generic_icon_name(mime_type) ?? ""; + switch (generic_icon_name) { + case "audio-x-generic": return "dino-file-music-symbolic"; + case "image-x-generic": return "dino-file-image-symbolic"; + case "text-x-generic": return "dino-file-document-symbolic"; + case "text-x-generic-template": return "dino-file-document-symbolic"; + case "video-x-generic": return "dino-file-video-symbolic"; + case "x-office-document": return "dino-file-document-symbolic"; + case "x-office-spreadsheet": return "dino-file-table-symbolic"; + default: return "dino-file-symbolic"; + } + } + + private static string get_size_string(int size) { + if (size < 1024) { + return @"$(size) B"; + } else if (size < 1000 * 1000) { + return @"$(size / 1000) kB"; + } else if (size < 1000 * 1000 * 1000) { + return @"$(size / 1000 / 1000) MB"; + } else { + return @"$(size / 1000 / 1000 / 1000) GB"; + } + } + + private bool show_image() { + if (file_transfer.mime_type == null || file_transfer.state != FileTransfer.State.COMPLETE) return false; + + foreach (PixbufFormat pixbuf_format in Pixbuf.get_formats()) { + foreach (string mime_type in pixbuf_format.get_mime_types()) { + if (mime_type == file_transfer.mime_type) { + return true; + } + } + } + return false; + } +} + +} diff --git a/main/src/ui/conversation_content_view/message_item.vala b/main/src/ui/conversation_content_view/message_item.vala new file mode 100644 index 00000000..e69de29b diff --git a/main/src/ui/conversation_content_view/subscription_notification.vala b/main/src/ui/conversation_content_view/subscription_notification.vala new file mode 100644 index 00000000..d493ff78 --- /dev/null +++ b/main/src/ui/conversation_content_view/subscription_notification.vala @@ -0,0 +1,55 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +public class SubscriptionNotitication : Object { + + private StreamInteractor stream_interactor; + private Conversation conversation; + private ConversationView conversation_view; + + public SubscriptionNotitication(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + stream_interactor.get_module(PresenceManager.IDENTITY).received_subscription_request.connect((jid, account) => { + Conversation relevant_conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(jid, account, Conversation.Type.CHAT); + stream_interactor.get_module(ConversationManager.IDENTITY).start_conversation(relevant_conversation); + if (conversation != null && account.equals(conversation.account) && jid.equals(conversation.counterpart)) { + show_notification(); + } + }); + } + + public void init(Conversation conversation, ConversationView conversation_view) { + this.conversation = conversation; + this.conversation_view = conversation_view; + + if (stream_interactor.get_module(PresenceManager.IDENTITY).exists_subscription_request(conversation.account, conversation.counterpart)) { + show_notification(); + } + } + + private void show_notification() { + Box box = new Box(Orientation.HORIZONTAL, 5) { visible=true }; + Button accept_button = new Button() { label=_("Accept"), visible=true }; + Button deny_button = new Button() { label=_("Deny"), visible=true }; + GLib.Application app = GLib.Application.get_default(); + accept_button.clicked.connect(() => { + app.activate_action("accept-subscription", conversation.id); + conversation_view.remove_notification(box); + }); + deny_button.clicked.connect(() => { + app.activate_action("deny-subscription", conversation.id); + conversation_view.remove_notification(box); + }); + box.add(new Label(_("This contact would like to add you to their contact list")) { margin_end=10, visible=true }); + box.add(accept_button); + box.add(deny_button); + conversation_view.add_notification(box); + } +} + +} diff --git a/main/src/ui/conversation_summary/chat_state_populator.vala b/main/src/ui/conversation_summary/chat_state_populator.vala deleted file mode 100644 index 54b41b7d..00000000 --- a/main/src/ui/conversation_summary/chat_state_populator.vala +++ /dev/null @@ -1,126 +0,0 @@ -using Gee; -using Gtk; - -using Dino.Entities; -using Xmpp; - -namespace Dino.Ui.ConversationSummary { - -class ChatStatePopulator : Plugins.ConversationItemPopulator, Plugins.ConversationAdditionPopulator, Object { - - public string id { get { return "chat_state"; } } - - private StreamInteractor? stream_interactor; - private Conversation? current_conversation; - private Plugins.ConversationItemCollection? item_collection; - - private MetaChatStateItem? meta_item; - - public ChatStatePopulator(StreamInteractor stream_interactor) { - this.stream_interactor = stream_interactor; - - stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).received_state.connect((conversation, state) => { - if (current_conversation != null && current_conversation.equals(conversation)) { - update_chat_state(); - } - }); - stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect((message, conversation) => { - if (conversation.equals(current_conversation)) { - update_chat_state(); - } - }); - } - - public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection, Plugins.WidgetType type) { - current_conversation = conversation; - this.item_collection = item_collection; - this.meta_item = null; - - update_chat_state(); - } - - public void close(Conversation conversation) { } - - public void populate_timespan(Conversation conversation, DateTime from, DateTime to) { } - - private void update_chat_state() { - Gee.List? typing_jids = stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).get_typing_jids(current_conversation); - - if (meta_item != null && typing_jids == null) { - // Remove state (stoped typing) - item_collection.remove_item(meta_item); - meta_item = null; - } else if (meta_item != null && typing_jids != null) { - // Update state (other people typing in MUC) - meta_item.set_new(typing_jids); - } else if (typing_jids != null) { - // New state (started typing) - meta_item = new MetaChatStateItem(stream_interactor, current_conversation, typing_jids); - item_collection.insert_item(meta_item); - } - } -} - -private class MetaChatStateItem : Plugins.MetaConversationItem { - public override bool dim { get; set; default=true; } - 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(); - private Label label; - private AvatarImage image; - - public MetaChatStateItem(StreamInteractor stream_interactor, Conversation conversation, Gee.List jids) { - this.stream_interactor = stream_interactor; - this.conversation = conversation; - this.jids = jids; - } - - public override Object? get_widget(Plugins.WidgetType widget_type) { - label = new Label("") { xalign=0, vexpand=true, visible=true }; - label.get_style_context().add_class("dim-label"); - image = new AvatarImage() { margin_top=2, valign=Align.START, visible=true }; - - Box image_content_box = new Box(Orientation.HORIZONTAL, 8) { visible=true }; - image_content_box.add(image); - image_content_box.add(label); - - update(); - return image_content_box; - } - - public void set_new(Gee.List jids) { - this.jids = jids; - update(); - } - - private void update() { - if (image == null || label == null) return; - - image.set_conversation_participants(stream_interactor, conversation, jids.to_array()); - - Gee.List display_names = new ArrayList(); - foreach (Jid jid in jids) { - display_names.add(Util.get_participant_display_name(stream_interactor, conversation, jid)); - } - string new_text = ""; - if (jids.size > 3) { - new_text = _("%s, %s and %i others").printf(display_names[0], display_names[1], jids.size - 2); - } else if (jids.size == 3) { - new_text = _("%s, %s and %s are typing…").printf(display_names[0], display_names[1], display_names[2]); - } else if (jids.size == 2) { - new_text =_("%s and %s are typing…").printf(display_names[0], display_names[1]); - } else { - new_text = "%s is typing…".printf(display_names[0]); - } - - label.label = new_text; - } -} - -} diff --git a/main/src/ui/conversation_summary/content_item_widget_factory.vala b/main/src/ui/conversation_summary/content_item_widget_factory.vala deleted file mode 100644 index 54283e75..00000000 --- a/main/src/ui/conversation_summary/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=FileItem.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_summary/content_populator.vala b/main/src/ui/conversation_summary/content_populator.vala deleted file mode 100644 index e8eee06c..00000000 --- a/main/src/ui/conversation_summary/content_populator.vala +++ /dev/null @@ -1,111 +0,0 @@ -using Gee; -using Gtk; - -using Xmpp; -using Dino.Entities; - -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) { - if (current_conversation != null) { - stream_interactor.get_module(ContentItemStore.IDENTITY).uninit(current_conversation, this); - } - current_conversation = conversation; - this.item_collection = item_collection; - stream_interactor.get_module(ContentItemStore.IDENTITY).init(conversation, this); - } - - public void insert_item(ContentItem item) { - item_collection.insert_item(new ContentMetaItem(item, widget_factory)); - } - - public void remove_item(ContentItem item) { } - - - public Gee.List populate_latest(Conversation conversation, int n) { - 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)); - } - return ret; - } - - public Gee.List populate_before(Conversation conversation, ContentItem before_item, int n) { - 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)); - } - return ret; - } - - public Gee.List populate_after(Conversation conversation, ContentItem after_item, int n) { - 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)); - } - return ret; - } - - public ContentMetaItem get_content_meta_item(ContentItem content_item) { - return new ContentMetaItem(content_item, widget_factory); - } -} - -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 ContentItem content_item; - private ContentItemWidgetFactory widget_factory; - - public ContentMetaItem(ContentItem content_item, ContentItemWidgetFactory widget_factory) { - this.jid = content_item.jid; - this.sort_time = content_item.sort_time; - this.seccondary_sort_indicator = (long) content_item.display_time.to_unix(); - this.tertiary_sort_indicator = content_item.id; - this.display_time = content_item.display_time; - this.encryption = content_item.encryption; - this.mark = content_item.mark; - - WeakRef weak_item = WeakRef(content_item); - content_item.notify["mark"].connect(() => { - ContentItem? ci = weak_item.get() as ContentItem; - if (ci == null) return; - this.mark = ci.mark; - }); - - this.can_merge = true; - this.requires_avatar = true; - 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_summary/conversation_item_skeleton.vala b/main/src/ui/conversation_summary/conversation_item_skeleton.vala deleted file mode 100644 index 40816493..00000000 --- a/main/src/ui/conversation_summary/conversation_item_skeleton.vala +++ /dev/null @@ -1,221 +0,0 @@ -using Gee; -using Gdk; -using Gtk; -using Markup; - -using Dino.Entities; - -namespace Dino.Ui.ConversationSummary { - -public class ConversationItemSkeleton : EventBox { - - private AvatarImage image = new AvatarImage() { margin_top=2, valign=Align.START, visible=true, allow_gray = false }; - - public bool show_skeleton { get; set; } - public bool last_group_item { get; set; } - - public StreamInteractor stream_interactor; - public Conversation conversation { get; set; } - public Plugins.MetaConversationItem item; - - private Box image_content_box = new Box(Orientation.HORIZONTAL, 8) { visible=true }; - private Box header_content_box = new Box(Orientation.VERTICAL, 0) { visible=true }; - private ItemMetaDataHeader metadata_header; - - public ConversationItemSkeleton(StreamInteractor stream_interactor, Conversation conversation, Plugins.MetaConversationItem item) { - this.stream_interactor = stream_interactor; - this.conversation = conversation; - this.item = item; - this.get_style_context().add_class("message-box"); - - if (item.requires_avatar) { - image.set_conversation_participant(stream_interactor, conversation, item.jid); - image_content_box.add(image); - } - if (item.requires_header) { - metadata_header = new ItemMetaDataHeader(stream_interactor, conversation, item) { visible=true }; - header_content_box.add(metadata_header); - } - - Widget? widget = item.get_widget(Plugins.WidgetType.GTK) as Widget; - if (widget != null) { - widget.valign = Align.END; - header_content_box.add(widget); - } - - image_content_box.add(header_content_box); - this.add(image_content_box); - - if (item.get_type().is_a(typeof(ContentMetaItem))) { - this.motion_notify_event.connect((event) => { - this.set_state_flags(StateFlags.PRELIGHT, false); - return false; - }); - this.enter_notify_event.connect((event) => { - this.set_state_flags(StateFlags.PRELIGHT, false); - return false; - }); - this.leave_notify_event.connect((event) => { - this.unset_state_flags(StateFlags.PRELIGHT); - return false; - }); - } - - this.notify["show-skeleton"].connect(update_margin); - this.notify["last-group-item"].connect(update_margin); - - this.show_skeleton = true; - this.last_group_item = true; - update_margin(); - this.notify["show-skeleton"].connect(update_margin); - } - - public void update_time() { - if (metadata_header != null) { - metadata_header.update_time(); - } - } - - public void update_margin() { - image.visible = this.show_skeleton; - if (metadata_header != null) { - metadata_header.visible = this.show_skeleton; - } - image_content_box.margin_start = this.show_skeleton ? 15 : 58; - image_content_box.margin_end = 15; - - if (this.show_skeleton && this.last_group_item) { - image_content_box.margin_top = 8; - image_content_box.margin_bottom = 8; - } else { - image_content_box.margin_top = 4; - image_content_box.margin_bottom = 4; - } - } -} - -[GtkTemplate (ui = "/im/dino/Dino/conversation_summary/item_metadata_header.ui")] -public class ItemMetaDataHeader : Box { - [GtkChild] public Label name_label; - [GtkChild] public Label dot_label; - [GtkChild] public Label time_label; - [GtkChild] public Image encryption_image; - [GtkChild] public Image received_image; - - public static IconSize ICON_SIZE_HEADER = Gtk.icon_size_register("im.dino.Dino.HEADER_ICON", 17, 12); - - private StreamInteractor stream_interactor; - private Conversation conversation; - private Plugins.MetaConversationItem item; - private ArrayList items = new ArrayList(); - - public ItemMetaDataHeader(StreamInteractor stream_interactor, Conversation conversation, Plugins.MetaConversationItem item) { - this.stream_interactor = stream_interactor; - this.conversation = conversation; - this.item = item; - items.add(item); - - update_name_label(); - name_label.style_updated.connect(update_name_label); - if (item.encryption != Encryption.NONE) { - encryption_image.visible = true; - encryption_image.set_from_icon_name("dino-changes-prevent-symbolic", ICON_SIZE_HEADER); - } - update_time(); - - item.notify["mark"].connect_after(update_received_mark); - update_received_mark(); - } - - public void update_time() { - if (item.display_time != null) { - time_label.label = get_relative_time(item.display_time.to_local()).to_string(); - } - } - - private void update_name_label() { - string display_name = Markup.escape_text(Util.get_participant_display_name(stream_interactor, conversation, item.jid)); - string color = Util.get_name_hex_color(stream_interactor, conversation.account, item.jid, Util.is_dark_theme(name_label)); - name_label.label = @"$display_name"; - } - - private void update_received_mark() { - bool all_received = true; - bool all_read = true; - bool all_sent = true; - foreach (Plugins.MetaConversationItem item in items) { - if (item.mark == Message.Marked.WONTSEND) { - received_image.visible = true; - received_image.set_from_icon_name("dialog-warning-symbolic", ICON_SIZE_HEADER); - Util.force_error_color(received_image); - Util.force_error_color(encryption_image); - Util.force_error_color(time_label); - string error_text = _("Unable to send message"); - received_image.tooltip_text = error_text; - encryption_image.tooltip_text = error_text; - time_label.tooltip_text = error_text; - return; - } else if (item.mark != Message.Marked.READ) { - all_read = false; - if (item.mark != Message.Marked.RECEIVED) { - all_received = false; - if (item.mark == Message.Marked.UNSENT) { - all_sent = false; - } - } - } - } - if (all_read) { - received_image.visible = true; - received_image.set_from_icon_name("dino-double-tick-symbolic", ICON_SIZE_HEADER); - } else if (all_received) { - received_image.visible = true; - received_image.set_from_icon_name("dino-tick-symbolic", ICON_SIZE_HEADER); - } else if (!all_sent) { - received_image.visible = true; - received_image.set_from_icon_name("image-loading-symbolic", ICON_SIZE_HEADER); - } else if (received_image.visible) { - received_image.set_from_icon_name("image-loading-symbolic", ICON_SIZE_HEADER); - - } - } - - public static string format_time(DateTime datetime, string format_24h, string format_12h) { - string format = Util.is_24h_format() ? format_24h : format_12h; - if (!get_charset(null)) { - // No UTF-8 support, use simple colon for time instead - format = format.replace("∶", ":"); - } - return datetime.format(format); - } - - public static string get_relative_time(DateTime datetime) { - DateTime now = new DateTime.now_local(); - TimeSpan timespan = now.difference(datetime); - if (timespan > 365 * TimeSpan.DAY) { - return format_time(datetime, - /* xgettext:no-c-format */ /* Date + time in 24h format (w/o seconds) */ _("%x, %H∶%M"), - /* xgettext:no-c-format */ /* Date + time in 12h format (w/o seconds)*/ _("%x, %l∶%M %p")); - } else if (timespan > 7 * TimeSpan.DAY) { - return format_time(datetime, - /* xgettext:no-c-format */ /* Month, day and time in 24h format (w/o seconds) */ _("%b %d, %H∶%M"), - /* xgettext:no-c-format */ /* Month, day and time in 12h format (w/o seconds) */ _("%b %d, %l∶%M %p")); - } else if (datetime.get_day_of_month() != now.get_day_of_month()) { - return format_time(datetime, - /* xgettext:no-c-format */ /* Day of week and time in 24h format (w/o seconds) */ _("%a, %H∶%M"), - /* xgettext:no-c-format */ /* Day of week and time in 12h format (w/o seconds) */_("%a, %l∶%M %p")); - } else if (timespan > 9 * TimeSpan.MINUTE) { - return format_time(datetime, - /* xgettext:no-c-format */ /* Time in 24h format (w/o seconds) */ _("%H∶%M"), - /* xgettext:no-c-format */ /* Time in 12h format (w/o seconds) */ _("%l∶%M %p")); - } else if (timespan > TimeSpan.MINUTE) { - ulong mins = (ulong) (timespan.abs() / TimeSpan.MINUTE); - /* xgettext:this is the beginning of a sentence. */ - return n("%i min ago", "%i mins ago", mins).printf(mins); - } else { - return _("Just now"); - } - } -} - -} diff --git a/main/src/ui/conversation_summary/conversation_view.vala b/main/src/ui/conversation_summary/conversation_view.vala deleted file mode 100644 index 6b3f0f8a..00000000 --- a/main/src/ui/conversation_summary/conversation_view.vala +++ /dev/null @@ -1,374 +0,0 @@ -using Gee; -using Gtk; -using Pango; - -using Dino.Entities; - -namespace Dino.Ui.ConversationSummary { - -[GtkTemplate (ui = "/im/dino/Dino/conversation_summary/view.ui")] -public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins.NotificationCollection { - - public Conversation? conversation { get; private set; } - - [GtkChild] public ScrolledWindow scrolled; - [GtkChild] private Revealer notification_revealer; - [GtkChild] private Box notifications; - [GtkChild] private Box main; - [GtkChild] private Stack stack; - - private StreamInteractor stream_interactor; - private Gee.TreeSet content_items = new Gee.TreeSet(compare_meta_items); - private Gee.TreeSet meta_items = new TreeSet(compare_meta_items); - private Gee.HashMap item_item_skeletons = new Gee.HashMap(); - private Gee.HashMap widgets = new Gee.HashMap(); - private Gee.List item_skeletons = new Gee.ArrayList(); - private ContentProvider content_populator; - private SubscriptionNotitication subscription_notification; - - private double? was_value; - private double? was_upper; - private double? was_page_size; - - private Mutex reloading_mutex = Mutex(); - private bool animate = false; - private bool firstLoad = true; - private bool at_current_content = true; - private bool reload_messages = true; - - public ConversationView init(StreamInteractor stream_interactor) { - this.stream_interactor = stream_interactor; - scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify); - scrolled.vadjustment.notify["value"].connect(on_value_notify); - - content_populator = new ContentProvider(stream_interactor); - subscription_notification = new SubscriptionNotitication(stream_interactor); - - add_meta_notification.connect(on_add_meta_notification); - remove_meta_notification.connect(on_remove_meta_notification); - - Application app = GLib.Application.get_default() as Application; - app.plugin_registry.register_conversation_addition_populator(new ChatStatePopulator(stream_interactor)); - app.plugin_registry.register_conversation_addition_populator(new DateSeparatorPopulator(stream_interactor)); - - Timeout.add_seconds(60, () => { - foreach (ConversationItemSkeleton item_skeleton in item_skeletons) { - item_skeleton.update_time(); - } - return true; - }); - return this; - } - - public void initialize_for_conversation(Conversation? conversation) { - // Workaround for rendering issues - if (firstLoad) { - main.visible = false; - Idle.add(() => { - main.visible=true; - return false; - }); - 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); - foreach (ContentMetaItem item in before_items) { - do_insert_item(item); - } - ContentMetaItem meta_item = content_populator.get_content_meta_item(content_item); - meta_item.can_merge = false; - Widget w = insert_new(meta_item); - content_items.add(meta_item); - meta_items.add(meta_item); - - Gee.List after_items = content_populator.populate_after(conversation, content_item, 40); - foreach (ContentMetaItem item in after_items) { - do_insert_item(item); - } - if (after_items.size == 40) { - at_current_content = false; - } - - // Compute where to jump to for centered message, jump, highlight. - reload_messages = false; - Timeout.add(700, () => { - int h = 0, i = 0; - bool @break = false; - main.@foreach((widget) => { - if (widget == w || @break) { - @break = true; - return; - } - h += widget.get_allocated_height(); - i++; - }); - scrolled.vadjustment.value = h - scrolled.vadjustment.page_size * 1/3; - w.get_style_context().add_class("highlight-once"); - reload_messages = true; - stack.set_visible_child_name("main"); - return false; - }); - } - - private void initialize_for_conversation_(Conversation? conversation) { - // Deinitialize old conversation - Dino.Application app = Dino.Application.get_default(); - if (this.conversation != null) { - foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) { - populator.close(conversation); - } - foreach (Plugins.NotificationPopulator populator in app.plugin_registry.notification_populators) { - populator.close(conversation); - } - } - - // Clear data structures - clear_notifications(); - this.conversation = conversation; - - // Init for new conversation - foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) { - populator.init(conversation, this, Plugins.WidgetType.GTK); - } - content_populator.init(this, conversation, Plugins.WidgetType.GTK); - subscription_notification.init(conversation, this); - - animate = false; - Timeout.add(20, () => { animate = true; return false; }); - } - - private void display_latest() { - Gee.List items = content_populator.populate_latest(conversation, 40); - foreach (ContentMetaItem item in items) { - do_insert_item(item); - } - Application app = GLib.Application.get_default() as Application; - foreach (Plugins.NotificationPopulator populator in app.plugin_registry.notification_populators) { - populator.init(conversation, this, Plugins.WidgetType.GTK); - } - Idle.add(() => { on_value_notify(); return false; }); - } - - public void insert_item(Plugins.MetaConversationItem item) { - if (meta_items.size > 0) { - bool after_last = meta_items.last().sort_time.compare(item.sort_time) <= 0; - bool within_range = meta_items.last().sort_time.compare(item.sort_time) > 0 && meta_items.first().sort_time.compare(item.sort_time) < 0; - bool accept = within_range || (at_current_content && after_last); - if (!accept) { - return; - } - } - do_insert_item(item); - } - - public void do_insert_item(Plugins.MetaConversationItem item) { - lock (meta_items) { - insert_new(item); - if (item as ContentMetaItem != null) { - content_items.add(item); - } - meta_items.add(item); - } - - inserted_item(item); - } - - private void remove_item(Plugins.MetaConversationItem item) { - ConversationItemSkeleton? skeleton = item_item_skeletons[item]; - if (skeleton != null) { - widgets[item].destroy(); - widgets.unset(item); - skeleton.destroy(); - item_skeletons.remove(skeleton); - item_item_skeletons.unset(item); - - content_items.remove(item); - meta_items.remove(item); - } - - removed_item(item); - } - - public void on_add_meta_notification(Plugins.MetaConversationNotification notification) { - Widget? widget = (Widget) notification.get_widget(Plugins.WidgetType.GTK); - if (widget != null) { - add_notification(widget); - } - } - - public void on_remove_meta_notification(Plugins.MetaConversationNotification notification){ - Widget? widget = (Widget) notification.get_widget(Plugins.WidgetType.GTK); - if (widget != null) { - remove_notification(widget); - } - } - - public void add_notification(Widget widget) { - notifications.add(widget); - Timeout.add(20, () => { - notification_revealer.transition_duration = 200; - notification_revealer.reveal_child = true; - return false; - }); - } - - public void remove_notification(Widget widget) { - notification_revealer.reveal_child = false; - widget.destroy(); - } - - private Widget insert_new(Plugins.MetaConversationItem item) { - Plugins.MetaConversationItem? lower_item = meta_items.lower(item); - - // Fill datastructure - ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation, item) { visible=true }; - item_item_skeletons[item] = item_skeleton; - int index = lower_item != null ? item_skeletons.index_of(item_item_skeletons[lower_item]) + 1 : 0; - item_skeletons.insert(index, item_skeleton); - - // Insert widget - Widget insert = item_skeleton; - if (animate) { - Revealer revealer = new Revealer() {transition_duration = 200, transition_type = RevealerTransitionType.SLIDE_UP, visible = true}; - revealer.add(item_skeleton); - insert = revealer; - main.add(insert); - revealer.reveal_child = true; - } else { - main.add(insert); - } - widgets[item] = insert; - main.reorder_child(insert, index); - - if (lower_item != null) { - if (can_merge(item, lower_item)) { - ConversationItemSkeleton lower_skeleton = item_item_skeletons[lower_item]; - item_skeleton.show_skeleton = false; - lower_skeleton.last_group_item = false; - } - } - - Plugins.MetaConversationItem? upper_item = meta_items.higher(item); - if (upper_item != null) { - if (!can_merge(upper_item, item)) { - ConversationItemSkeleton upper_skeleton = item_item_skeletons[upper_item]; - upper_skeleton.show_skeleton = true; - } - } - - // If an item from the past was added, add everything between that item and the (post-)first present item - if (index == 0) { - Dino.Application app = Dino.Application.get_default(); - if (item_skeletons.size == 1) { - foreach (Plugins.ConversationAdditionPopulator populator in app.plugin_registry.conversation_addition_populators) { - populator.populate_timespan(conversation, item.sort_time, new DateTime.now_utc()); - } - } else { - foreach (Plugins.ConversationAdditionPopulator populator in app.plugin_registry.conversation_addition_populators) { - populator.populate_timespan(conversation, item.sort_time, meta_items.higher(item).sort_time); - } - } - } - return insert; - } - - private bool can_merge(Plugins.MetaConversationItem upper_item /*more recent, displayed below*/, Plugins.MetaConversationItem lower_item /*less recent, displayed above*/) { - return upper_item.display_time != null && lower_item.display_time != null && - upper_item.display_time.difference(lower_item.display_time) < TimeSpan.MINUTE && - upper_item.jid.equals(lower_item.jid) && - upper_item.encryption == lower_item.encryption && - (upper_item.mark == Message.Marked.WONTSEND) == (lower_item.mark == Message.Marked.WONTSEND); - } - - private void on_upper_notify() { - if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size - if (at_current_content) { - scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down - } - } else if (scrolled.vadjustment.value < scrolled.vadjustment.upper - scrolled.vadjustment.page_size - 1) { - scrolled.vadjustment.value = scrolled.vadjustment.upper - was_upper + scrolled.vadjustment.value; // stay at same content - } - was_upper = scrolled.vadjustment.upper; - was_page_size = scrolled.vadjustment.page_size; - was_value = scrolled.vadjustment.value; - reloading_mutex.trylock(); - reloading_mutex.unlock(); - } - - private void on_value_notify() { - if (scrolled.vadjustment.value < 400) { - load_earlier_messages(); - } else if (scrolled.vadjustment.upper - (scrolled.vadjustment.value + scrolled.vadjustment.page_size) < 400) { - load_later_messages(); - } - } - - private void load_earlier_messages() { - was_value = scrolled.vadjustment.value; - if (!reloading_mutex.trylock()) return; - if (meta_items.size > 0) { - Gee.List items = content_populator.populate_before(conversation, (content_items.first() as ContentMetaItem).content_item, 20); - foreach (ContentMetaItem item in items) { - do_insert_item(item); - } - } else { - reloading_mutex.unlock(); - } - } - - private void load_later_messages() { - if (!reloading_mutex.trylock()) return; - if (meta_items.size > 0 && !at_current_content) { - Gee.List items = content_populator.populate_after(conversation, (content_items.last() as ContentMetaItem).content_item, 20); - if (items.size == 0) { - at_current_content = true; - } - foreach (ContentMetaItem item in items) { - do_insert_item(item); - } - } else { - reloading_mutex.unlock(); - } - } - - private static int compare_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) { - int cmp1 = a.sort_time.compare(b.sort_time); - if (cmp1 == 0) { - double cmp2 = a.seccondary_sort_indicator - b.seccondary_sort_indicator; - if (cmp2 == 0) { - return (int) (a.tertiary_sort_indicator - b.tertiary_sort_indicator); - } - return (int) cmp2; - } - return cmp1; - } - - private void clear() { - was_upper = null; - was_page_size = null; - content_items.clear(); - meta_items.clear(); - item_skeletons.clear(); - item_item_skeletons.clear(); - widgets.clear(); - main.@foreach((widget) => { widget.destroy(); }); - } - - private void clear_notifications() { - notifications.@foreach((widget) => { widget.destroy(); }); - notification_revealer.transition_duration = 0; - notification_revealer.set_reveal_child(false); - } -} - -} diff --git a/main/src/ui/conversation_summary/date_separator_populator.vala b/main/src/ui/conversation_summary/date_separator_populator.vala deleted file mode 100644 index 3ddb0d9a..00000000 --- a/main/src/ui/conversation_summary/date_separator_populator.vala +++ /dev/null @@ -1,105 +0,0 @@ -using Gee; -using Gtk; - -using Dino.Entities; -using Xmpp; - -namespace Dino.Ui.ConversationSummary { - -class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Plugins.ConversationAdditionPopulator, Object { - - public string id { get { return "date_separator"; } } - - private StreamInteractor stream_interactor; - private Conversation? current_conversation; - private Plugins.ConversationItemCollection? item_collection; - private Gee.TreeSet insert_times; - - - public DateSeparatorPopulator(StreamInteractor stream_interactor) { - this.stream_interactor = stream_interactor; - } - - public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection, Plugins.WidgetType type) { - current_conversation = conversation; - this.item_collection = item_collection; - item_collection.inserted_item.connect(on_inserted_item); - this.insert_times = new TreeSet((a, b) => { - return a.compare(b); - }); - } - - public void close(Conversation conversation) { - item_collection.inserted_item.disconnect(on_inserted_item); - } - - public void populate_timespan(Conversation conversation, DateTime after, DateTime before) { } - - private void on_inserted_item(Plugins.MetaConversationItem item) { - if (!(item is ContentMetaItem)) return; - - DateTime time = item.sort_time.to_local(); - DateTime msg_date = new DateTime.local(time.get_year(), time.get_month(), time.get_day_of_month(), 0, 0, 0); - if (!insert_times.contains(msg_date)) { - if (insert_times.lower(msg_date) != null) { - item_collection.insert_item(new MetaDateItem(msg_date.to_utc())); - } else if (insert_times.size > 0) { - item_collection.insert_item(new MetaDateItem(insert_times.first().to_utc())); - } - insert_times.add(msg_date); - } - } -} - -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) { - this.date = date; - this.sort_time = date; - } - - public override Object? get_widget(Plugins.WidgetType widget_type) { - Box box = new Box(Orientation.HORIZONTAL, 10) { width_request=300, halign=Align.CENTER, visible=true }; - box.add(new Separator(Orientation.HORIZONTAL) { valign=Align.CENTER, hexpand=true, visible=true }); - string date_str = get_relative_time(date); - Label label = new Label(@"$date_str") { use_markup=true, halign=Align.CENTER, hexpand=false, visible=true }; - label.get_style_context().add_class("dim-label"); - box.add(label); - box.add(new Separator(Orientation.HORIZONTAL) { valign=Align.CENTER, hexpand=true, visible=true }); - return box; - } - - private static string get_relative_time(DateTime time) { - DateTime time_local = time.to_local(); - DateTime now_local = new DateTime.now_local(); - if (time_local.get_year() == now_local.get_year() && - time_local.get_month() == now_local.get_month() && - time_local.get_day_of_month() == now_local.get_day_of_month()) { - return _("Today"); - } - DateTime now_local_minus = now_local.add_days(-1); - if (time_local.get_year() == now_local_minus.get_year() && - time_local.get_month() == now_local_minus.get_month() && - time_local.get_day_of_month() == now_local_minus.get_day_of_month()) { - return _("Yesterday"); - } - if (time_local.get_year() != now_local.get_year()) { - return time_local.format("%x"); - } - TimeSpan timespan = now_local.difference(time_local); - if (timespan < 7 * TimeSpan.DAY) { - return time_local.format(_("%a, %b %d")); - } else { - return time_local.format(_("%b %d")); - } - } -} - -} diff --git a/main/src/ui/conversation_summary/file_widget.vala b/main/src/ui/conversation_summary/file_widget.vala deleted file mode 100644 index 91b8912b..00000000 --- a/main/src/ui/conversation_summary/file_widget.vala +++ /dev/null @@ -1,338 +0,0 @@ -using Gee; -using Gdk; -using Gtk; -using Pango; - -using Dino.Entities; - -namespace Dino.Ui.ConversationSummary { - -public class FileWidget : Box { - - enum State { - IMAGE, - DEFAULT - } - - private const int MAX_HEIGHT = 300; - private const int MAX_WIDTH = 600; - - private StreamInteractor stream_interactor; - private FileTransfer file_transfer; - private State state; - - // default box - private Box main_box; - private Image content_type_image; - private Image download_image; - private Spinner spinner; - private Label mime_label; - private Stack image_stack; - - private Widget content; - - private bool pointer_inside = false; - - public FileWidget(StreamInteractor stream_interactor, FileTransfer file_transfer) { - this.stream_interactor = stream_interactor; - this.file_transfer = file_transfer; - - load_widget.begin(); - } - - private async void load_widget() { - if (show_image()) { - content = yield get_image_widget(file_transfer); - if (content != null) { - this.state = State.IMAGE; - this.add(content); - return; - } - } - content = get_default_widget(file_transfer); - this.state = State.DEFAULT; - this.add(content); - } - - private async Widget? get_image_widget(FileTransfer file_transfer) { - // Load and prepare image in tread - Thread thread = new Thread (null, () => { - Image image = new Image() { halign=Align.START, visible = true }; - - Gdk.Pixbuf pixbuf; - try { - pixbuf = new Gdk.Pixbuf.from_file(file_transfer.get_file().get_path()); - } catch (Error error) { - warning("Can't load picture %s - %s", file_transfer.get_file().get_path(), error.message); - Idle.add(get_image_widget.callback); - return null; - } - - pixbuf = pixbuf.apply_embedded_orientation(); - - int max_scaled_height = MAX_HEIGHT * image.scale_factor; - if (pixbuf.height > max_scaled_height) { - pixbuf = pixbuf.scale_simple((int) ((double) max_scaled_height / pixbuf.height * pixbuf.width), max_scaled_height, Gdk.InterpType.BILINEAR); - } - int max_scaled_width = MAX_WIDTH * image.scale_factor; - if (pixbuf.width > max_scaled_width) { - pixbuf = pixbuf.scale_simple(max_scaled_width, (int) ((double) max_scaled_width / pixbuf.width * pixbuf.height), Gdk.InterpType.BILINEAR); - } - pixbuf = crop_corners(pixbuf, 3 * image.get_scale_factor()); - Util.image_set_from_scaled_pixbuf(image, pixbuf); - - Idle.add(get_image_widget.callback); - return image; - }); - yield; - Image image = thread.join(); - if (image == null) return null; - - Util.force_css(image, "* { box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.1); margin: 2px; border-radius: 3px; }"); - - Builder builder = new Builder.from_resource("/im/dino/Dino/conversation_summary/image_toolbar.ui"); - Widget toolbar = builder.get_object("main") as Widget; - Util.force_background(toolbar, "rgba(0, 0, 0, 0.5)"); - Util.force_css(toolbar, "* { padding: 3px; border-radius: 3px; }"); - - Label url_label = builder.get_object("url_label") as Label; - Util.force_color(url_label, "#eee"); - - if (file_transfer.file_name != null && file_transfer.file_name != "") { - string caption = file_transfer.file_name; - url_label.label = caption; - } else { - url_label.visible = false; - } - - Image open_image = builder.get_object("open_image") as Image; - Util.force_css(open_image, "*:not(:hover) { color: #eee; }"); - Button open_button = builder.get_object("open_button") as Button; - Util.force_css(open_button, "*:hover { background-color: rgba(255,255,255,0.3); border-color: transparent; }"); - open_button.clicked.connect(() => { - try{ - AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null); - } catch (Error err) { - info("Could not to open file://%s: %s", file_transfer.get_file().get_path(), err.message); - } - }); - - Revealer toolbar_revealer = new Revealer() { transition_type=RevealerTransitionType.CROSSFADE, transition_duration=400, visible=true }; - toolbar_revealer.add(toolbar); - - Grid grid = new Grid() { visible=true }; - grid.attach(toolbar_revealer, 0, 0, 1, 1); - grid.attach(image, 0, 0, 1, 1); - - EventBox event_box = new EventBox() { margin_top=5, halign=Align.START, visible=true }; - event_box.events = EventMask.POINTER_MOTION_MASK; - event_box.add(grid); - event_box.enter_notify_event.connect(() => { toolbar_revealer.reveal_child = true; return false; }); - event_box.leave_notify_event.connect(() => { toolbar_revealer.reveal_child = false; return false; }); - - return event_box; - } - - private static Gdk.Pixbuf crop_corners(Gdk.Pixbuf pixbuf, double radius = 3) { - Cairo.Context ctx = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.ARGB32, pixbuf.width, pixbuf.height)); - Gdk.cairo_set_source_pixbuf(ctx, pixbuf, 0, 0); - double degrees = Math.PI / 180.0; - ctx.new_sub_path(); - ctx.arc(pixbuf.width - radius, radius, radius, -90 * degrees, 0 * degrees); - ctx.arc(pixbuf.width - radius, pixbuf.height - radius, radius, 0 * degrees, 90 * degrees); - ctx.arc(radius, pixbuf.height - radius, radius, 90 * degrees, 180 * degrees); - ctx.arc(radius, radius, radius, 180 * degrees, 270 * degrees); - ctx.close_path(); - ctx.clip(); - ctx.paint(); - return Gdk.pixbuf_get_from_surface(ctx.get_target(), 0, 0, pixbuf.width, pixbuf.height); - } - - private Widget get_default_widget(FileTransfer file_transfer) { - string icon_name = get_file_icon_name(file_transfer.mime_type); - - main_box = new Box(Orientation.HORIZONTAL, 10) { halign=Align.FILL, hexpand=true, visible=true }; - content_type_image = new Image.from_icon_name(icon_name, IconSize.DND) { opacity=0.5, visible=true }; - download_image = new Image.from_icon_name("dino-file-download-symbolic", IconSize.DND) { opacity=0.7, visible=true }; - spinner = new Spinner() { visible=true }; - - EventBox stack_event_box = new EventBox() { visible=true }; - image_stack = new Stack() { transition_type = StackTransitionType.CROSSFADE, transition_duration=50, valign=Align.CENTER, visible=true }; - image_stack.add_named(download_image, "download_image"); - image_stack.add_named(spinner, "spinner"); - image_stack.add_named(content_type_image, "content_type_image"); - stack_event_box.add(image_stack); - - main_box.add(stack_event_box); - - Box right_box = new Box(Orientation.VERTICAL, 0) { hexpand=true, visible=true }; - Label name_label = new Label(file_transfer.file_name) { ellipsize=EllipsizeMode.MIDDLE, max_width_chars=1, hexpand=true, xalign=0, yalign=0, visible=true}; - right_box.add(name_label); - - EventBox mime_label_event_box = new EventBox() { visible=true }; - mime_label = new Label("") { use_markup=true, xalign=0, yalign=1, visible=true}; - - mime_label_event_box.add(mime_label); - mime_label.get_style_context().add_class("dim-label"); - - right_box.add(mime_label_event_box); - main_box.add(right_box); - - EventBox event_box = new EventBox() { margin_top=5, width_request=500, halign=Align.START, visible=true }; - event_box.get_style_context().add_class("file-box-outer"); - event_box.add(main_box); - main_box.get_style_context().add_class("file-box"); - - event_box.enter_notify_event.connect((event) => { - pointer_inside = true; - Timeout.add(20, () => { - if (pointer_inside) { - event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.HAND2)); - content_type_image.opacity = 0.7; - if (file_transfer.state == FileTransfer.State.NOT_STARTED) { - image_stack.set_visible_child_name("download_image"); - } - } - return false; - }); - return false; - }); - stack_event_box.enter_notify_event.connect((event) => { pointer_inside = true; return false; }); - mime_label_event_box.enter_notify_event.connect((event) => { pointer_inside = true; return false; }); - mime_label.enter_notify_event.connect((event) => { pointer_inside = true; return false; }); - event_box.leave_notify_event.connect((event) => { - pointer_inside = false; - Timeout.add(20, () => { - if (!pointer_inside) { - event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.XTERM)); - content_type_image.opacity = 0.5; - if (file_transfer.state == FileTransfer.State.NOT_STARTED) { - image_stack.set_visible_child_name("content_type_image"); - } - } - return false; - }); - return false; - }); - stack_event_box.leave_notify_event.connect((event) => { pointer_inside = true; return false; }); - mime_label_event_box.leave_notify_event.connect((event) => { pointer_inside = true; return false; }); - mime_label.leave_notify_event.connect((event) => { pointer_inside = true; return false; }); - event_box.button_release_event.connect((event_button) => { - switch (file_transfer.state) { - case FileTransfer.State.COMPLETE: - if (event_button.button == 1) { - try{ - AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null); - } catch (Error err) { - print("Tried to open " + file_transfer.get_file().get_path()); - } - } - break; - case FileTransfer.State.NOT_STARTED: - stream_interactor.get_module(FileManager.IDENTITY).download_file.begin(file_transfer); - break; - } - return false; - }); - - main_box.events = EventMask.POINTER_MOTION_MASK; - content_type_image.events = EventMask.POINTER_MOTION_MASK; - download_image.events = EventMask.POINTER_MOTION_MASK; - spinner.events = EventMask.POINTER_MOTION_MASK; - image_stack.events = EventMask.POINTER_MOTION_MASK; - right_box.events = EventMask.POINTER_MOTION_MASK; - name_label.events = EventMask.POINTER_MOTION_MASK; - mime_label.events = EventMask.POINTER_MOTION_MASK; - event_box.events = EventMask.POINTER_MOTION_MASK; - mime_label.events = EventMask.POINTER_MOTION_MASK; - mime_label_event_box.events = EventMask.POINTER_MOTION_MASK; - - file_transfer.notify["path"].connect(update_file_info); - file_transfer.notify["state"].connect(update_file_info); - file_transfer.notify["mime-type"].connect(update_file_info); - update_file_info.begin(); - - return event_box; - } - - private async void update_file_info() { - if (file_transfer.state == FileTransfer.State.COMPLETE && show_image() && state != State.IMAGE) { - this.remove(content); - this.add(yield get_image_widget(file_transfer)); - state = State.IMAGE; - } - - spinner.active = false; // A hidden spinning spinner still uses CPU. Deactivate asap - - string? mime_description = file_transfer.mime_type != null ? ContentType.get_description(file_transfer.mime_type) : null; - - switch (file_transfer.state) { - case FileTransfer.State.COMPLETE: - mime_label.label = "" + mime_description + ""; - image_stack.set_visible_child_name("content_type_image"); - break; - case FileTransfer.State.IN_PROGRESS: - mime_label.label = "" + _("Downloading %s…").printf(get_size_string(file_transfer.size)) + ""; - spinner.active = true; - image_stack.set_visible_child_name("spinner"); - break; - case FileTransfer.State.NOT_STARTED: - if (mime_description != null) { - mime_label.label = "" + _("%s offered: %s").printf(mime_description, get_size_string(file_transfer.size)) + ""; - } else if (file_transfer.size != -1) { - mime_label.label = "" + _("File offered: %s").printf(get_size_string(file_transfer.size)) + ""; - } else { - mime_label.label = "" + _("File offered") + ""; - } - image_stack.set_visible_child_name("content_type_image"); - break; - case FileTransfer.State.FAILED: - mime_label.label = "" + _("File transfer failed") + ""; - image_stack.set_visible_child_name("content_type_image"); - break; - } - } - - private static string get_file_icon_name(string? mime_type) { - if (mime_type == null) return "dino-file-symbolic"; - - string generic_icon_name = ContentType.get_generic_icon_name(mime_type) ?? ""; - switch (generic_icon_name) { - case "audio-x-generic": return "dino-file-music-symbolic"; - case "image-x-generic": return "dino-file-image-symbolic"; - case "text-x-generic": return "dino-file-document-symbolic"; - case "text-x-generic-template": return "dino-file-document-symbolic"; - case "video-x-generic": return "dino-file-video-symbolic"; - case "x-office-document": return "dino-file-document-symbolic"; - case "x-office-spreadsheet": return "dino-file-table-symbolic"; - default: return "dino-file-symbolic"; - } - } - - private static string get_size_string(int size) { - if (size < 1024) { - return @"$(size) B"; - } else if (size < 1000 * 1000) { - return @"$(size / 1000) kB"; - } else if (size < 1000 * 1000 * 1000) { - return @"$(size / 1000 / 1000) MB"; - } else { - return @"$(size / 1000 / 1000 / 1000) GB"; - } - } - - private bool show_image() { - if (file_transfer.mime_type == null || file_transfer.state != FileTransfer.State.COMPLETE) return false; - - foreach (PixbufFormat pixbuf_format in Pixbuf.get_formats()) { - foreach (string mime_type in pixbuf_format.get_mime_types()) { - if (mime_type == file_transfer.mime_type) { - return true; - } - } - } - return false; - } -} - -} diff --git a/main/src/ui/conversation_summary/message_item.vala b/main/src/ui/conversation_summary/message_item.vala deleted file mode 100644 index e69de29b..00000000 diff --git a/main/src/ui/conversation_summary/subscription_notification.vala b/main/src/ui/conversation_summary/subscription_notification.vala deleted file mode 100644 index d493ff78..00000000 --- a/main/src/ui/conversation_summary/subscription_notification.vala +++ /dev/null @@ -1,55 +0,0 @@ -using Gee; -using Gtk; - -using Dino.Entities; - -namespace Dino.Ui.ConversationSummary { - -public class SubscriptionNotitication : Object { - - private StreamInteractor stream_interactor; - private Conversation conversation; - private ConversationView conversation_view; - - public SubscriptionNotitication(StreamInteractor stream_interactor) { - this.stream_interactor = stream_interactor; - - stream_interactor.get_module(PresenceManager.IDENTITY).received_subscription_request.connect((jid, account) => { - Conversation relevant_conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(jid, account, Conversation.Type.CHAT); - stream_interactor.get_module(ConversationManager.IDENTITY).start_conversation(relevant_conversation); - if (conversation != null && account.equals(conversation.account) && jid.equals(conversation.counterpart)) { - show_notification(); - } - }); - } - - public void init(Conversation conversation, ConversationView conversation_view) { - this.conversation = conversation; - this.conversation_view = conversation_view; - - if (stream_interactor.get_module(PresenceManager.IDENTITY).exists_subscription_request(conversation.account, conversation.counterpart)) { - show_notification(); - } - } - - private void show_notification() { - Box box = new Box(Orientation.HORIZONTAL, 5) { visible=true }; - Button accept_button = new Button() { label=_("Accept"), visible=true }; - Button deny_button = new Button() { label=_("Deny"), visible=true }; - GLib.Application app = GLib.Application.get_default(); - accept_button.clicked.connect(() => { - app.activate_action("accept-subscription", conversation.id); - conversation_view.remove_notification(box); - }); - deny_button.clicked.connect(() => { - app.activate_action("deny-subscription", conversation.id); - conversation_view.remove_notification(box); - }); - box.add(new Label(_("This contact would like to add you to their contact list")) { margin_end=10, visible=true }); - box.add(accept_button); - box.add(deny_button); - conversation_view.add_notification(box); - } -} - -} -- cgit v1.2.3-54-g00ecf