From f7750c548abd9f686b12380a7aa852fe3a2a8d1b Mon Sep 17 00:00:00 2001 From: fiaxh Date: Fri, 10 Feb 2023 14:25:00 +0100 Subject: [PATCH 01/27] Clear chat input after /command fixes #1359 --- main/src/ui/chat_input/chat_input_controller.vala | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/main/src/ui/chat_input/chat_input_controller.vala b/main/src/ui/chat_input/chat_input_controller.vala index 92a12bc9..d9608a85 100644 --- a/main/src/ui/chat_input/chat_input_controller.vala +++ b/main/src/ui/chat_input/chat_input_controller.vala @@ -135,6 +135,12 @@ public class ChatInputController : Object { } string text = chat_input.chat_text_view.text_view.buffer.text; + ContentItem? quoted_content_item_bak = quoted_content_item; + + // Reset input state. Has do be done before parsing commands, because those directly return. + chat_input.chat_text_view.text_view.buffer.text = ""; + chat_input.unset_quoted_message(); + quoted_content_item = null; if (text.has_prefix("/")) { string[] token = text.split(" ", 2); @@ -189,15 +195,10 @@ public class ChatInputController : Object { } } Message out_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(text, conversation); - if (quoted_content_item != null) { - stream_interactor.get_module(Replies.IDENTITY).set_message_is_reply_to(out_message, quoted_content_item); + if (quoted_content_item_bak != null) { + stream_interactor.get_module(Replies.IDENTITY).set_message_is_reply_to(out_message, quoted_content_item_bak); } stream_interactor.get_module(MessageProcessor.IDENTITY).send_message(out_message, conversation); - - // Reset input state - chat_input.chat_text_view.text_view.buffer.text = ""; - chat_input.unset_quoted_message(); - quoted_content_item = null; } private void on_text_input_changed() { From c526848098ff187615f0be0b531c9b45644d0e03 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Thu, 16 Feb 2023 12:49:31 +0100 Subject: [PATCH 02/27] Stop regenerating message menu buttons mitigates #1343 --- libdino/src/plugin/interfaces.vala | 3 +- .../conversation_view.vala | 81 +++++++++++-------- .../item_actions.vala | 11 +-- .../message_widget.vala | 3 +- 4 files changed, 58 insertions(+), 40 deletions(-) diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala index a73cb5f7..cfe4d0cb 100644 --- a/libdino/src/plugin/interfaces.vala +++ b/libdino/src/plugin/interfaces.vala @@ -151,8 +151,9 @@ public interface ConversationItemWidgetInterface: Object { public abstract void set_widget(Object object, WidgetType type, int priority); } -public delegate void MessageActionEvoked(Object button, Plugins.MetaConversationItem evoked_on, Object widget); +public delegate void MessageActionEvoked(Variant? variant); public class MessageAction : Object { + public string name; public bool sensitive = true; public string icon_name; public string? tooltip; diff --git a/main/src/ui/conversation_content_view/conversation_view.vala b/main/src/ui/conversation_content_view/conversation_view.vala index 36e19474..ae7ed657 100644 --- a/main/src/ui/conversation_content_view/conversation_view.vala +++ b/main/src/ui/conversation_content_view/conversation_view.vala @@ -20,7 +20,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug [GtkChild] private unowned Box main; [GtkChild] private unowned Box main_wrap_box; - private ArrayList action_buttons = new ArrayList(); + private HashMap action_buttons = new HashMap(); private Gee.List? message_actions = null; private StreamInteractor stream_interactor; @@ -46,6 +46,30 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug construct { this.layout_manager = new BinLayout(); + + // Setup all message menu buttons + var correction_button = new Button() { name="correction" }; + correction_button.clicked.connect((button) => { + on_action_button_clicked(button, null); + }); + action_buttons["correction"] = correction_button; + message_menu_box.append(correction_button); + + var reply_button = new Button() { name="reply" }; + reply_button.clicked.connect((button) => { + on_action_button_clicked(button, null); + }); + action_buttons["reply"] = reply_button; + message_menu_box.append(reply_button); + + var reaction_button = new MenuButton() { name="reaction" }; + EmojiChooser chooser = new EmojiChooser(); + chooser.emoji_picked.connect((emoji) => { + on_action_button_clicked(reaction_button, new GLib.Variant.string(emoji)); + }); + reaction_button.popover = chooser; + action_buttons["reaction"] = reaction_button; + message_menu_box.append(reaction_button); } public ConversationView init(StreamInteractor stream_interactor) { @@ -112,7 +136,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug } private bool is_highlight_fixed() { - foreach (Widget widget in action_buttons) { + foreach (Widget widget in action_buttons.values) { MenuButton? menu_button = widget as MenuButton; if (menu_button != null && menu_button.popover.visible) return true; @@ -192,39 +216,32 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug return; } - foreach (Widget widget in action_buttons) { - message_menu_box.remove(widget); - } - action_buttons.clear(); + var current_message_actions = current_meta_item.get_item_actions(Plugins.WidgetType.GTK4); message_actions = current_meta_item.get_item_actions(Plugins.WidgetType.GTK4); if (message_actions != null) { message_menu_box.visible = true; - // Configure as many buttons as we need with the actions for the current meta item - foreach (var message_action in message_actions) { - if (message_action.popover != null) { - MenuButton button = new MenuButton(); - button.sensitive = message_action.sensitive; - button.icon_name = message_action.icon_name; - button.set_popover(message_action.popover as Popover); - button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip); - action_buttons.add(button); - } else if (message_action.callback != null) { - Button button = new Button(); - button.sensitive = message_action.sensitive; - button.icon_name = message_action.icon_name; - button.clicked.connect(() => { - message_action.callback(button, current_meta_item, currently_highlighted); - }); - button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip); - action_buttons.add(button); - } + foreach (Widget widget in action_buttons.values) { + widget.visible = false; } - foreach (Widget widget in action_buttons) { - message_menu_box.append(widget); + // Configure as many buttons as we need with the actions for the current meta item + foreach (var message_action in current_message_actions) { + Widget button_widget = action_buttons[message_action.name]; + button_widget.visible = true; + if (message_action.name == "reaction") { + MenuButton button = (MenuButton) button_widget; + button.sensitive = message_action.sensitive; + button.icon_name = message_action.icon_name; + button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip); + } else if (message_action.callback != null) { + Button button = (Button) button_widget; + button.sensitive = message_action.sensitive; + button.icon_name = message_action.icon_name; + button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip); + } } } else { message_menu_box.visible = false; @@ -498,12 +515,10 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug (upper_item.mark == Message.Marked.WONTSEND) == (lower_item.mark == Message.Marked.WONTSEND); } - private void on_action_button_clicked(ToggleButton button) { - int button_idx = action_buttons.index_of(button); - print(button_idx.to_string() + "\n"); - Plugins.MessageAction message_action = message_actions[button_idx]; - if (message_action.callback != null) { - message_action.callback(button, current_meta_item, currently_highlighted); + private void on_action_button_clicked(Widget widget, GLib.Variant? variant = null) { + foreach (var action in message_actions) { + if (action.name != widget.name) continue; + action.callback(variant); } } diff --git a/main/src/ui/conversation_content_view/item_actions.vala b/main/src/ui/conversation_content_view/item_actions.vala index 2cca7565..d4195bdd 100644 --- a/main/src/ui/conversation_content_view/item_actions.vala +++ b/main/src/ui/conversation_content_view/item_actions.vala @@ -4,14 +4,14 @@ using Gtk; namespace Dino.Ui { public Plugins.MessageAction get_reaction_action(ContentItem content_item, Conversation conversation, StreamInteractor stream_interactor) { Plugins.MessageAction action = new Plugins.MessageAction(); + action.name = "reaction"; action.icon_name = "dino-emoticon-add-symbolic"; action.tooltip = _("Add reaction"); - EmojiChooser chooser = new EmojiChooser(); - chooser.emoji_picked.connect((emoji) => { + action.callback = (variant) => { + string emoji = variant.get_string(); stream_interactor.get_module(Reactions.IDENTITY).add_reaction(conversation, content_item, emoji); - }); - action.popover = chooser; + }; // Disable the button if reaction aren't possible. bool supports_reactions = stream_interactor.get_module(Reactions.IDENTITY).conversation_supports_reactions(conversation); @@ -29,9 +29,10 @@ namespace Dino.Ui { public Plugins.MessageAction get_reply_action(ContentItem content_item, Conversation conversation, StreamInteractor stream_interactor) { Plugins.MessageAction action = new Plugins.MessageAction(); + action.name = "reply"; action.icon_name = "mail-reply-sender-symbolic"; action.tooltip = _("Reply"); - action.callback = (button, content_meta_item_activated, widget) => { + action.callback = () => { GLib.Application.get_default().activate_action("quote", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(conversation.id), new GLib.Variant.int32(content_item.id) })); }; diff --git a/main/src/ui/conversation_content_view/message_widget.vala b/main/src/ui/conversation_content_view/message_widget.vala index b05fe850..d7733d5a 100644 --- a/main/src/ui/conversation_content_view/message_widget.vala +++ b/main/src/ui/conversation_content_view/message_widget.vala @@ -209,9 +209,10 @@ public class MessageMetaItem : ContentMetaItem { bool correction_allowed = stream_interactor.get_module(MessageCorrection.IDENTITY).is_own_correction_allowed(message_item.conversation, message_item.message); if (correction_allowed) { Plugins.MessageAction action1 = new Plugins.MessageAction(); + action1.name = "correction"; action1.icon_name = "document-edit-symbolic"; action1.tooltip = _("Edit message"); - action1.callback = (button, content_meta_item_activated, widget) => { + action1.callback = () => { this.in_edit_mode = true; }; actions.add(action1); From fb799e3ba8313ff2da53d9d5c34f8421f164cbf1 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Mon, 27 Feb 2023 23:38:31 +0100 Subject: [PATCH 03/27] Fix some memory leaks --- .../conversation_item_skeleton.vala | 36 +++++++- .../conversation_view.vala | 10 ++- .../file_widget.vala | 11 +++ .../message_widget.vala | 88 +++++++++++-------- main/src/ui/widgets/date_separator.vala | 10 ++- .../omemo/src/ui/bad_messages_populator.vala | 41 ++++++--- 6 files changed, 143 insertions(+), 53 deletions(-) diff --git a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala index 9e98cacb..b6c0f7c6 100644 --- a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala +++ b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala @@ -71,9 +71,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, ContentMetaItem? content_meta_item = item as ContentMetaItem; if (content_meta_item != null) { reactions_controller = new ReactionsController(conversation, content_meta_item.content_item, stream_interactor); - reactions_controller.box_activated.connect((widget) => { - set_widget(widget, Plugins.WidgetType.GTK4, 3); - }); + reactions_controller.box_activated.connect(on_reaction_box_activated); reactions_controller.init(); } @@ -170,6 +168,10 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, } } + private void on_reaction_box_activated(Widget widget) { + set_widget(widget, Plugins.WidgetType.GTK4, 3); + } + private void update_time() { time_label.label = get_relative_time(item.time.to_local()).to_string(); @@ -271,6 +273,34 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, stream_interactor.get_module(RosterManager.IDENTITY).disconnect(updated_roster_handler_id); updated_roster_handler_id = 0; } + reactions_controller = null; + + // Children won't be disposed automatically + if (name_label != null) { + name_label.unparent(); + name_label.dispose(); + name_label = null; + } + if (time_label != null) { + time_label.unparent(); + time_label.dispose(); + time_label = null; + } + if (avatar_image != null) { + avatar_image.unparent(); + avatar_image.dispose(); + avatar_image = null; + } + if (encryption_image != null) { + encryption_image.unparent(); + encryption_image.dispose(); + encryption_image = null; + } + if (received_image != null) { + received_image.unparent(); + received_image.dispose(); + received_image = null; + } base.dispose(); } } diff --git a/main/src/ui/conversation_content_view/conversation_view.vala b/main/src/ui/conversation_content_view/conversation_view.vala index ae7ed657..badc6c65 100644 --- a/main/src/ui/conversation_content_view/conversation_view.vala +++ b/main/src/ui/conversation_content_view/conversation_view.vala @@ -426,6 +426,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug content_items.remove((ContentMetaItem)item); } meta_items.remove(item); + skeleton.dispose(); } removed_item(item); @@ -591,12 +592,19 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug private void clear() { was_upper = null; was_page_size = null; + foreach (var item in content_items) { + item.dispose(); + } content_items.clear(); meta_items.clear(); widget_order.clear(); + foreach (var skeleton in item_item_skeletons.values) { + skeleton.dispose(); + } item_item_skeletons.clear(); foreach (Widget widget in widgets.values) { - main.remove(widget); + widget.unparent(); + widget.dispose(); } widgets.clear(); } diff --git a/main/src/ui/conversation_content_view/file_widget.vala b/main/src/ui/conversation_content_view/file_widget.vala index 543eb169..785acf7d 100644 --- a/main/src/ui/conversation_content_view/file_widget.vala +++ b/main/src/ui/conversation_content_view/file_widget.vala @@ -135,6 +135,17 @@ public class FileWidget : SizeRequestBox { } return false; } + + public override void dispose() { + if (default_widget_controller != null) default_widget_controller.dispose(); + default_widget_controller = null; + if (content != null) { + content.unparent(); + content.dispose(); + content = null; + } + base.dispose(); + } } public class FileWidgetController : Object { diff --git a/main/src/ui/conversation_content_view/message_widget.vala b/main/src/ui/conversation_content_view/message_widget.vala index d7733d5a..11b38286 100644 --- a/main/src/ui/conversation_content_view/message_widget.vala +++ b/main/src/ui/conversation_content_view/message_widget.vala @@ -19,6 +19,7 @@ public class MessageMetaItem : ContentMetaItem { private StreamInteractor stream_interactor; private MessageItem message_item; public Message.Marked marked { get; set; } + public Plugins.ConversationItemWidgetInterface outer = null; MessageItemEditMode? edit_mode = null; ChatTextViewController? controller = null; @@ -35,6 +36,8 @@ public class MessageMetaItem : ContentMetaItem { message_item = content_item as MessageItem; this.stream_interactor = stream_interactor; + stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.connect(on_received_correction); + label.activate_link.connect(on_label_activate_link); Message message = ((MessageItem) content_item).message; @@ -146,43 +149,9 @@ public class MessageMetaItem : ContentMetaItem { } public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) { + this.outer = outer; - stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.connect(on_received_correction); - - this.notify["in-edit-mode"].connect(() => { - if (in_edit_mode == false) return; - bool allowed = stream_interactor.get_module(MessageCorrection.IDENTITY).is_own_correction_allowed(message_item.conversation, message_item.message); - if (allowed) { - MessageItem message_item = content_item as MessageItem; - Message message = message_item.message; - - edit_mode = new MessageItemEditMode(); - controller = new ChatTextViewController(edit_mode.chat_text_view, stream_interactor); - Conversation conversation = message_item.conversation; - controller.initialize_for_conversation(conversation); - - edit_mode.cancelled.connect(() => { - in_edit_mode = false; - outer.set_widget(label, Plugins.WidgetType.GTK4, 2); - }); - edit_mode.send.connect(() => { - if (((MessageItem) content_item).message.body != edit_mode.chat_text_view.text_view.buffer.text) { - on_edit_send(edit_mode.chat_text_view.text_view.buffer.text); - } else { -// edit_cancelled(); - } - in_edit_mode = false; - outer.set_widget(label, Plugins.WidgetType.GTK4, 2); - }); - - edit_mode.chat_text_view.text_view.buffer.text = message.body; - - outer.set_widget(edit_mode, Plugins.WidgetType.GTK4, 2); - edit_mode.chat_text_view.text_view.grab_focus(); - } else { - this.in_edit_mode = false; - } - }); + this.notify["in-edit-mode"].connect(on_in_edit_mode_changed); outer.set_widget(label, Plugins.WidgetType.GTK4, 2); @@ -201,7 +170,6 @@ public class MessageMetaItem : ContentMetaItem { } public override Gee.List? get_item_actions(Plugins.WidgetType type) { - if (content_item as FileItem != null || this.in_edit_mode) return null; if (in_edit_mode) return null; Gee.List actions = new ArrayList(); @@ -224,6 +192,41 @@ public class MessageMetaItem : ContentMetaItem { return actions; } + private void on_in_edit_mode_changed() { + if (in_edit_mode == false) return; + bool allowed = stream_interactor.get_module(MessageCorrection.IDENTITY).is_own_correction_allowed(message_item.conversation, message_item.message); + if (allowed) { + MessageItem message_item = content_item as MessageItem; + Message message = message_item.message; + + edit_mode = new MessageItemEditMode(); + controller = new ChatTextViewController(edit_mode.chat_text_view, stream_interactor); + Conversation conversation = message_item.conversation; + controller.initialize_for_conversation(conversation); + + edit_mode.cancelled.connect(() => { + in_edit_mode = false; + outer.set_widget(label, Plugins.WidgetType.GTK4, 2); + }); + edit_mode.send.connect(() => { + if (((MessageItem) content_item).message.body != edit_mode.chat_text_view.text_view.buffer.text) { + on_edit_send(edit_mode.chat_text_view.text_view.buffer.text); + } else { +// edit_cancelled(); + } + in_edit_mode = false; + outer.set_widget(label, Plugins.WidgetType.GTK4, 2); + }); + + edit_mode.chat_text_view.text_view.buffer.text = message.body; + + outer.set_widget(edit_mode, Plugins.WidgetType.GTK4, 2); + edit_mode.chat_text_view.text_view.grab_focus(); + } else { + this.in_edit_mode = false; + } + } + private void on_edit_send(string text) { stream_interactor.get_module(MessageCorrection.IDENTITY).send_correction(message_item.conversation, message_item.message, text); this.in_edit_mode = false; @@ -244,6 +247,17 @@ public class MessageMetaItem : ContentMetaItem { Dino.Application.get_default().open(new File[]{file}, ""); return true; } + + public override void dispose() { + stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.disconnect(on_received_correction); + this.notify["in-edit-mode"].disconnect(on_in_edit_mode_changed); + if (label != null) { + label.unparent(); + label.dispose(); + label = null; + } + base.dispose(); + } } [GtkTemplate (ui = "/im/dino/Dino/message_item_widget_edit_mode.ui")] diff --git a/main/src/ui/widgets/date_separator.vala b/main/src/ui/widgets/date_separator.vala index 95729bce..b5d84a5b 100644 --- a/main/src/ui/widgets/date_separator.vala +++ b/main/src/ui/widgets/date_separator.vala @@ -40,8 +40,14 @@ public class Dino.Ui.ViewModel.CompatDateSeparatorModel : DateSeparatorModel { private void update_time_label() { date_label = get_relative_time(date); - time_update_timeout = Timeout.add_seconds((int) get_next_time_change(), () => { - if (time_update_timeout != 0) update_time_label(); + time_update_timeout = set_update_time_label_timeout((int) get_next_time_change(), this); + } + + private static uint set_update_time_label_timeout(int interval, CompatDateSeparatorModel model_) { + WeakRef model_weak = WeakRef(model_); + return Timeout.add_seconds(interval, () => { + CompatDateSeparatorModel? model = (CompatDateSeparatorModel) model_weak.get(); + if (model != null && model.time_update_timeout != 0) model.update_time_label(); return false; }); } diff --git a/plugins/omemo/src/ui/bad_messages_populator.vala b/plugins/omemo/src/ui/bad_messages_populator.vala index 3cb3375b..8f087482 100644 --- a/plugins/omemo/src/ui/bad_messages_populator.vala +++ b/plugins/omemo/src/ui/bad_messages_populator.vala @@ -94,6 +94,7 @@ public class BadMessagesPopulator : Plugins.ConversationItemPopulator, Plugins.C foreach (BadMessageItem bad_item in bad_items) { item_collection.remove_item(bad_item); } + bad_items.clear(); } public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection, Plugins.WidgetType type) { @@ -103,7 +104,9 @@ public class BadMessagesPopulator : Plugins.ConversationItemPopulator, Plugins.C init_state(); } - public void close(Conversation conversation) { } + public void close(Conversation conversation) { + clear_state(); + } public void populate_timespan(Conversation conversation, DateTime after, DateTime before) { } } @@ -131,9 +134,17 @@ public class BadMessageItem : Plugins.MetaConversationItem { } public class BadMessagesWidget : Box { + private Plugin plugin; + private Conversation conversation; + private Jid jid; + private Label label; + public BadMessagesWidget(Plugin plugin, Conversation conversation, Jid jid, BadnessType badness_type) { Object(orientation:Orientation.HORIZONTAL, spacing:5); + this.plugin = plugin; + this.conversation = conversation; + this.jid = jid; this.halign = Align.CENTER; this.visible = true; @@ -159,19 +170,29 @@ public class BadMessagesWidget : Box { } else { warning_text += _("%s does not trust this device. That means, you might be missing messages.").printf(who); } - Label label = new Label(warning_text) { margin_start=70, margin_end=70, justify=Justification.CENTER, use_markup=true, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=true }; + label = new Label(warning_text) { margin_start=70, margin_end=70, justify=Justification.CENTER, use_markup=true, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=true }; label.add_css_class("dim-label"); this.append(label); - label.activate_link.connect(() => { - if (badness_type == BadnessType.UNTRUSTED) { - ContactDetailsDialog dialog = new ContactDetailsDialog(plugin, conversation.account, jid); - dialog.set_transient_for((Window) get_root()); - dialog.present(); - } + if (badness_type == BadnessType.UNTRUSTED) { + label.activate_link.connect(on_label_activate_link); + } + } - return false; - }); + private bool on_label_activate_link() { + ContactDetailsDialog dialog = new ContactDetailsDialog(plugin, conversation.account, jid); + dialog.set_transient_for((Window) get_root()); + dialog.present(); + return false; + } + + public override void dispose() { + if (label != null) { + label.unparent(); + label.dispose(); + label = null; + } + base.dispose(); } } From 76e1410c2aaa14a14d43851afc05f3793628cabc Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 1 Mar 2023 19:50:29 +0100 Subject: [PATCH 04/27] Fix typing notifications in groupchats --- libdino/src/service/message_processor.vala | 4 ++++ .../xep/0085_chat_state_notifications.vala | 21 +++---------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala index 12bbeeac..45f06a69 100644 --- a/libdino/src/service/message_processor.vala +++ b/libdino/src/service/message_processor.vala @@ -451,6 +451,10 @@ public class MessageProcessor : StreamInteractionModule, Object { } } + if (conversation.get_send_typing_setting(stream_interactor) == Conversation.Setting.ON) { + ChatStateNotifications.add_state_to_message(new_message, ChatStateNotifications.STATE_ACTIVE); + } + stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, new_message, (_, res) => { try { stream.get_module(MessageModule.IDENTITY).send_message.end(res); diff --git a/xmpp-vala/src/module/xep/0085_chat_state_notifications.vala b/xmpp-vala/src/module/xep/0085_chat_state_notifications.vala index fa0360c0..7634e605 100644 --- a/xmpp-vala/src/module/xep/0085_chat_state_notifications.vala +++ b/xmpp-vala/src/module/xep/0085_chat_state_notifications.vala @@ -16,14 +16,12 @@ public class Module : XmppStreamModule { public signal void chat_state_received(XmppStream stream, Jid jid, string state, MessageStanza stanza); - private SendPipelineListener send_pipeline_listener = new SendPipelineListener(); - /** * "A message stanza that does not contain standard messaging content [...] SHOULD be a state other than " (0085, 5.6) */ public void send_state(XmppStream stream, Jid jid, string message_type, string state) { MessageStanza message = new MessageStanza() { to=jid, type_=message_type }; - message.stanza.put_node(new StanzaNode.build(state, NS_URI).add_self_xmlns()); + add_state_to_message(message, state); MessageProcessingHints.set_message_hint(message, MessageProcessingHints.HINT_NO_STORE); @@ -32,14 +30,12 @@ public class Module : XmppStreamModule { public override void attach(XmppStream stream) { stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); - stream.get_module(MessageModule.IDENTITY).send_pipeline.connect(send_pipeline_listener); stream.get_module(MessageModule.IDENTITY).received_message.connect(on_received_message); } public override void detach(XmppStream stream) { stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI); stream.get_module(MessageModule.IDENTITY).received_message.disconnect(on_received_message); - stream.get_module(MessageModule.IDENTITY).send_pipeline.disconnect(send_pipeline_listener); } public override string get_ns() { return NS_URI; } @@ -57,19 +53,8 @@ public class Module : XmppStreamModule { } } -public class SendPipelineListener : StanzaListener { - - private string[] after_actions_const = {"MODIFY_BODY"}; - - public override string action_group { get { return "ADD_NODES"; } } - public override string[] after_actions { get { return after_actions_const; } } - - public override async bool run(XmppStream stream, MessageStanza message) { - if (message.body == null) return false; - if (message.type_ != MessageStanza.TYPE_CHAT) return false; - message.stanza.put_node(new StanzaNode.build(STATE_ACTIVE, NS_URI).add_self_xmlns()); - return false; - } +public static void add_state_to_message(MessageStanza message, string state) { + message.stanza.put_node(new StanzaNode.build(state, NS_URI).add_self_xmlns()); } } From 74ca991ddf89a1ff9096303ab64ac861003c492f Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 1 Mar 2023 20:02:02 +0100 Subject: [PATCH 05/27] Fix critical warnings after DTLS-SRTP calls without OMEMO verification libdino-CRITICAL **: dino_plugins_encryption_list_entry_get_encryption_icon_name: assertion 'self != NULL' failed --- .../conversation_content_view/conversation_item_skeleton.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala index b6c0f7c6..bbde76b1 100644 --- a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala +++ b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala @@ -150,7 +150,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, if (item.encryption != Encryption.NONE && item.encryption != Encryption.UNKNOWN && ci != null) { string? icon_name = null; var encryption_entry = app.plugin_registry.encryption_list_entries[item.encryption]; - icon_name = encryption_entry.get_encryption_icon_name(conversation, ci.content_item); + if (encryption_entry != null) icon_name = encryption_entry.get_encryption_icon_name(conversation, ci.content_item); encryption_image.icon_name = icon_name ?? "changes-prevent-symbolic"; encryption_image.visible = true; } From 503de303d7019e5fa3d57f3d8051cff28baeb8d3 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 1 Mar 2023 23:56:43 +0100 Subject: [PATCH 06/27] Consider stream readable when EOS is reached. Fixes #1373 --- plugins/http-files/src/file_provider.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/http-files/src/file_provider.vala b/plugins/http-files/src/file_provider.vala index 1433a74f..98c1d95c 100644 --- a/plugins/http-files/src/file_provider.vala +++ b/plugins/http-files/src/file_provider.vala @@ -66,7 +66,7 @@ public class FileProvider : Dino.FileProvider, Object { public bool is_readable() { if (!can_poll()) throw new IOError.NOT_SUPPORTED("Stream is not pollable"); - return ((PollableInputStream)inner).is_readable(); + return remaining_size <= 0 || ((PollableInputStream)inner).is_readable(); } private ssize_t check_limit(ssize_t read) throws IOError { From d81829652057d63b9971b9217996438ee41788ca Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sat, 18 Aug 2018 20:09:52 +0100 Subject: [PATCH 07/27] Implement XEP-0392: Consistent Color Generation --- main/src/ui/util/helper.vala | 23 +- xmpp-vala/CMakeLists.txt | 6 +- .../consistent_color.vala | 37 ++ .../xep/0392_consistent_color/hsluv.vala | 393 ++++++++++++++++++ xmpp-vala/tests/color.vala | 50 +++ xmpp-vala/tests/common.vala | 17 + 6 files changed, 515 insertions(+), 11 deletions(-) create mode 100644 xmpp-vala/src/module/xep/0392_consistent_color/consistent_color.vala create mode 100644 xmpp-vala/src/module/xep/0392_consistent_color/hsluv.vala create mode 100644 xmpp-vala/tests/color.vala diff --git a/main/src/ui/util/helper.vala b/main/src/ui/util/helper.vala index 485e469f..35e16426 100644 --- a/main/src/ui/util/helper.vala +++ b/main/src/ui/util/helper.vala @@ -3,6 +3,7 @@ using Gtk; using Dino.Entities; using Xmpp; +using Xmpp.Xep; namespace Dino.Ui.Util { @@ -18,20 +19,22 @@ private const string[] material_colors_500 = {"F44336", "E91E63", "9C27B0", "673 private const string[] material_colors_300 = {"E57373", "F06292", "BA68C8", "9575CD", "7986CB", "64B5F6", "4FC3F7", "4DD0E1", "4DB6AC", "81C784", "AED581", "DCE775", "FFD54F", "FFB74D", "FF8A65", "A1887F"}; private const string[] material_colors_200 = {"EF9A9A", "F48FB1", "CE93D8", "B39DDB", "9FA8DA", "90CAF9", "81D4FA", "80DEEA", "80CBC4", "A5D6A7", "C5E1A5", "E6EE9C", "FFE082", "FFCC80", "FFAB91", "BCAAA4"}; +public static string get_consistent_hex_color(StreamInteractor stream_interactor, Account account, Jid jid, bool dark_theme = false) { + uint8[] rgb; + if (stream_interactor.get_module(MucManager.IDENTITY).is_groupchat(jid.bare_jid, account) && jid.resourcepart != null) { + rgb = ConsistentColor.string_to_rgb(jid.resourcepart); + } else { + rgb = ConsistentColor.string_to_rgb(jid.bare_jid.to_string()); + } + return "%.2x%.2x%.2x".printf(rgb[0], rgb[1], rgb[2]); +} + public static string get_avatar_hex_color(StreamInteractor stream_interactor, Account account, Jid jid, Conversation? conversation = null) { - uint hash = get_relevant_jid(stream_interactor, account, jid, conversation).to_string().hash(); - return material_colors_300[hash % material_colors_300.length]; -// return tango_colors_light[name.hash() % tango_colors_light.length]; + return get_consistent_hex_color(stream_interactor, account, get_relevant_jid(stream_interactor, account, jid, conversation)); } public static string get_name_hex_color(StreamInteractor stream_interactor, Account account, Jid jid, bool dark_theme = false, Conversation? conversation = null) { - uint hash = get_relevant_jid(stream_interactor, account, jid, conversation).to_string().hash(); - if (dark_theme) { - return material_colors_300[hash % material_colors_300.length]; - } else { - return material_colors_500[hash % material_colors_500.length]; - } -// return tango_colors_medium[name.hash() % tango_colors_medium.length]; + return get_consistent_hex_color(stream_interactor, account, get_relevant_jid(stream_interactor, account, jid, conversation), dark_theme); } private static Jid get_relevant_jid(StreamInteractor stream_interactor, Account account, Jid jid, Conversation? conversation = null) { diff --git a/xmpp-vala/CMakeLists.txt b/xmpp-vala/CMakeLists.txt index a988a088..39c090fe 100644 --- a/xmpp-vala/CMakeLists.txt +++ b/xmpp-vala/CMakeLists.txt @@ -106,6 +106,9 @@ SOURCES "src/module/xep/0384_omemo/omemo_encryptor.vala" "src/module/xep/0384_omemo/omemo_decryptor.vala" + "src/module/xep/0392_consistent_color/consistent_color.vala" + "src/module/xep/0392_consistent_color/hsluv.vala" + "src/module/xep/0184_message_delivery_receipts.vala" "src/module/xep/0191_blocking_command.vala" "src/module/xep/0198_stream_management.vala" @@ -160,7 +163,7 @@ DEPENDS add_definitions(${VALA_CFLAGS} -DG_LOG_DOMAIN="xmpp-vala") add_library(xmpp-vala SHARED ${ENGINE_VALA_C}) add_dependencies(xmpp-vala xmpp-vala-vapi) -target_link_libraries(xmpp-vala ${ENGINE_PACKAGES}) +target_link_libraries(xmpp-vala ${ENGINE_PACKAGES} m) set_target_properties(xmpp-vala PROPERTIES VERSION 0.1 SOVERSION 0) install(TARGETS xmpp-vala ${TARGET_INSTALL}) @@ -175,6 +178,7 @@ if(BUILD_TESTS) "tests/jid.vala" "tests/stanza.vala" + "tests/color.vala" "tests/util.vala" CUSTOM_VAPIS ${CMAKE_BINARY_DIR}/exports/xmpp-vala_internal.vapi diff --git a/xmpp-vala/src/module/xep/0392_consistent_color/consistent_color.vala b/xmpp-vala/src/module/xep/0392_consistent_color/consistent_color.vala new file mode 100644 index 00000000..9ab7e4bb --- /dev/null +++ b/xmpp-vala/src/module/xep/0392_consistent_color/consistent_color.vala @@ -0,0 +1,37 @@ +namespace Xmpp.Xep.ConsistentColor { +private const double KR = 0.299; +private const double KG = 0.587; +private const double KB = 0.114; +private const double Y = 0.732; + +public float string_to_angle(string s) { + Checksum checksum = new Checksum(ChecksumType.SHA1); + checksum.update(s.data, -1); + size_t len = 20; + uint8[] digest = new uint8[len]; + checksum.get_digest(digest, ref len); + uint16 output = ((uint16)(*(uint16*)digest)).to_little_endian(); + return (((float) output) / 65536.0f) * 360.0f; +} + +private uint8[] rgbd_to_rgb(double[] rgbd) { + return {(uint8)(rgbd[0] * 255.0), (uint8)(rgbd[1] * 255.0), (uint8)(rgbd[2] * 255.0)}; +} + +private float[] rgbd_to_rgbf(double[] rgbd) { + return {(float)rgbd[0], (float)rgbd[1], (float)rgbd[2]}; +} + +private double[] angle_to_rgbd(double angle) { + return Hsluv.hsluv_to_rgb(new double[] {angle, 100, 50}); +} + +public float[] string_to_rgbf(string s) { + return rgbd_to_rgbf(angle_to_rgbd(string_to_angle(s))); +} + +public uint8[] string_to_rgb(string s) { + return rgbd_to_rgb(angle_to_rgbd(string_to_angle(s))); +} + +} diff --git a/xmpp-vala/src/module/xep/0392_consistent_color/hsluv.vala b/xmpp-vala/src/module/xep/0392_consistent_color/hsluv.vala new file mode 100644 index 00000000..b45d84de --- /dev/null +++ b/xmpp-vala/src/module/xep/0392_consistent_color/hsluv.vala @@ -0,0 +1,393 @@ +namespace Hsluv { + +private const double[] M0 = { 3.240969941904521, -1.537383177570093, -0.498610760293 }; +private const double[] M1 = { -0.96924363628087, 1.87596750150772, 0.041555057407175 }; +private const double[] M2 = { 0.055630079696993, -0.20397695888897, 1.056971514242878 }; + +private const double[] MInv0 = { 0.41239079926595, 0.35758433938387, 0.18048078840183 }; +private const double[] MInv1 = { 0.21263900587151, 0.71516867876775, 0.072192315360733 }; +private const double[] MInv2 = { 0.019330818715591, 0.11919477979462, 0.95053215224966 }; + +private double RefX = 0.95045592705167; +private double RefY = 1.0; +private double RefZ = 1.089057750759878; + +private double RefU = 0.19783000664283; +private double RefV = 0.46831999493879; + +private double Kappa = 903.2962962; +private double Epsilon = 0.0088564516; + +private struct Bounds { + double t0; + double t1; +} + +private Bounds get_bounds_sub(double L, double sub1, double sub2, int t, double[] m) { + double m1 = m[0]; + double m2 = m[1]; + double m3 = m[2]; + double top1 = (284517 * m1 - 94839 * m3) * sub2; + double top2 = (838422 * m3 + 769860 * m2 + 731718 * m1) * L * sub2 - 769860 * t * L; + double bottom = (632260 * m3 - 126452 * m2) * sub2 + 126452 * t; + return { top1 / bottom, top2 / bottom }; +} + +private Bounds[] get_bounds(double L) { + double sub1 = Math.pow(L + 16, 3) / 1560896; + double sub2 = sub1 > Epsilon ? sub1 : L / Kappa; + + return { + get_bounds_sub(L, sub1, sub2, 0, M0), + get_bounds_sub(L, sub1, sub2, 1, M0), + get_bounds_sub(L, sub1, sub2, 0, M1), + get_bounds_sub(L, sub1, sub2, 1, M1), + get_bounds_sub(L, sub1, sub2, 0, M2), + get_bounds_sub(L, sub1, sub2, 1, M2) + }; +} + +private double intersect_line_line(double[] lineA, double[] lineB) { + return (lineA[1] - lineB[1]) / (lineB[0] - lineA[0]); +} + +private double distance_from_pole(double[] point) { + return Math.sqrt(Math.pow(point[0], 2) + Math.pow(point[1], 2)); +} + +private bool length_of_ray_until_intersect(double theta, Bounds line, out double length) { + length = line.t1 / (Math.sin(theta) - line.t0 * Math.cos(theta)); + + return length >= 0; +} + +private double max_safe_chroma_for_l(double L) { + Bounds[] bounds = get_bounds(L); + double min = double.MAX; + + for (int i = 0; i < 2; ++i) { + var m1 = bounds[i].t0; + var b1 = bounds[i].t1; + var line = new double[] { m1, b1 }; + + double x = intersect_line_line(line, new double[] {-1 / m1, 0 }); + double length = distance_from_pole(new double[] { x, b1 + x * m1 }); + + min = double.min(min, length); + } + + return min; +} + +private double max_chroma_for_lh(double L, double H) { + double hrad = H / 360 * Math.PI * 2; + + Bounds[] bounds = get_bounds(L); + double min = double.MAX; + + foreach (var bound in bounds) { + double length; + + if (length_of_ray_until_intersect(hrad, bound, out length)) { + min = double.min(min, length); + } + } + + return min; +} + +private double dot_product(double[] a, double[] b) { + double sum = 0; + + for (int i = 0; i < a.length; ++i) { + sum += a[i] * b[i]; + } + + return sum; +} + +private double round(double value, int places) { + double n = Math.pow(10, places); + + return Math.round(value * n) / n; +} + +private double from_linear(double c) { + if (c <= 0.0031308) { + return 12.92 * c; + } else { + return 1.055 * Math.pow(c, 1 / 2.4) - 0.055; + } +} + +private double to_linear(double c) { + if (c > 0.04045) { + return Math.pow((c + 0.055) / (1 + 0.055), 2.4); + } else { + return c / 12.92; + } +} + +private int[] rgb_prepare(double[] tuple) { + for (int i = 0; i < tuple.length; ++i) { + tuple[i] = round(tuple[i], 3); + } + + for (int i = 0; i < tuple.length; ++i) { + double ch = tuple[i]; + + if (ch < -0.0001 || ch > 1.0001) { + return null; //throw new Error("Illegal rgb value: " + ch); + } + } + + var results = new int[tuple.length]; + + for (int i = 0; i < tuple.length; ++i) { + results[i] = (int) Math.round(tuple[i] * 255); + } + + return results; +} + +internal double[] xyz_to_rgb(double[] tuple) { + return new double[] { + from_linear(dot_product(M0, tuple)), + from_linear(dot_product(M1, tuple)), + from_linear(dot_product(M2, tuple)) + }; +} + +internal double[] rgb_to_xyz(double[] tuple) { + var rgbl = new double[] { + to_linear(tuple[0]), + to_linear(tuple[1]), + to_linear(tuple[2]) + }; + + return new double[] { + dot_product(MInv0, rgbl), + dot_product(MInv1, rgbl), + dot_product(MInv2, rgbl) + }; +} + +private double y_to_l(double Y) { + if (Y <= Epsilon) { + return (Y / RefY) * Kappa; + } else { + return 116 * Math.pow(Y / RefY, 1.0 / 3.0) - 16; + } +} + +private double l_to_y(double L) { + if (L <= 8) { + return RefY * L / Kappa; + } else { + return RefY * Math.pow((L + 16) / 116, 3); + } +} + +internal double[] xyz_to_luv(double[] tuple) { + double X = tuple[0]; + double Y = tuple[1]; + double Z = tuple[2]; + + double varU = (4 * X) / (X + (15 * Y) + (3 * Z)); + double varV = (9 * Y) / (X + (15 * Y) + (3 * Z)); + + double L = y_to_l(Y); + + if (L == 0) { + return new double[] { 0, 0, 0 }; + } + + var U = 13 * L * (varU - RefU); + var V = 13 * L * (varV - RefV); + + return new double [] { L, U, V }; +} + +internal double[] luv_to_xyz(double[] tuple) { + double L = tuple[0]; + double U = tuple[1]; + double V = tuple[2]; + + if (L == 0) { + return new double[] { 0, 0, 0 }; + } + + double varU = U / (13 * L) + RefU; + double varV = V / (13 * L) + RefV; + + double Y = l_to_y(L); + double X = 0 - (9 * Y * varU) / ((varU - 4) * varV - varU * varV); + double Z = (9 * Y - (15 * varV * Y) - (varV * X)) / (3 * varV); + + return new double[] { X, Y, Z }; +} + +internal double[] luv_to_lch(double[] tuple) { + double L = tuple[0]; + double U = tuple[1]; + double V = tuple[2]; + + double C = Math.pow(Math.pow(U, 2) + Math.pow(V, 2), 0.5); + double Hrad = Math.atan2(V, U); + + double H = Hrad * 180.0 / Math.PI; + + if (H < 0) { + H = 360 + H; + } + + return new double[] { L, C, H }; +} + +internal double[] lch_to_luv(double[] tuple) { + double L = tuple[0]; + double C = tuple[1]; + double H = tuple[2]; + + double Hrad = H / 360.0 * 2 * Math.PI; + double U = Math.cos(Hrad) * C; + double V = Math.sin(Hrad) * C; + + return new double [] { L, U, V }; +} + +internal double[] hsluv_to_lch(double[] tuple) { + double H = tuple[0]; + double S = tuple[1]; + double L = tuple[2]; + + if (L > 99.9999999) { + return new double[] { 100, 0, H }; + } + + if (L < 0.00000001) { + return new double[] { 0, 0, H }; + } + + double max = max_chroma_for_lh(L, H); + double C = max / 100 * S; + + return new double[] { L, C, H }; +} + +internal double[] lch_to_hsluv(double[] tuple) { + double L = tuple[0]; + double C = tuple[1]; + double H = tuple[2]; + + if (L > 99.9999999) { + return new double[] { H, 0, 100 }; + } + + if (L < 0.00000001) { + return new double[] { H, 0, 0 }; + } + + double max = max_chroma_for_lh(L, H); + double S = C / max * 100; + + return new double[] { H, S, L }; +} + +internal double[] hpluv_to_lch(double[] tuple) { + double H = tuple[0]; + double S = tuple[1]; + double L = tuple[2]; + + if (L > 99.9999999) { + return new double[] { 100, 0, H }; + } + + if (L < 0.00000001) { + return new double[] { 0, 0, H }; + } + + double max = max_safe_chroma_for_l(L); + double C = max / 100 * S; + + return new double[] { L, C, H }; +} + +internal double[] lch_to_hpluv(double[] tuple) { + double L = tuple[0]; + double C = tuple[1]; + double H = tuple[2]; + + if (L > 99.9999999) { + return new double[] { H, 0, 100 }; + } + + if (L < 0.00000001) { + return new double[] { H, 0, 0 }; + } + + double max = max_safe_chroma_for_l(L); + double S = C / max * 100; + + return new double[] { H, S, L }; +} + +internal string rgb_to_hex(double[] tuple) { + int[] prepared = rgb_prepare(tuple); + + return "#%.2x%.2x%.2x".printf(prepared[0], prepared[1], prepared[2]); +} + +internal double[] hex_to_tgb(string hex) { + return new double[] { + hex.substring(1, 2).to_long(null, 16) / 255.0, + hex.substring(3, 2).to_long(null, 16) / 255.0, + hex.substring(5, 2).to_long(null, 16) / 255.0 + }; +} + +internal double[] lch_to_rgb(double[] tuple) { + return xyz_to_rgb(luv_to_xyz(lch_to_luv(tuple))); +} + +internal double[] rgb_to_lch(double[] tuple) { + return luv_to_lch(xyz_to_luv(rgb_to_xyz(tuple))); +} + +// Rgb <--> Hsluv(p) + +internal double[] hsluv_to_rgb(double[] tuple) { + return lch_to_rgb(hsluv_to_lch(tuple)); +} + +internal double[] rgb_to_hsluv(double[] tuple) { + return lch_to_hsluv(rgb_to_lch(tuple)); +} + +internal double[] hpluv_to_rgb(double[] tuple) { + return lch_to_rgb(hpluv_to_lch(tuple)); +} + +internal double[] rgb_to_hpluv(double[] tuple) { + return lch_to_hpluv(rgb_to_lch(tuple)); +} + +// Hex + +internal string hsluv_to_hex(double[] tuple) { + return rgb_to_hex(hsluv_to_rgb(tuple)); +} + +internal string hpluv_to_hex(double[] tuple) { + return rgb_to_hex(hpluv_to_rgb(tuple)); +} + +internal double[] hex_to_hsluv(string s) { + return rgb_to_hsluv(hex_to_tgb(s)); +} + +internal double[] hex_to_hpluv(string s) { + return rgb_to_hpluv(hex_to_tgb(s)); +} + +} \ No newline at end of file diff --git a/xmpp-vala/tests/color.vala b/xmpp-vala/tests/color.vala new file mode 100644 index 00000000..ded67d53 --- /dev/null +++ b/xmpp-vala/tests/color.vala @@ -0,0 +1,50 @@ +using Xmpp.Xep; + +namespace Xmpp.Test { + +class ColorTest : Gee.TestCase { + + public ColorTest() { + base("color"); + + add_test("xep-vectors-angle", () => { text_xep_vectors_angle(); }); + add_test("xep-vectors-rgbf", () => { test_xep_vectors_rgbf(); }); + add_test("rgb-to-angle", () => { test_rgb_to_angle(); }); + } + + public void text_xep_vectors_angle() { + fail_if_not_eq_double(ConsistentColor.string_to_angle("Romeo"), 327.255249); + fail_if_not_eq_double(ConsistentColor.string_to_angle("juliet@capulet.lit"), 209.410400); + fail_if_not_eq_double(ConsistentColor.string_to_angle("😺"), 331.199341); + fail_if_not_eq_double(ConsistentColor.string_to_angle("council"), 359.994507); + fail_if_not_eq_double(ConsistentColor.string_to_angle("Board"), 171.430664); + } + + private bool fail_if_not_eq_rgbf(float[] left, float[] right) { + bool failed = false; + for (int i = 0; i < 3; i++) { + failed = fail_if_not_eq_float(left[i], right[i]) || failed; + } + return failed; + } + + public void test_xep_vectors_rgbf() { + fail_if_not_eq_rgbf(ConsistentColor.string_to_rgbf("Romeo"), {0.865f,0.000f,0.686f}); + fail_if_not_eq_rgbf(ConsistentColor.string_to_rgbf("juliet@capulet.lit"), {0.000f,0.515f,0.573f}); + fail_if_not_eq_rgbf(ConsistentColor.string_to_rgbf("😺"), {0.872f,0.000f,0.659f}); + fail_if_not_eq_rgbf(ConsistentColor.string_to_rgbf("council"), {0.918f,0.000f,0.394f}); + fail_if_not_eq_rgbf(ConsistentColor.string_to_rgbf("Board"), {0.000f,0.527f,0.457f}); + } + + public void test_rgb_to_angle() { + string[] colors = {"e57373", "f06292", "ba68c8", "9575cd", "7986cb", "64b5f6", "4fc3f7", "4dd0e1", "4db6ac", "81c784", "aed581", "dce775", "fff176", "ffd54f", "ffb74d", "ff8a65"}; + foreach(string hex_color in colors) { + uint8 r = (uint8) ((double) hex_color.substring(0, 2).to_long(null, 16)); + uint8 g = (uint8) ((double) hex_color.substring(2, 2).to_long(null, 16)); + uint8 b = (uint8) ((double) hex_color.substring(4, 2).to_long(null, 16)); + //print(@"$hex_color, $r, $g, $b, $(ConsistentColor.rgb_to_angle(r, g, b))\n"); + } + } +} + +} \ No newline at end of file diff --git a/xmpp-vala/tests/common.vala b/xmpp-vala/tests/common.vala index 47dbce0e..dc1c8e50 100644 --- a/xmpp-vala/tests/common.vala +++ b/xmpp-vala/tests/common.vala @@ -6,6 +6,7 @@ int main(string[] args) { TestSuite.get_root().add_suite(new Xmpp.Test.StanzaTest().get_suite()); TestSuite.get_root().add_suite(new Xmpp.Test.UtilTest().get_suite()); TestSuite.get_root().add_suite(new Xmpp.Test.JidTest().get_suite()); + TestSuite.get_root().add_suite(new Xmpp.Test.ColorTest().get_suite()); return GLib.Test.run(); } @@ -68,6 +69,22 @@ bool fail_if_not_eq_int(int left, int right, string? reason = null) { return fail_if_not(left == right, @"$(reason + ": " ?? "")$left != $right"); } +private float float_to_accuracy(float f, float accuracy) { + return (float) (Math.round(f * Math.pow(10, accuracy)) / Math.pow(10, accuracy)); +} + +private float double_to_accuracy(double f, float accuracy) { + return (float) (Math.round(f * Math.pow(10, accuracy)) / Math.pow(10, accuracy)); +} + +bool fail_if_not_eq_float(float left, float right, float accuracy = 3, string? reason = null) { + return fail_if_not(float_to_accuracy(left, accuracy) == float_to_accuracy(right, accuracy), @"$(reason + ": " ?? "")$left != $right"); +} + +bool fail_if_not_eq_double(double left, double right, float accuracy = 3, string? reason = null) { + return fail_if_not(double_to_accuracy(left, accuracy) == double_to_accuracy(right, accuracy), @"$(reason + ": " ?? "")$left != $right"); +} + bool fail_if_not_eq_str(string? left, string? right, string? reason = null) { bool nullcheck = (left == null || right == null) && (left != null && right != null); if (left == null) left = "(null)"; From db3b0d5f233ee3587ae54f8f035222cb098b11dd Mon Sep 17 00:00:00 2001 From: Marvin W Date: Tue, 24 Jan 2023 18:59:46 +0100 Subject: [PATCH 08/27] New Avatar UI --- libdino/src/service/avatar_manager.vala | 78 ++- main/CMakeLists.txt | 3 +- main/data/add_conversation/list_row.ui | 7 +- main/data/contact_details_dialog.ui | 8 +- main/data/conversation_item_widget.ui | 6 +- main/data/conversation_row.ui | 6 +- main/data/manage_accounts/account_row.ui | 6 +- main/data/manage_accounts/dialog.ui | 8 +- main/data/occupant_list_item.ui | 6 +- main/data/quote.ui | 7 +- main/data/search_autocomplete.ui | 7 +- main/data/style.css | 10 +- .../ui/add_conversation/conference_list.vala | 2 +- main/src/ui/add_conversation/list_row.vala | 6 +- .../add_conversation/select_jid_fragment.vala | 2 +- main/src/ui/avatar_drawer.vala | 193 ------- main/src/ui/avatar_generator.vala | 0 main/src/ui/avatar_image.vala | 267 --------- .../ui/call_window/participant_widget.vala | 6 +- main/src/ui/contact_details/dialog.vala | 4 +- .../call_widget.vala | 16 +- .../chat_state_populator.vala | 24 +- .../conversation_item_skeleton.vala | 17 +- .../quote_widget.vala | 4 +- .../conversation_selector_row.vala | 4 +- main/src/ui/global_search.vala | 12 +- main/src/ui/manage_accounts/account_row.vala | 4 +- main/src/ui/manage_accounts/dialog.vala | 6 +- main/src/ui/notifier_freedesktop.vala | 8 +- main/src/ui/notifier_gnotifications.vala | 8 +- main/src/ui/occupant_menu/list_row.vala | 8 +- main/src/ui/util/helper.vala | 77 --- main/src/ui/widgets/avatar_picture.vala | 519 ++++++++++++++++++ 33 files changed, 694 insertions(+), 645 deletions(-) delete mode 100644 main/src/ui/avatar_drawer.vala delete mode 100644 main/src/ui/avatar_generator.vala delete mode 100644 main/src/ui/avatar_image.vala create mode 100644 main/src/ui/widgets/avatar_picture.vala diff --git a/libdino/src/service/avatar_manager.vala b/libdino/src/service/avatar_manager.vala index b308aa2b..1296856b 100644 --- a/libdino/src/service/avatar_manager.vala +++ b/libdino/src/service/avatar_manager.vala @@ -12,6 +12,7 @@ public class AvatarManager : StreamInteractionModule, Object { public string id { get { return IDENTITY.id; } } public signal void received_avatar(Jid jid, Account account); + public signal void fetched_avatar(Jid jid, Account account); private enum Source { USER_AVATARS, @@ -25,6 +26,7 @@ public class AvatarManager : StreamInteractionModule, Object { private HashMap vcard_avatars = new HashMap(Jid.hash_func, Jid.equals_func); private HashMap cached_pixbuf = new HashMap(); private HashMap> pending_pixbuf = new HashMap>(); + private HashSet pending_fetch = new HashSet(); private const int MAX_PIXEL = 192; public static void start(StreamInteractor stream_interactor, Database db) { @@ -45,6 +47,18 @@ public class AvatarManager : StreamInteractionModule, Object { }); } + public File? get_avatar_file(Account account, Jid jid_) { + string? hash = get_avatar_hash(account, jid_); + if (hash == null) return null; + File file = File.new_for_path(Path.build_filename(folder, hash)); + if (!file.query_exists()) { + fetch_and_store_for_jid(account, jid_); + return null; + } else { + return file; + } + } + private string? get_avatar_hash(Account account, Jid jid_) { Jid jid = jid_; if (!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid_, account)) { @@ -59,6 +73,7 @@ public class AvatarManager : StreamInteractionModule, Object { } } + [Version (deprecated = true)] public bool has_avatar_cached(Account account, Jid jid) { string? hash = get_avatar_hash(account, jid); return hash != null && cached_pixbuf.has_key(hash); @@ -68,6 +83,7 @@ public class AvatarManager : StreamInteractionModule, Object { return get_avatar_hash(account, jid) != null; } + [Version (deprecated = true)] public Pixbuf? get_cached_avatar(Account account, Jid jid_) { string? hash = get_avatar_hash(account, jid_); if (hash == null) return null; @@ -75,6 +91,7 @@ public class AvatarManager : StreamInteractionModule, Object { return null; } + [Version (deprecated = true)] public async Pixbuf? get_avatar(Account account, Jid jid_) { Jid jid = jid_; if (!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid_, account)) { @@ -111,17 +128,7 @@ public class AvatarManager : StreamInteractionModule, Object { if (image != null) { cached_pixbuf[hash] = image; } else { - Bytes? bytes = null; - if (source == 1) { - bytes = yield Xmpp.Xep.UserAvatars.fetch_image(stream, jid, hash); - } else if (source == 2) { - bytes = yield Xmpp.Xep.VCard.fetch_image(stream, jid, hash); - if (bytes == null && jid.is_bare()) { - db.avatar.delete().with(db.avatar.jid_id, "=", db.get_jid_id(jid)).perform(); - } - } - if (bytes != null) { - store_image(hash, bytes); + if (yield fetch_and_store(stream, account, jid, source, hash)) { image = yield get_image(hash); } cached_pixbuf[hash] = image; @@ -162,7 +169,7 @@ public class AvatarManager : StreamInteractionModule, Object { ); foreach (var entry in get_avatar_hashes(account, Source.USER_AVATARS).entries) { - user_avatars[entry.key] = entry.value; + on_user_avatar_received(account, entry.key, entry.value); } foreach (var entry in get_avatar_hashes(account, Source.VCARD).entries) { @@ -172,7 +179,7 @@ public class AvatarManager : StreamInteractionModule, Object { continue; } - vcard_avatars[entry.key] = entry.value; + on_vcard_avatar_received(account, entry.key, entry.value); } } @@ -218,12 +225,53 @@ public class AvatarManager : StreamInteractionModule, Object { return ret; } - public void store_image(string id, Bytes data) { + public async bool fetch_and_store_for_jid(Account account, Jid jid) { + int source = -1; + string? hash = null; + if (user_avatars.has_key(jid)) { + hash = user_avatars[jid]; + source = 1; + } else if (vcard_avatars.has_key(jid)) { + hash = vcard_avatars[jid]; + source = 2; + } else { + return false; + } + + XmppStream? stream = stream_interactor.get_stream(account); + if (stream == null || !stream.negotiation_complete) return false; + + return yield fetch_and_store(stream, account, jid, source, hash); + } + + private async bool fetch_and_store(XmppStream stream, Account account, Jid jid, int source, string? hash) { + if (hash == null || pending_fetch.contains(hash)) return false; + + pending_fetch.add(hash); + Bytes? bytes = null; + if (source == 1) { + bytes = yield Xmpp.Xep.UserAvatars.fetch_image(stream, jid, hash); + } else if (source == 2) { + bytes = yield Xmpp.Xep.VCard.fetch_image(stream, jid, hash); + if (bytes == null && jid.is_bare()) { + db.avatar.delete().with(db.avatar.jid_id, "=", db.get_jid_id(jid)).perform(); + } + } + + if (bytes != null) { + yield store_image(hash, bytes); + fetched_avatar(jid, account); + } + pending_fetch.remove(hash); + return bytes != null; + } + + private async void store_image(string id, Bytes data) { File file = File.new_for_path(Path.build_filename(folder, id)); try { if (file.query_exists()) file.delete(); //TODO y? DataOutputStream fos = new DataOutputStream(file.create(FileCreateFlags.REPLACE_DESTINATION)); - fos.write_bytes_async.begin(data); + yield fos.write_bytes_async(data); } catch (Error e) { // Ignore: we failed in storing, so we refuse to display later... } diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 30b4a52f..9ca7ce81 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -124,8 +124,6 @@ SOURCES src/main.vala src/ui/application.vala - src/ui/avatar_drawer.vala - src/ui/avatar_image.vala src/ui/conversation_list_titlebar.vala src/ui/conversation_view.vala src/ui/conversation_view_controller.vala @@ -209,6 +207,7 @@ SOURCES src/ui/util/sizing_bin.vala src/ui/util/size_request_box.vala + src/ui/widgets/avatar_picture.vala src/ui/widgets/date_separator.vala src/ui/widgets/fixed_ratio_picture.vala src/ui/widgets/natural_size_increase.vala diff --git a/main/data/add_conversation/list_row.ui b/main/data/add_conversation/list_row.ui index c0d7e517..b8a97174 100644 --- a/main/data/add_conversation/list_row.ui +++ b/main/data/add_conversation/list_row.ui @@ -8,10 +8,9 @@ 3 10 - - False - 30 - 30 + + 30 + 30 center diff --git a/main/data/contact_details_dialog.ui b/main/data/contact_details_dialog.ui index 64a0e5fc..4802ae9a 100644 --- a/main/data/contact_details_dialog.ui +++ b/main/data/contact_details_dialog.ui @@ -30,10 +30,10 @@ 100 10 - - 50 - 50 - False + + 50 + 50 + center 0 0 diff --git a/main/data/conversation_item_widget.ui b/main/data/conversation_item_widget.ui index cd6c9269..3216232d 100644 --- a/main/data/conversation_item_widget.ui +++ b/main/data/conversation_item_widget.ui @@ -5,9 +5,9 @@ 7 2 - - 35 - 35 + + 35 + 35 start 2 diff --git a/main/data/conversation_row.ui b/main/data/conversation_row.ui index 2eb9071b..3bd5527d 100644 --- a/main/data/conversation_row.ui +++ b/main/data/conversation_row.ui @@ -14,9 +14,9 @@ 7 14 - - 35 - 35 + + 35 + 35 center diff --git a/main/data/manage_accounts/account_row.ui b/main/data/manage_accounts/account_row.ui index 845010a2..91891b91 100644 --- a/main/data/manage_accounts/account_row.ui +++ b/main/data/manage_accounts/account_row.ui @@ -10,9 +10,9 @@ 6 6 - - 40 - 40 + + 40 + 40 diff --git a/main/data/manage_accounts/dialog.ui b/main/data/manage_accounts/dialog.ui index 90a36b83..4931507c 100644 --- a/main/data/manage_accounts/dialog.ui +++ b/main/data/manage_accounts/dialog.ui @@ -93,11 +93,9 @@ - - 50 - 50 - - False + + 50 + 50 diff --git a/main/data/occupant_list_item.ui b/main/data/occupant_list_item.ui index 1915aee6..47e63bc9 100644 --- a/main/data/occupant_list_item.ui +++ b/main/data/occupant_list_item.ui @@ -8,9 +8,9 @@ 7 10 - - 30 - 30 + + 30 + 30 diff --git a/main/data/quote.ui b/main/data/quote.ui index a7c32ed8..277fc374 100644 --- a/main/data/quote.ui +++ b/main/data/quote.ui @@ -7,10 +7,9 @@ - - False - 15 - 15 + + 15 + 15 center 0 diff --git a/main/data/search_autocomplete.ui b/main/data/search_autocomplete.ui index a63bdce9..d607b192 100644 --- a/main/data/search_autocomplete.ui +++ b/main/data/search_autocomplete.ui @@ -3,14 +3,13 @@ horizontal - + 4 4 6 6 - 24 - 24 - False + 24 + 24 diff --git a/main/data/style.css b/main/data/style.css index fffee8a3..deac24fe 100644 --- a/main/data/style.css +++ b/main/data/style.css @@ -31,8 +31,8 @@ window.dino-main .dino-conversation viewport /* Some themes set this */ { } @keyframes highlight { - from { background: alpha(@warning_color, 0.5); } - to { background: transparent; } + from { background-color: alpha(@accent_color, 0.5); } + to { background-color: transparent; } } window.dino-main .dino-conversation .highlight-once { @@ -42,7 +42,7 @@ window.dino-main .dino-conversation .highlight-once { animation-name: highlight; } -window.dino-main .dino-conversation .message-box.highlight { +window.dino-main .dino-conversation .message-box.highlight:not(.highlight-once) { background: @window_bg_color; } @@ -119,6 +119,10 @@ window.dino-main .dino-quote:hover { background: alpha(@theme_fg_color, 0.08); } +picture.avatar { + border-radius: 3px; +} + /* Overlay Toolbar */ .dino-main .overlay-toolbar { diff --git a/main/src/ui/add_conversation/conference_list.vala b/main/src/ui/add_conversation/conference_list.vala index 14beaf92..0b630ae4 100644 --- a/main/src/ui/add_conversation/conference_list.vala +++ b/main/src/ui/add_conversation/conference_list.vala @@ -112,7 +112,7 @@ internal class ConferenceListRow : ListRow { via_label.visible = false; } - image.set_conversation(stream_interactor, new Conversation(jid, account, Conversation.Type.GROUPCHAT)); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(new Conversation(jid, account, Conversation.Type.GROUPCHAT)); } } diff --git a/main/src/ui/add_conversation/list_row.vala b/main/src/ui/add_conversation/list_row.vala index 5b3ec49a..c5e344d0 100644 --- a/main/src/ui/add_conversation/list_row.vala +++ b/main/src/ui/add_conversation/list_row.vala @@ -9,7 +9,7 @@ namespace Dino.Ui { public class ListRow : Widget { public Grid outer_grid; - public AvatarImage image; + public AvatarPicture picture; public Label name_label; public Label via_label; @@ -19,7 +19,7 @@ public class ListRow : Widget { construct { Builder builder = new Builder.from_resource("/im/dino/Dino/add_conversation/list_row.ui"); outer_grid = (Grid) builder.get_object("outer_grid"); - image = (AvatarImage) builder.get_object("image"); + picture = (AvatarPicture) builder.get_object("picture"); name_label = (Label) builder.get_object("name_label"); via_label = (Label) builder.get_object("via_label"); @@ -45,7 +45,7 @@ public class ListRow : Widget { via_label.visible = false; } name_label.label = display_name; - image.set_conversation(stream_interactor, conv); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conv); } public override void dispose() { diff --git a/main/src/ui/add_conversation/select_jid_fragment.vala b/main/src/ui/add_conversation/select_jid_fragment.vala index 25b0b11f..e0682e29 100644 --- a/main/src/ui/add_conversation/select_jid_fragment.vala +++ b/main/src/ui/add_conversation/select_jid_fragment.vala @@ -132,7 +132,7 @@ public class SelectJidFragment : Gtk.Box { } else { via_label.visible = false; } - image.set_text("?"); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add("+"); } } } diff --git a/main/src/ui/avatar_drawer.vala b/main/src/ui/avatar_drawer.vala deleted file mode 100644 index c14d7fda..00000000 --- a/main/src/ui/avatar_drawer.vala +++ /dev/null @@ -1,193 +0,0 @@ -using Cairo; -using Gee; -using Gdk; -using Gtk; -using Xmpp.Util; - -namespace Dino.Ui { - -public class AvatarDrawer { - public const string GRAY = "555753"; - - private Gee.List tiles = new ArrayList(); - private int height = 35; - private int width = 35; - private bool gray; - private int base_factor = 1; - private string font_family = "Sans"; - - public AvatarDrawer size(int height, int width = height) { - this.height = height; - this.width = width; - return this; - } - - public AvatarDrawer grayscale() { - this.gray = true; - return this; - } - - public AvatarDrawer tile(Pixbuf? image, string? name, string? hex_color) { - tiles.add(new AvatarTile(image, name, hex_color)); - return this; - } - - public AvatarDrawer plus() { - tiles.add(new AvatarTile(null, "…", GRAY)); - return this; - } - - public AvatarDrawer scale(int base_factor) { - this.base_factor = base_factor; - return this; - } - - public AvatarDrawer font(string font_family) { - this.font_family = font_family; - return this; - } - - public ImageSurface draw_image_surface() { - ImageSurface surface = new ImageSurface(Format.ARGB32, width, height); - draw_on_context(new Context(surface)); - return surface; - } - - public void draw_on_context(Cairo.Context ctx) { - double radius = 3 * base_factor; - double degrees = Math.PI / 180.0; - ctx.new_sub_path(); - ctx.arc(width - radius, radius, radius, -90 * degrees, 0 * degrees); - ctx.arc(width - radius, height - radius, radius, 0 * degrees, 90 * degrees); - ctx.arc(radius, height - radius, radius, 90 * degrees, 180 * degrees); - ctx.arc(radius, radius, radius, 180 * degrees, 270 * degrees); - ctx.close_path(); - ctx.clip(); - - if (this.tiles.size == 4) { - Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height); - Cairo.Context bufctx = new Cairo.Context(buffer); - bufctx.scale(0.5, 0.5); - bufctx.set_source_surface(sub_surface_idx(ctx, 0, width - 1, height - 1, 2 * base_factor), 0, 0); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(ctx, 1, width - 1, height - 1, 2 * base_factor), width + 1, 0); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(ctx, 2, width - 1, height - 1, 2 * base_factor), 0, height + 1); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(ctx, 3, width - 1, height - 1, 2 * base_factor), width + 1, height + 1); - bufctx.paint(); - - ctx.set_source_surface(buffer, 0, 0); - ctx.paint(); - } else if (this.tiles.size == 3) { - Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height); - Cairo.Context bufctx = new Cairo.Context(buffer); - bufctx.scale(0.5, 0.5); - bufctx.set_source_surface(sub_surface_idx(ctx, 0, width - 1, height - 1, 2 * base_factor), 0, 0); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(ctx, 1, width - 1, height * 2, 2 * base_factor), width + 1, 0); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(ctx, 2, width - 1, height - 1, 2 * base_factor), 0, height + 1); - bufctx.paint(); - - ctx.set_source_surface(buffer, 0, 0); - ctx.paint(); - } else if (this.tiles.size == 2) { - Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height); - Cairo.Context bufctx = new Cairo.Context(buffer); - bufctx.scale(0.5, 0.5); - bufctx.set_source_surface(sub_surface_idx(ctx, 0, width - 1, height * 2, 2 * base_factor), 0, 0); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(ctx, 1, width - 1, height * 2, 2 * base_factor), width + 1, 0); - bufctx.paint(); - - ctx.set_source_surface(buffer, 0, 0); - ctx.paint(); - } else if (this.tiles.size == 1) { - ctx.set_source_surface(sub_surface_idx(ctx, 0, width, height, base_factor), 0, 0); - ctx.paint(); - } else if (this.tiles.size == 0) { - ctx.set_source_surface(sub_surface_idx(ctx, -1, width, height, base_factor), 0, 0); - ctx.paint(); - } - - if (gray) { - // convert to greyscale - ctx.set_operator(Cairo.Operator.HSL_COLOR); - ctx.set_source_rgb(1, 1, 1); - ctx.rectangle(0, 0, width, height); - ctx.fill(); - // make the visible part more light - ctx.set_operator(Cairo.Operator.ATOP); - ctx.set_source_rgba(1, 1, 1, 0.7); - ctx.rectangle(0, 0, width, height); - ctx.fill(); - } - ctx.set_source_rgb(0, 0, 0); - } - - private Cairo.Surface sub_surface_idx(Cairo.Context ctx, int idx, int width, int height, int font_factor = 1) { - Gdk.Pixbuf? avatar = idx >= 0 ? tiles[idx].image : null; - string? name = idx >= 0 ? tiles[idx].name : ""; - string hex_color = !gray && idx >= 0 ? tiles[idx].hex_color : GRAY; - return sub_surface(ctx, font_family, avatar, name, hex_color, width, height, font_factor); - } - - private static Cairo.Surface sub_surface(Cairo.Context ctx, string font_family, Gdk.Pixbuf? avatar, string? name, string? hex_color, int width, int height, int font_factor = 1) { - Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height); - Cairo.Context bufctx = new Cairo.Context(buffer); - if (avatar == null) { - set_source_hex_color(bufctx, hex_color ?? GRAY); - bufctx.rectangle(0, 0, width, height); - bufctx.fill(); - - string text = name == null ? "…" : name.get_char(0).toupper().to_string(); - bufctx.select_font_face(font_family, Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL); - bufctx.set_font_size(width / font_factor < 40 ? font_factor * 17 : font_factor * 25); - Cairo.TextExtents extents; - bufctx.text_extents(text, out extents); - double x_pos = width/2 - (extents.width/2 + extents.x_bearing); - double y_pos = height/2 - (extents.height/2 + extents.y_bearing); - bufctx.move_to(x_pos, y_pos); - bufctx.set_source_rgba(1, 1, 1, 1); - bufctx.show_text(text); - } else { - double w_scale = (double) width / avatar.width; - double h_scale = (double) height / avatar.height; - double scale = double.max(w_scale, h_scale); - bufctx.scale(scale, scale); - - double x_off = 0, y_off = 0; - if (scale == h_scale) { - x_off = (width / scale - avatar.width) / 2.0; - } else { - y_off = (height / scale - avatar.height) / 2.0; - } - Gdk.cairo_set_source_pixbuf(bufctx, avatar, x_off, y_off); - bufctx.get_source().set_filter(Cairo.Filter.BEST); - bufctx.paint(); - } - return buffer; - } - - private static void set_source_hex_color(Cairo.Context ctx, string hex_color) { - ctx.set_source_rgba((double) from_hex(hex_color.substring(0, 2)) / 255, - (double) from_hex(hex_color.substring(2, 2)) / 255, - (double) from_hex(hex_color.substring(4, 2)) / 255, - hex_color.length > 6 ? (double) from_hex(hex_color.substring(6, 2)) / 255 : 1); - } -} - -private class AvatarTile { - public Pixbuf? image { get; private set; } - public string? name { get; private set; } - public string? hex_color { get; private set; } - - public AvatarTile(Pixbuf? image, string? name, string? hex_color) { - this.image = image; - this.name = name; - this.hex_color = hex_color; - } -} - -} \ No newline at end of file diff --git a/main/src/ui/avatar_generator.vala b/main/src/ui/avatar_generator.vala deleted file mode 100644 index e69de29b..00000000 diff --git a/main/src/ui/avatar_image.vala b/main/src/ui/avatar_image.vala deleted file mode 100644 index f348dd4b..00000000 --- a/main/src/ui/avatar_image.vala +++ /dev/null @@ -1,267 +0,0 @@ -using Gtk; -using Dino.Entities; -using Xmpp; -using Xmpp.Util; - -namespace Dino.Ui { - -public class AvatarImage : Widget { - public int height { get; set; default = 35; } - public int width { get; set; default = 35; } - public bool allow_gray { get; set; default = true; } - public bool force_gray { get; set; default = false; } - public StreamInteractor? stream_interactor { get; set; } - public AvatarManager? avatar_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(AvatarManager.IDENTITY); } } - public MucManager? muc_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(MucManager.IDENTITY); } } - public PresenceManager? presence_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(PresenceManager.IDENTITY); } } - public ConnectionManager? connection_manager { owned get { return stream_interactor == null ? null : stream_interactor.connection_manager; } } - public Account account { get { return conversation.account; } } - private AvatarDrawer? drawer; - private Conversation conversation; - private Jid[] jids; - private Cairo.ImageSurface? cached_surface; - private static int8 use_image_surface = -1; - - public AvatarImage() { - can_focus = false; - add_css_class("avatar"); - } - - public override void dispose() { - base.dispose(); - drawer = null; - cached_surface = null; - disconnect_stream_interactor(); - } - - public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { - if (orientation == Orientation.HORIZONTAL) { - minimum = width; - natural = width; - } else { - minimum = height; - natural = height; - } - minimum_baseline = natural_baseline = -1; - } - - public override void snapshot(Snapshot snapshot) { - Cairo.Context context = snapshot.append_cairo(Graphene.Rect.alloc().init(0, 0, width, height)); - draw(context); - } - - public bool draw(Cairo.Context ctx_in) { - Cairo.Context ctx = ctx_in; - int width = this.width, height = this.height, base_factor = 1; - if (use_image_surface == -1) { - // TODO: detect if we have to buffer in image surface - use_image_surface = 1; - } - if (use_image_surface == 1) { - ctx_in.scale(1f / scale_factor, 1f / scale_factor); - if (cached_surface != null) { - ctx_in.set_source_surface(cached_surface, 0, 0); - ctx_in.paint(); - ctx_in.set_source_rgb(0, 0, 0); - return true; - } - width *= scale_factor; - height *= scale_factor; - base_factor *= scale_factor; - cached_surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height); - ctx = new Cairo.Context(cached_surface); - } - - AvatarDrawer drawer = this.drawer; - Jid[] jids = this.jids; - if (drawer == null && jids.length == 0) { - switch (conversation.type_) { - case Conversation.Type.CHAT: - case Conversation.Type.GROUPCHAT_PM: - // In direct chats or group chats, conversation avatar is same as counterpart avatar - jids = { conversation.counterpart }; - break; - case Conversation.Type.GROUPCHAT: - string user_color = Util.get_avatar_hex_color(stream_interactor, account, conversation.counterpart, conversation); - if (avatar_manager.has_avatar_cached(account, conversation.counterpart)) { - drawer = new AvatarDrawer().tile(avatar_manager.get_cached_avatar(account, conversation.counterpart), "#", user_color); - if (force_gray || allow_gray && (!is_self_online() || !is_counterpart_online())) drawer.grayscale(); - } else { - Gee.List? occupants = muc_manager.get_other_offline_members(conversation.counterpart, account); - if (muc_manager.is_private_room(account, conversation.counterpart) && occupants != null && occupants.size > 0) { - jids = occupants.to_array(); - } else { - drawer = new AvatarDrawer().tile(null, "#", user_color); - if (force_gray || allow_gray && (!is_self_online() || !is_counterpart_online())) drawer.grayscale(); - } - try_load_avatar_async(conversation.counterpart); - } - break; - } - } - if (drawer == null && jids.length > 0) { - drawer = new AvatarDrawer(); - for (int i = 0; i < (jids.length <= 4 ? jids.length : 3); i++) { - Jid avatar_jid = jids[i]; - Jid? real_avatar_jid = null; - if (conversation.type_ != Conversation.Type.CHAT && avatar_jid.equals_bare(conversation.counterpart) && muc_manager.is_private_room(account, conversation.counterpart.bare_jid)) { - // In private room, consider real jid - real_avatar_jid = muc_manager.get_real_jid(avatar_jid, account) ?? avatar_jid; - } - string display_name = Util.get_participant_display_name(stream_interactor, conversation, jids[i]); - string user_color = Util.get_avatar_hex_color(stream_interactor, account, jids[i], conversation); - if (avatar_manager.has_avatar_cached(account, avatar_jid)) { - drawer.tile(avatar_manager.get_cached_avatar(account, avatar_jid), display_name, user_color); - } else if (real_avatar_jid != null && avatar_manager.has_avatar_cached(account, real_avatar_jid)) { - drawer.tile(avatar_manager.get_cached_avatar(account, real_avatar_jid), display_name, user_color); - } else { - drawer.tile(null, display_name, user_color); - try_load_avatar_async(avatar_jid); - if (real_avatar_jid != null) try_load_avatar_async(real_avatar_jid); - } - } - if (jids.length > 4) { - drawer.plus(); - } - if (force_gray || allow_gray && (!is_self_online() || !is_counterpart_online())) drawer.grayscale(); - } - - - if (drawer == null) return false; - drawer.size(height, width) - .scale(base_factor) - .font(get_pango_context().get_font_description().get_family()) - .draw_on_context(ctx); - - if (use_image_surface == 1) { - ctx_in.set_source_surface(ctx.get_target(), 0, 0); - ctx_in.paint(); - ctx_in.set_source_rgb(0, 0, 0); - } - - return true; - } - - private void try_load_avatar_async(Jid jid) { - if (avatar_manager.has_avatar(account, jid)) { - avatar_manager.get_avatar.begin(account, jid, (_, res) => { - var avatar = avatar_manager.get_avatar.end(res); - if (avatar != null) force_redraw(); - }); - } - } - - private void force_redraw() { - this.cached_surface = null; - queue_draw(); - } - - private void disconnect_stream_interactor() { - if (stream_interactor != null) { - presence_manager.show_received.disconnect(on_show_received); - presence_manager.received_offline_presence.disconnect(on_show_received); - avatar_manager.received_avatar.disconnect(on_received_avatar); - stream_interactor.connection_manager.connection_state_changed.disconnect(on_connection_changed); - stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.disconnect(on_roster_updated); - muc_manager.private_room_occupant_updated.disconnect(on_private_room_occupant_updated); - muc_manager.room_info_updated.disconnect(on_room_info_updated); - stream_interactor = null; - } - } - - private void on_show_received(Jid jid, Account account) { - if (!account.equals(this.account)) return; - update_avatar_if_jid(jid); - } - - private void on_received_avatar(Jid jid, Account account) { - if (!account.equals(this.account)) return; - update_avatar_if_jid(jid); - } - - private void update_avatar_if_jid(Jid jid) { - if (jid.equals_bare(this.conversation.counterpart)) { - force_redraw(); - return; - } - foreach (Jid ours in this.jids) { - if (jid.equals_bare(ours)) { - force_redraw(); - return; - } - } - } - - private void on_connection_changed(Account account, ConnectionManager.ConnectionState state) { - if (!account.equals(this.account)) return; - force_redraw(); - } - - private void on_roster_updated(Account account, Jid jid, Roster.Item roster_item) { - if (!account.equals(this.account)) return; - update_avatar_if_jid(jid); - } - - private void on_private_room_occupant_updated(Account account, Jid room, Jid occupant) { - if (!account.equals(this.account)) return; - update_avatar_if_jid(room); - } - - private void on_room_info_updated(Account account, Jid muc_jid) { - if (!account.equals(this.account)) return; - update_avatar_if_jid(muc_jid); - } - - private bool is_self_online() { - if (connection_manager != null) { - return connection_manager.get_state(account) == ConnectionManager.ConnectionState.CONNECTED; - } - return false; - } - - private bool is_counterpart_online() { - return presence_manager.get_full_jids(conversation.counterpart, account) != null; - } - - public void set_conversation(StreamInteractor stream_interactor, Conversation conversation) { - set_avatar(stream_interactor, conversation, new Jid[0]); - } - - public void set_conversation_participant(StreamInteractor stream_interactor, Conversation conversation, Jid sub_jid) { - set_avatar(stream_interactor, conversation, new Jid[] {sub_jid}); - } - - public void set_conversation_participants(StreamInteractor stream_interactor, Conversation conversation, Jid[] sub_jids) { - set_avatar(stream_interactor, conversation, sub_jids); - } - - private void set_avatar(StreamInteractor stream_interactor, Conversation conversation, Jid[] jids) { - if (this.stream_interactor != null && stream_interactor != this.stream_interactor) { - disconnect_stream_interactor(); - } - if (this.stream_interactor != stream_interactor) { - this.stream_interactor = stream_interactor; - presence_manager.show_received.connect(on_show_received); - presence_manager.received_offline_presence.connect(on_show_received); - stream_interactor.get_module(AvatarManager.IDENTITY).received_avatar.connect(on_received_avatar); - stream_interactor.connection_manager.connection_state_changed.connect(on_connection_changed); - stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.connect(on_roster_updated); - muc_manager.private_room_occupant_updated.connect(on_private_room_occupant_updated); - muc_manager.room_info_updated.connect(on_room_info_updated); - } - this.cached_surface = null; - this.conversation = conversation; - this.jids = jids; - - force_redraw(); - } - - public void set_text(string text, bool gray = true) { - disconnect_stream_interactor(); - this.drawer = new AvatarDrawer().tile(null, text, null); - if (gray) drawer.grayscale(); - force_redraw(); - } -} - -} diff --git a/main/src/ui/call_window/participant_widget.vala b/main/src/ui/call_window/participant_widget.vala index 180923f1..8ec1f5ea 100644 --- a/main/src/ui/call_window/participant_widget.vala +++ b/main/src/ui/call_window/participant_widget.vala @@ -96,11 +96,11 @@ namespace Dino.Ui { shows_video = false; Box box = new Box(Orientation.HORIZONTAL, 0); box.add_css_class("video-placeholder-box"); - AvatarImage avatar = new AvatarImage() { allow_gray=false, hexpand=true, vexpand=true, halign=Align.CENTER, valign=Align.CENTER, height=100, width=100 }; + AvatarPicture avatar = new AvatarPicture() { hexpand=true, vexpand=true, halign=Align.CENTER, valign=Align.CENTER, height_request=100, width_request=100 }; if (conversation != null) { - avatar.set_conversation(stream_interactor, conversation); + avatar.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conversation); } else { - avatar.set_text("?", false); + avatar.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add("?"); } box.append(avatar); diff --git a/main/src/ui/contact_details/dialog.vala b/main/src/ui/contact_details/dialog.vala index 134bb559..c897fe4e 100644 --- a/main/src/ui/contact_details/dialog.vala +++ b/main/src/ui/contact_details/dialog.vala @@ -10,7 +10,7 @@ namespace Dino.Ui.ContactDetails { [GtkTemplate (ui = "/im/dino/Dino/contact_details_dialog.ui")] public class Dialog : Gtk.Dialog { - [GtkChild] public unowned AvatarImage avatar; + [GtkChild] public unowned AvatarPicture avatar; [GtkChild] public unowned Util.EntryLabelHybrid name_hybrid; [GtkChild] public unowned Label name_label; [GtkChild] public unowned Label jid_label; @@ -87,7 +87,7 @@ public class Dialog : Gtk.Dialog { } jid_label.label = conversation.counterpart.to_string(); account_label.label = "via " + conversation.account.bare_jid.to_string(); - avatar.set_conversation(stream_interactor, conversation); + avatar.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conversation); } private void add_entry(string category, string label, string? description, Object wo) { diff --git a/main/src/ui/conversation_content_view/call_widget.vala b/main/src/ui/conversation_content_view/call_widget.vala index 4f7e2953..ab047196 100644 --- a/main/src/ui/conversation_content_view/call_widget.vala +++ b/main/src/ui/conversation_content_view/call_widget.vala @@ -94,15 +94,15 @@ namespace Dino.Ui { } foreach (Jid counterpart in call.counterparts) { - AvatarImage image = new AvatarImage() { force_gray=true, margin_top=2 }; - image.set_conversation_participant(stream_interactor, conversation, counterpart.bare_jid); - multiparty_peer_box.append(image); - multiparty_peer_widgets.add(image); + AvatarPicture picture = new AvatarPicture() { margin_top=2 }; + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(conversation, counterpart.bare_jid); + multiparty_peer_box.append(picture); + multiparty_peer_widgets.add(picture); } - AvatarImage image2 = new AvatarImage() { force_gray=true, margin_top=2 }; - image2.set_conversation_participant(stream_interactor, conversation, call.account.bare_jid); - multiparty_peer_box.append(image2); - multiparty_peer_widgets.add(image2); + AvatarPicture picture2 = new AvatarPicture() { margin_top=2 }; + picture2.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(conversation, call.account.bare_jid); + multiparty_peer_box.append(picture2); + multiparty_peer_widgets.add(picture2); outer_additional_box.add_css_class("multiparty-participants"); diff --git a/main/src/ui/conversation_content_view/chat_state_populator.vala b/main/src/ui/conversation_content_view/chat_state_populator.vala index 803739c8..2f02c635 100644 --- a/main/src/ui/conversation_content_view/chat_state_populator.vala +++ b/main/src/ui/conversation_content_view/chat_state_populator.vala @@ -68,7 +68,7 @@ private class MetaChatStateItem : Plugins.MetaConversationItem { private Conversation conversation; private Gee.List jids = new ArrayList(); private Label label; - private AvatarImage image; + private AvatarPicture picture; public MetaChatStateItem(StreamInteractor stream_interactor, Conversation conversation, Gee.List jids) { this.stream_interactor = stream_interactor; @@ -79,10 +79,10 @@ private class MetaChatStateItem : Plugins.MetaConversationItem { public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType widget_type) { label = new Label("") { xalign=0, vexpand=true }; label.add_css_class("dim-label"); - image = new AvatarImage() { margin_top=2, valign=Align.START }; + picture = new AvatarPicture() { margin_top=2, valign=Align.START }; Box image_content_box = new Box(Orientation.HORIZONTAL, 8); - image_content_box.append(image); + image_content_box.append(picture); image_content_box.append(label); update(); @@ -97,9 +97,7 @@ private class MetaChatStateItem : Plugins.MetaConversationItem { } private void update() { - if (image == null || label == null) return; - - image.set_conversation_participants(stream_interactor, conversation, jids.to_array()); + if (picture == null || label == null) return; Gee.List display_names = new ArrayList(); foreach (Jid jid in jids) { @@ -108,12 +106,26 @@ private class MetaChatStateItem : Plugins.MetaConversationItem { string new_text = ""; if (jids.size > 3) { new_text = _("%s, %s and %i others are typing…").printf(display_names[0], display_names[1], jids.size - 2); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor) + .add_participant(conversation, jids[0]) + .add_participant(conversation, jids[1]) + .add_participant(conversation, jids[2]) + .add("+"); } else if (jids.size == 3) { new_text = _("%s, %s and %s are typing…").printf(display_names[0], display_names[1], display_names[2]); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor) + .add_participant(conversation, jids[0]) + .add_participant(conversation, jids[1]) + .add_participant(conversation, jids[2]); } else if (jids.size == 2) { new_text =_("%s and %s are typing…").printf(display_names[0], display_names[1]); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor) + .add_participant(conversation, jids[0]) + .add_participant(conversation, jids[1]); } else { new_text = _("%s is typing…").printf(display_names[0]); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor) + .add_participant(conversation, jids[0]); } label.label = new_text; diff --git a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala index bbde76b1..5d86f6c7 100644 --- a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala +++ b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala @@ -12,7 +12,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, public Grid main_grid { get; set; } public Label name_label { get; set; } public Label time_label { get; set; } - public AvatarImage avatar_image { get; set; } + public AvatarPicture avatar_picture { get; set; } public Image encryption_image { get; set; } public Image received_image { get; set; } @@ -51,7 +51,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, main_grid.add_css_class("message-box"); name_label = (Label) builder.get_object("name_label"); time_label = (Label) builder.get_object("time_label"); - avatar_image = (AvatarImage) builder.get_object("avatar_image"); + avatar_picture = (AvatarPicture) builder.get_object("avatar_picture"); encryption_image = (Image) builder.get_object("encrypted_image"); received_image = (Image) builder.get_object("marked_image"); @@ -62,7 +62,8 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, } if (item.requires_header) { - avatar_image.set_conversation_participant(stream_interactor, conversation, item.jid); + // TODO: For MUC messags, use real jid from message if known + avatar_picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(conversation, item.jid); } this.notify["show-skeleton"].connect(update_margin); @@ -116,7 +117,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, } private void update_margin() { - avatar_image.visible = show_skeleton; + avatar_picture.visible = show_skeleton; name_label.visible = show_skeleton; time_label.visible = show_skeleton; encryption_image.visible = show_skeleton; @@ -286,10 +287,10 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, time_label.dispose(); time_label = null; } - if (avatar_image != null) { - avatar_image.unparent(); - avatar_image.dispose(); - avatar_image = null; + if (avatar_picture != null) { + avatar_picture.unparent(); + avatar_picture.dispose(); + avatar_picture = null; } if (encryption_image != null) { encryption_image.unparent(); diff --git a/main/src/ui/conversation_content_view/quote_widget.vala b/main/src/ui/conversation_content_view/quote_widget.vala index 6dbf459c..23b62e6a 100644 --- a/main/src/ui/conversation_content_view/quote_widget.vala +++ b/main/src/ui/conversation_content_view/quote_widget.vala @@ -61,13 +61,13 @@ namespace Dino.Ui.Quote { public Widget get_widget(Model model) { Builder builder = new Builder.from_resource("/im/dino/Dino/quote.ui"); - AvatarImage avatar = (AvatarImage) builder.get_object("avatar"); + AvatarPicture avatar = (AvatarPicture) builder.get_object("avatar"); Label author = (Label) builder.get_object("author"); Label time = (Label) builder.get_object("time"); Label message = (Label) builder.get_object("message"); Button abort_button = (Button) builder.get_object("abort-button"); - avatar.set_conversation_participant(model.stream_interactor, model.conversation, model.author_jid); + avatar.model = new ViewModel.CompatAvatarPictureModel(model.stream_interactor).add_participant(model.conversation, model.author_jid); model.bind_property("display-name", author, "label", BindingFlags.SYNC_CREATE); model.bind_property("display-time", time, "label", BindingFlags.SYNC_CREATE); model.bind_property("message", message, "label", BindingFlags.SYNC_CREATE); diff --git a/main/src/ui/conversation_selector/conversation_selector_row.vala b/main/src/ui/conversation_selector/conversation_selector_row.vala index e71176aa..6ef61b3c 100644 --- a/main/src/ui/conversation_selector/conversation_selector_row.vala +++ b/main/src/ui/conversation_selector/conversation_selector_row.vala @@ -12,7 +12,7 @@ namespace Dino.Ui { [GtkTemplate (ui = "/im/dino/Dino/conversation_row.ui")] public class ConversationSelectorRow : ListBoxRow { - [GtkChild] protected unowned AvatarImage image; + [GtkChild] protected unowned AvatarPicture picture; [GtkChild] protected unowned Label name_label; [GtkChild] protected unowned Label time_label; [GtkChild] protected unowned Label nick_label; @@ -101,7 +101,7 @@ public class ConversationSelectorRow : ListBoxRow { x_button.clicked.connect(() => { stream_interactor.get_module(ConversationManager.IDENTITY).close_conversation(conversation); }); - image.set_conversation(stream_interactor, conversation); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conversation); conversation.notify["read-up-to-item"].connect(() => update_read()); conversation.notify["pinned"].connect(() => { update_pinned_icon(); }); diff --git a/main/src/ui/global_search.vala b/main/src/ui/global_search.vala index d206220d..6872f631 100644 --- a/main/src/ui/global_search.vala +++ b/main/src/ui/global_search.vala @@ -116,15 +116,15 @@ public class GlobalSearch { // Populate new suggestions foreach(SearchSuggestion suggestion in suggestions) { Builder builder = new Builder.from_resource("/im/dino/Dino/search_autocomplete.ui"); - AvatarImage avatar = (AvatarImage)builder.get_object("image"); + AvatarPicture avatar = (AvatarPicture)builder.get_object("picture"); Label label = (Label)builder.get_object("label"); string display_name; if (suggestion.conversation.type_ == Conversation.Type.GROUPCHAT && !suggestion.conversation.counterpart.equals(suggestion.jid) || suggestion.conversation.type_ == Conversation.Type.GROUPCHAT_PM) { display_name = Util.get_participant_display_name(stream_interactor, suggestion.conversation, suggestion.jid); - avatar.set_conversation_participant(stream_interactor, suggestion.conversation, suggestion.jid); + avatar.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(suggestion.conversation, suggestion.jid); } else { display_name = Util.get_conversation_display_name(stream_interactor, suggestion.conversation); - avatar.set_conversation(stream_interactor, suggestion.conversation); + avatar.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(suggestion.conversation); } if (display_name != suggestion.jid.to_string()) { label.set_markup("%s %s".printf(Markup.escape_text(display_name), Markup.escape_text(suggestion.jid.to_string()))); @@ -289,10 +289,10 @@ public class GlobalSearch { } private Grid get_skeleton(MessageItem item) { - AvatarImage image = new AvatarImage() { height=32, width=32, margin_end=7, valign=Align.START, allow_gray = false }; - image.set_conversation_participant(stream_interactor, item.conversation, item.jid); + AvatarPicture picture = new AvatarPicture() { height_request=32, width_request=32, margin_end=7, valign=Align.START }; + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(item.conversation, item.jid); Grid grid = new Grid() { row_homogeneous=false }; - grid.attach(image, 0, 0, 1, 2); + grid.attach(picture, 0, 0, 1, 2); string display_name = Util.get_participant_display_name(stream_interactor, item.conversation, item.jid); Label name_label = new Label(display_name) { ellipsize=EllipsizeMode.END, xalign=0 }; diff --git a/main/src/ui/manage_accounts/account_row.vala b/main/src/ui/manage_accounts/account_row.vala index b3a33eae..ae734b83 100644 --- a/main/src/ui/manage_accounts/account_row.vala +++ b/main/src/ui/manage_accounts/account_row.vala @@ -7,7 +7,7 @@ namespace Dino.Ui.ManageAccounts { [GtkTemplate (ui = "/im/dino/Dino/manage_accounts/account_row.ui")] public class AccountRow : Gtk.ListBoxRow { - [GtkChild] public unowned AvatarImage image; + [GtkChild] public unowned AvatarPicture picture; [GtkChild] public unowned Label jid_label; [GtkChild] public unowned Image icon; @@ -17,7 +17,7 @@ public class AccountRow : Gtk.ListBoxRow { public AccountRow(StreamInteractor stream_interactor, Account account) { this.stream_interactor = stream_interactor; this.account = account; - image.set_conversation(stream_interactor, new Conversation(account.bare_jid, account, Conversation.Type.CHAT)); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(new Conversation(account.bare_jid, account, Conversation.Type.CHAT), account.bare_jid); jid_label.set_label(account.bare_jid.to_string()); stream_interactor.connection_manager.connection_error.connect((account, error) => { diff --git a/main/src/ui/manage_accounts/dialog.vala b/main/src/ui/manage_accounts/dialog.vala index 0a37b052..a326aeff 100644 --- a/main/src/ui/manage_accounts/dialog.vala +++ b/main/src/ui/manage_accounts/dialog.vala @@ -19,7 +19,7 @@ public class Dialog : Gtk.Dialog { [GtkChild] public unowned Button no_accounts_add; [GtkChild] public unowned Button add_account_button; [GtkChild] public unowned Button remove_account_button; - [GtkChild] public unowned AvatarImage image; + [GtkChild] public unowned AvatarPicture picture; [GtkChild] public unowned Button image_button; [GtkChild] public unowned Label jid_label; [GtkChild] public unowned Label state_label; @@ -178,14 +178,14 @@ public class Dialog : Gtk.Dialog { private void on_received_avatar(Jid jid, Account account) { if (selected_account.equals(account) && jid.equals(account.bare_jid)) { - image.set_conversation(stream_interactor, new Conversation(account.bare_jid, account, Conversation.Type.CHAT)); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(new Conversation(account.bare_jid, account, Conversation.Type.CHAT), account.bare_jid); } } private void populate_grid_data(Account account) { active_switch.state_set.disconnect(change_account_state); - image.set_conversation(stream_interactor, new Conversation(account.bare_jid, account, Conversation.Type.CHAT)); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(new Conversation(account.bare_jid, account, Conversation.Type.CHAT), account.bare_jid); active_switch.set_active(account.enabled); jid_label.label = account.bare_jid.to_string(); diff --git a/main/src/ui/notifier_freedesktop.vala b/main/src/ui/notifier_freedesktop.vala index b6b31d34..8af96975 100644 --- a/main/src/ui/notifier_freedesktop.vala +++ b/main/src/ui/notifier_freedesktop.vala @@ -287,8 +287,12 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { } private async Variant get_conversation_icon(Conversation conversation) { - AvatarDrawer drawer = yield Util.get_conversation_avatar_drawer(stream_interactor, conversation); - Cairo.ImageSurface surface = drawer.size(40, 40).draw_image_surface(); + CompatAvatarDrawer drawer = new CompatAvatarDrawer() { + model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conversation), + width_request = 40, + height_request = 40 + }; + Cairo.ImageSurface surface = drawer.draw_image_surface(); Gdk.Pixbuf avatar = Gdk.pixbuf_get_from_surface(surface, 0, 0, surface.get_width(), surface.get_height()); var bytes = avatar.pixel_bytes; var image_bytes = Variant.new_from_data(new VariantType("ay"), bytes.get_data(), true, bytes); diff --git a/main/src/ui/notifier_gnotifications.vala b/main/src/ui/notifier_gnotifications.vala index d569a358..90c8ca8c 100644 --- a/main/src/ui/notifier_gnotifications.vala +++ b/main/src/ui/notifier_gnotifications.vala @@ -171,8 +171,12 @@ namespace Dino.Ui { } private async Icon get_conversation_icon(Conversation conversation) throws Error { - AvatarDrawer drawer = yield Util.get_conversation_avatar_drawer(stream_interactor, conversation); - Cairo.ImageSurface surface = drawer.size(40, 40).draw_image_surface(); + CompatAvatarDrawer drawer = new CompatAvatarDrawer() { + model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conversation), + width_request = 40, + height_request = 40 + }; + Cairo.ImageSurface surface = drawer.draw_image_surface(); Gdk.Pixbuf avatar = Gdk.pixbuf_get_from_surface(surface, 0, 0, surface.get_width(), surface.get_height()); uint8[] buffer; avatar.save_to_buffer(out buffer, "png"); diff --git a/main/src/ui/occupant_menu/list_row.vala b/main/src/ui/occupant_menu/list_row.vala index 6b43fe7f..476d9e61 100644 --- a/main/src/ui/occupant_menu/list_row.vala +++ b/main/src/ui/occupant_menu/list_row.vala @@ -8,7 +8,7 @@ namespace Dino.Ui.OccupantMenu { public class ListRow : Object { private Grid main_grid; - private AvatarImage image; + private AvatarPicture picture; public Label name_label; public Conversation? conversation; @@ -17,7 +17,7 @@ public class ListRow : Object { construct { Builder builder = new Builder.from_resource("/im/dino/Dino/occupant_list_item.ui"); main_grid = (Grid) builder.get_object("main_grid"); - image = (AvatarImage) builder.get_object("image"); + picture = (AvatarPicture) builder.get_object("picture"); name_label = (Label) builder.get_object("name_label"); } @@ -26,12 +26,12 @@ public class ListRow : Object { this.jid = jid; name_label.label = Util.get_participant_display_name(stream_interactor, conversation, jid); - image.set_conversation_participant(stream_interactor, conversation, jid); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(conversation, jid); } public ListRow.label(string c, string text) { name_label.label = text; - image.set_text(c); + picture.model = new ViewModel.CompatAvatarPictureModel(null).add(c); } public Widget get_widget() { diff --git a/main/src/ui/util/helper.vala b/main/src/ui/util/helper.vala index 35e16426..d6da72dd 100644 --- a/main/src/ui/util/helper.vala +++ b/main/src/ui/util/helper.vala @@ -61,62 +61,6 @@ public static string color_for_show(string show) { } } -public static async AvatarDrawer get_conversation_avatar_drawer(StreamInteractor stream_interactor, Conversation conversation) { - return yield get_conversation_participants_avatar_drawer(stream_interactor, conversation, new Jid[0]); -} - -public static async AvatarDrawer get_conversation_participants_avatar_drawer(StreamInteractor stream_interactor, Conversation conversation, owned Jid[] jids) { - AvatarManager avatar_manager = stream_interactor.get_module(AvatarManager.IDENTITY); - MucManager muc_manager = stream_interactor.get_module(MucManager.IDENTITY); - if (conversation.type_ != Conversation.Type.GROUPCHAT) { - Jid jid = jids.length == 1 ? jids[0] : conversation.counterpart; - Jid avatar_jid = jid; - if (conversation.type_ == Conversation.Type.GROUPCHAT_PM) avatar_jid = muc_manager.get_real_jid(avatar_jid, conversation.account) ?? avatar_jid; - return new AvatarDrawer().tile(yield avatar_manager.get_avatar(conversation.account, avatar_jid), jids.length == 1 ? - get_participant_display_name(stream_interactor, conversation, jid) : - get_conversation_display_name(stream_interactor, conversation), - Util.get_avatar_hex_color(stream_interactor, conversation.account, jid, conversation)); - } - if (jids.length > 0) { - AvatarDrawer drawer = new AvatarDrawer(); - for (int i = 0; i < (jids.length <= 4 ? jids.length : 3); i++) { - Jid avatar_jid = jids[i]; - Gdk.Pixbuf? part_avatar = yield avatar_manager.get_avatar(conversation.account, avatar_jid); - if (part_avatar == null && avatar_jid.equals_bare(conversation.counterpart) && muc_manager.is_private_room(conversation.account, conversation.counterpart)) { - avatar_jid = muc_manager.get_real_jid(avatar_jid, conversation.account) ?? avatar_jid; - part_avatar = yield avatar_manager.get_avatar(conversation.account, avatar_jid); - } - drawer.tile(part_avatar, get_participant_display_name(stream_interactor, conversation, jids[i]), - Util.get_avatar_hex_color(stream_interactor, conversation.account, jids[i], conversation)); - } - if (jids.length > 4) { - drawer.plus(); - } - return drawer; - } - Gdk.Pixbuf? room_avatar = yield avatar_manager.get_avatar(conversation.account, conversation.counterpart); - Gee.List? occupants = muc_manager.get_other_offline_members(conversation.counterpart, conversation.account); - if (room_avatar != null || !muc_manager.is_private_room(conversation.account, conversation.counterpart) || occupants == null || occupants.size == 0) { - return new AvatarDrawer().tile(room_avatar, "#", Util.get_avatar_hex_color(stream_interactor, conversation.account, conversation.counterpart, conversation)); - } - AvatarDrawer drawer = new AvatarDrawer(); - for (int i = 0; i < (occupants.size <= 4 ? occupants.size : 3); i++) { - Jid jid = occupants[i]; - Jid avatar_jid = jid; - Gdk.Pixbuf? part_avatar = yield avatar_manager.get_avatar(conversation.account, avatar_jid); - if (part_avatar == null && avatar_jid.equals_bare(conversation.counterpart) && muc_manager.is_private_room(conversation.account, conversation.counterpart)) { - avatar_jid = muc_manager.get_real_jid(avatar_jid, conversation.account) ?? avatar_jid; - part_avatar = yield avatar_manager.get_avatar(conversation.account, avatar_jid); - } - drawer.tile(part_avatar, get_participant_display_name(stream_interactor, conversation, jid), - Util.get_avatar_hex_color(stream_interactor, conversation.account, jid, conversation)); - } - if (occupants.size > 4) { - drawer.plus(); - } - return drawer; -} - public static string get_conversation_display_name(StreamInteractor stream_interactor, Conversation conversation) { return Dino.get_conversation_display_name(stream_interactor, conversation, _("%s from %s")); } @@ -137,27 +81,6 @@ public static string get_occupant_display_name(StreamInteractor stream_interacto return Dino.get_occupant_display_name(stream_interactor, conversation, jid, me_is_me ? _("Me") : null); } -// TODO this has no usages? -//public static void image_set_from_scaled_pixbuf(Image image, Gdk.Pixbuf pixbuf, int scale = 0, int width = 0, int height = 0) { -// if (scale == 0) scale = image.scale_factor; -// Cairo.Surface surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, scale, image.get_window()); -// if (height == 0 && width != 0) { -// height = (int) ((double) width / pixbuf.width * pixbuf.height); -// } else if (height != 0 && width == 0) { -// width = (int) ((double) height / pixbuf.height * pixbuf.width); -// } -// if (width != 0) { -// Cairo.Surface surface_new = new Cairo.Surface.similar_image(surface, Cairo.Format.ARGB32, width, height); -// Cairo.Context context = new Cairo.Context(surface_new); -// context.scale((double) width * scale / pixbuf.width, (double) height * scale / pixbuf.height); -// context.set_source_surface(surface, 0, 0); -// context.get_source().set_filter(Cairo.Filter.BEST); -// context.paint(); -// surface = surface_new; -// } -// image.set_from_surface(surface); -//} - public static Gdk.RGBA get_label_pango_color(Label label, string css_color) { Gtk.CssProvider provider = force_color(label, css_color); Gdk.RGBA color_rgba = label.get_style_context().get_color(); diff --git a/main/src/ui/widgets/avatar_picture.vala b/main/src/ui/widgets/avatar_picture.vala new file mode 100644 index 00000000..e632413c --- /dev/null +++ b/main/src/ui/widgets/avatar_picture.vala @@ -0,0 +1,519 @@ +using Dino.Entities; +using Gtk; +using Xmpp; + +public class Dino.Ui.ViewModel.AvatarPictureTileModel : Object { + public string display_text { get; set; } + public Gdk.RGBA background_color { get; set; } + public File? image_file { get; set; } +} + +public class Dino.Ui.ViewModel.AvatarPictureModel : Object { + public ListModel tiles { get; set; } +} + +public class Dino.Ui.ViewModel.ConversationParticipantAvatarPictureTileModel : AvatarPictureTileModel { + private StreamInteractor stream_interactor; + private AvatarManager? avatar_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(AvatarManager.IDENTITY); } } + private MucManager? muc_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(MucManager.IDENTITY); } } + private Conversation? conversation; + private Jid? primary_avatar_jid; + private Jid? secondary_avatar_jid; + private Jid? display_name_jid; + + public ConversationParticipantAvatarPictureTileModel(StreamInteractor stream_interactor, Conversation conversation, Jid jid) { + this.stream_interactor = stream_interactor; + this.conversation = conversation; + this.primary_avatar_jid = jid; + this.display_name_jid = jid; + + string color_id = jid.to_string(); + if (conversation.type_ != Conversation.Type.CHAT && primary_avatar_jid.equals_bare(conversation.counterpart)) { + Jid? real_jid = muc_manager.get_real_jid(primary_avatar_jid, conversation.account); + if (real_jid != null && muc_manager.is_private_room(conversation.account, conversation.counterpart.bare_jid)) { + secondary_avatar_jid = primary_avatar_jid; + primary_avatar_jid = real_jid.bare_jid; + color_id = primary_avatar_jid.to_string(); + } else { + color_id = jid.resourcepart.to_string(); + } + } else if (conversation.type_ == Conversation.Type.CHAT) { + primary_avatar_jid = jid.bare_jid; + color_id = primary_avatar_jid.to_string(); + } + string display = Util.get_participant_display_name(stream_interactor, conversation, display_name_jid); + display_text = display.get_char(0).toupper().to_string(); + stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.connect(on_roster_updated); + + float[] rgbf = color_id != null ? Xep.ConsistentColor.string_to_rgbf(color_id) : new float[] {0.5f, 0.5f, 0.5f}; + background_color = Gdk.RGBA() { red = rgbf[0], green = rgbf[1], blue = rgbf[2], alpha = 1.0f}; + + update_image_file(); + avatar_manager.received_avatar.connect(on_received_avatar); + avatar_manager.fetched_avatar.connect(on_received_avatar); + } + + private void update_image_file() { + File image_file = avatar_manager.get_avatar_file(conversation.account, primary_avatar_jid); + if (image_file == null && secondary_avatar_jid != null) { + image_file = avatar_manager.get_avatar_file(conversation.account, secondary_avatar_jid); + } + this.image_file = image_file; + } + + private void on_received_avatar(Jid jid, Account account) { + if (account.equals(conversation.account) && (jid.equals(primary_avatar_jid) || jid.equals(secondary_avatar_jid))) { + update_image_file(); + } + } + + private void on_roster_updated(Account account, Jid jid) { + if (account.equals(conversation.account) && jid.equals(display_name_jid)) { + string display = Util.get_participant_display_name(stream_interactor, conversation, display_name_jid); + display_text = display.get_char(0).toupper().to_string(); + } + } +} + +public class Dino.Ui.ViewModel.CompatAvatarPictureModel : AvatarPictureModel { + private StreamInteractor stream_interactor; + private AvatarManager? avatar_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(AvatarManager.IDENTITY); } } + private MucManager? muc_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(MucManager.IDENTITY); } } + private PresenceManager? presence_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(PresenceManager.IDENTITY); } } + private ConnectionManager? connection_manager { owned get { return stream_interactor == null ? null : stream_interactor.connection_manager; } } + private Conversation? conversation; + + construct { + tiles = new GLib.ListStore(typeof(ViewModel.AvatarPictureTileModel)); + } + + public CompatAvatarPictureModel(StreamInteractor? stream_interactor) { + this.stream_interactor = stream_interactor; + if (stream_interactor != null) { + connect_signals_weak(this); + } + } + + private static void connect_signals_weak(CompatAvatarPictureModel model_) { + WeakRef model_weak = WeakRef(model_); + ulong muc_manager_private_room_occupant_updated_handler_id = 0; + ulong muc_manager_proom_info_updated_handler_id = 0; + ulong avatar_manager_received_avatar_handler_id = 0; + ulong avatar_manager_fetched_avatar_handler_id = 0; + muc_manager_private_room_occupant_updated_handler_id = model_.muc_manager.private_room_occupant_updated.connect((muc_manager, account, room, jid) => { + CompatAvatarPictureModel? model = (CompatAvatarPictureModel) model_weak.get(); + if (model != null) { + model.on_room_updated(account, room); + } else if (muc_manager_private_room_occupant_updated_handler_id != 0) { + muc_manager.disconnect(muc_manager_private_room_occupant_updated_handler_id); + muc_manager_private_room_occupant_updated_handler_id = 0; + } + }); + muc_manager_proom_info_updated_handler_id = model_.muc_manager.room_info_updated.connect((muc_manager, account, room) => { + CompatAvatarPictureModel? model = (CompatAvatarPictureModel) model_weak.get(); + if (model != null) { + model.on_room_updated(account, room); + } else if (muc_manager_proom_info_updated_handler_id != 0) { + muc_manager.disconnect(muc_manager_proom_info_updated_handler_id); + muc_manager_proom_info_updated_handler_id = 0; + } + }); + avatar_manager_received_avatar_handler_id = model_.avatar_manager.received_avatar.connect((avatar_manager, jid, account) => { + CompatAvatarPictureModel? model = (CompatAvatarPictureModel) model_weak.get(); + if (model != null) { + model.on_received_avatar(jid, account); + } else if (avatar_manager_received_avatar_handler_id != 0) { + avatar_manager.disconnect(avatar_manager_received_avatar_handler_id); + avatar_manager_received_avatar_handler_id = 0; + } + }); + avatar_manager_fetched_avatar_handler_id = model_.avatar_manager.fetched_avatar.connect((avatar_manager, jid, account) => { + CompatAvatarPictureModel? model = (CompatAvatarPictureModel) model_weak.get(); + if (model != null) { + model.on_received_avatar(jid, account); + } else if (avatar_manager_fetched_avatar_handler_id != 0) { + avatar_manager.disconnect(avatar_manager_fetched_avatar_handler_id); + avatar_manager_fetched_avatar_handler_id = 0; + } + }); + } + + private void on_room_updated(Account account, Jid room) { + if (conversation != null && account.equals(conversation.account) && conversation.counterpart.equals_bare(room)) { + reset(); + set_conversation(conversation); + } + } + + private void on_received_avatar(Jid jid, Account account) { + on_room_updated(account, jid); + } + + public void reset() { + (tiles as GLib.ListStore).remove_all(); + } + + public CompatAvatarPictureModel set_conversation(Conversation conversation) { + if (stream_interactor == null) { + critical("set_conversation() used on CompatAvatarPictureModel without stream_interactor"); + return this; + } + this.conversation = conversation; + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + if (avatar_manager.has_avatar(conversation.account, conversation.counterpart)) { + add_internal("#", conversation.counterpart.to_string(), avatar_manager.get_avatar_file(conversation.account, conversation.counterpart)); + } else { + Gee.List? occupants = muc_manager.get_other_offline_members(conversation.counterpart, conversation.account); + if (occupants != null && !occupants.is_empty && muc_manager.is_private_room(conversation.account, conversation.counterpart)) { + int count = occupants.size > 4 ? 3 : occupants.size; + for (int i = 0; i < count; i++) { + add_participant(conversation, occupants[i]); + } + if (occupants.size > 4) { + add_internal("+"); + } + } else { + add_internal("#", conversation.counterpart.to_string()); + } + } + } else { + add_participant(conversation, conversation.counterpart); + } + return this; + } + + public CompatAvatarPictureModel add_participant(Conversation conversation, Jid jid) { + if (stream_interactor == null) { + critical("add_participant() used on CompatAvatarPictureModel without stream_interactor"); + return this; + } + (tiles as GLib.ListStore).append(new ConversationParticipantAvatarPictureTileModel(stream_interactor, conversation, jid)); + return this; + } + + public CompatAvatarPictureModel add(string display, string? color_id = null, File? image_file = null) { + add_internal(display, color_id, image_file); + return this; + } + + private AvatarPictureTileModel add_internal(string display, string? color_id = null, File? image_file = null) { + GLib.ListStore store = tiles as GLib.ListStore; + float[] rgbf = color_id != null ? Xep.ConsistentColor.string_to_rgbf(color_id) : new float[] {0.5f, 0.5f, 0.5f}; + var model = new ViewModel.AvatarPictureTileModel() { + display_text = display.get_char(0).toupper().to_string(), + background_color = Gdk.RGBA() { red = rgbf[0], green = rgbf[1], blue = rgbf[2], alpha = 1.0f}, + image_file = image_file + }; + store.append(model); + return model; + } +} + + +public class Dino.Ui.CompatAvatarDrawer { + public float radius_percent { get; set; default = 0.2f; } + public ViewModel.AvatarPictureModel? model { get; set; } + public int height_request { get; set; default = 35; } + public int width_request { get; set; default = 35; } + public string font_family { get; set; default = "Sans"; } + + public Cairo.ImageSurface draw_image_surface() { + Cairo.ImageSurface surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, width_request, height_request); + draw_on_context(new Cairo.Context(surface)); + return surface; + } + + public void draw_on_context(Cairo.Context ctx) { + double radius = (width_request + height_request) * 0.25f * radius_percent; + double degrees = Math.PI / 180.0; + ctx.new_sub_path(); + ctx.arc(width_request - radius, radius, radius, -90 * degrees, 0 * degrees); + ctx.arc(width_request - radius, height_request - radius, radius, 0 * degrees, 90 * degrees); + ctx.arc(radius, height_request - radius, radius, 90 * degrees, 180 * degrees); + ctx.arc(radius, radius, radius, 180 * degrees, 270 * degrees); + ctx.close_path(); + ctx.clip(); + + if (this.model.tiles.get_n_items() == 4) { + Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width_request, height_request); + Cairo.Context bufctx = new Cairo.Context(buffer); + bufctx.scale(0.5, 0.5); + bufctx.set_source_surface(sub_surface_idx(ctx, 0, width_request - 1, height_request - 1, 2), 0, 0); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 1, width_request - 1, height_request - 1, 2), width_request + 1, 0); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 2, width_request - 1, height_request - 1, 2), 0, height_request + 1); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 3, width_request - 1, height_request - 1, 2), width_request + 1, height_request + 1); + bufctx.paint(); + + ctx.set_source_surface(buffer, 0, 0); + ctx.paint(); + } else if (this.model.tiles.get_n_items() == 3) { + Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width_request, height_request); + Cairo.Context bufctx = new Cairo.Context(buffer); + bufctx.scale(0.5, 0.5); + bufctx.set_source_surface(sub_surface_idx(ctx, 0, width_request - 1, height_request - 1, 2), 0, 0); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 1, width_request - 1, height_request * 2, 2), width_request + 1, 0); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 2, width_request - 1, height_request - 1, 2), 0, width_request + 1); + bufctx.paint(); + + ctx.set_source_surface(buffer, 0, 0); + ctx.paint(); + } else if (this.model.tiles.get_n_items() == 2) { + Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width_request, height_request); + Cairo.Context bufctx = new Cairo.Context(buffer); + bufctx.scale(0.5, 0.5); + bufctx.set_source_surface(sub_surface_idx(ctx, 0, width_request - 1, height_request * 2, 2), 0, 0); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 1, width_request - 1, height_request * 2, 2), width_request + 1, 0); + bufctx.paint(); + + ctx.set_source_surface(buffer, 0, 0); + ctx.paint(); + } else if (this.model.tiles.get_n_items() == 1) { + ctx.set_source_surface(sub_surface_idx(ctx, 0, width_request, height_request, 1), 0, 0); + ctx.paint(); + } else if (this.model.tiles.get_n_items() == 0) { + ctx.set_source_surface(sub_surface_idx(ctx, -1, width_request, height_request, 1), 0, 0); + ctx.paint(); + } + ctx.set_source_rgb(0, 0, 0); + } + + private Cairo.Surface sub_surface_idx(Cairo.Context ctx, int idx, int width, int height, int font_factor = 1) { + ViewModel.AvatarPictureTileModel tile = (ViewModel.AvatarPictureTileModel) this.model.tiles.get_item(idx); + Gdk.Pixbuf? avatar = new Gdk.Pixbuf.from_file(tile.image_file.get_path()); + string? name = idx >= 0 ? tile.display_text : ""; + Gdk.RGBA hex_color = tile.background_color; + return sub_surface(ctx, font_family, avatar, name, hex_color, width, height, font_factor); + } + + private static Cairo.Surface sub_surface(Cairo.Context ctx, string font_family, Gdk.Pixbuf? avatar, string? name, Gdk.RGBA background_color, int width, int height, int font_factor = 1) { + Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height); + Cairo.Context bufctx = new Cairo.Context(buffer); + if (avatar == null) { + Gdk.cairo_set_source_rgba(bufctx, background_color); + bufctx.rectangle(0, 0, width, height); + bufctx.fill(); + + string text = name == null ? "…" : name.get_char(0).toupper().to_string(); + bufctx.select_font_face(font_family, Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL); + bufctx.set_font_size(width / font_factor < 40 ? font_factor * 17 : font_factor * 25); + Cairo.TextExtents extents; + bufctx.text_extents(text, out extents); + double x_pos = width/2 - (extents.width/2 + extents.x_bearing); + double y_pos = height/2 - (extents.height/2 + extents.y_bearing); + bufctx.move_to(x_pos, y_pos); + bufctx.set_source_rgba(1, 1, 1, 1); + bufctx.show_text(text); + } else { + double w_scale = (double) width / avatar.width; + double h_scale = (double) height / avatar.height; + double scale = double.max(w_scale, h_scale); + bufctx.scale(scale, scale); + + double x_off = 0, y_off = 0; + if (scale == h_scale) { + x_off = (width / scale - avatar.width) / 2.0; + } else { + y_off = (height / scale - avatar.height) / 2.0; + } + + Gdk.cairo_set_source_pixbuf(bufctx, avatar, x_off, y_off); + bufctx.get_source().set_filter(Cairo.Filter.BEST); + bufctx.paint(); + } + return buffer; + } +} + +public class Dino.Ui.AvatarPicture : Gtk.Widget { + public float radius_percent { get; set; default = 0.2f; } + public ViewModel.AvatarPictureModel? model { get; set; } + private Gee.List tiles = new Gee.ArrayList(); + + private ViewModel.AvatarPictureModel? old_model; + private ulong model_tiles_items_changed_handler; + + construct { + height_request = 35; + width_request = 35; + set_css_name("picture"); + add_css_class("avatar"); + notify["radius-percent"].connect(queue_draw); + notify["model"].connect(on_model_changed); + } + + private void on_model_changed() { + if (old_model != null) { + old_model.tiles.disconnect(model_tiles_items_changed_handler); + } + foreach (Tile tile in tiles) { + tile.unparent(); + tile.dispose(); + } + tiles.clear(); + old_model = model; + if (model != null) { + model_tiles_items_changed_handler = model.tiles.items_changed.connect(on_model_items_changed); + for(int i = 0; i < model.tiles.get_n_items(); i++) { + Tile tile = new Tile(); + tile.model = model.tiles.get_item(i) as ViewModel.AvatarPictureTileModel; + tile.insert_after(this, tiles.is_empty ? null : tiles.last()); + tiles.add(tile); + } + } + } + + private void on_model_items_changed(uint position, uint removed, uint added) { + while (removed > 0) { + Tile old = tiles.remove_at((int) position); + old.unparent(); + old.dispose(); + removed--; + } + while (added > 0) { + Tile tile = new Tile(); + tile.model = model.tiles.get_item(position) as ViewModel.AvatarPictureTileModel; + tile.insert_after(this, position == 0 ? null : tiles[(int) position - 1]); + tiles.insert((int) position, tile); + position++; + added--; + } + queue_allocate(); + } + + public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { + minimum_baseline = natural_baseline = -1; + if (orientation == Orientation.HORIZONTAL) { + minimum = natural = width_request; + } else { + minimum = natural = height_request; + } + } + + public override void size_allocate(int width, int height, int baseline) { + int half_width_size = width / 2; + int half_height_size = height / 2; + int half_width_offset = (width % 2 == 0) ? half_width_size : half_width_size + 1; + int half_height_offset = (height % 2 == 0) ? half_height_size : half_height_size + 1; + if (tiles.size == 1) { + tiles[0].allocate(width, height, baseline, null); + } else if (tiles.size == 2) { + tiles[0].allocate_size(Allocation() { x = 0, y = 0, width = half_width_size, height = height }, baseline); + tiles[1].allocate_size(Allocation() { x = half_width_offset, y = 0, width = half_width_size, height = height }, baseline); + } else if (tiles.size == 3) { + tiles[0].allocate_size(Allocation() { x = 0, y = 0, width = half_width_size, height = height }, baseline); + tiles[1].allocate_size(Allocation() { x = half_width_offset, y = 0, width = half_width_size, height = half_height_size }, baseline); + tiles[2].allocate_size(Allocation() { x = half_width_offset, y = half_height_offset, width = half_width_size, height = half_height_size }, baseline); + } else if (tiles.size == 4) { + tiles[0].allocate_size(Allocation() { x = 0, y = 0, width = half_width_size, height = half_height_size }, baseline); + tiles[1].allocate_size(Allocation() { x = half_width_offset, y = 0, width = half_width_size, height = half_height_size }, baseline); + tiles[2].allocate_size(Allocation() { x = 0, y = half_height_offset, width = half_width_size, height = half_height_size }, baseline); + tiles[3].allocate_size(Allocation() { x = half_width_offset, y = half_height_offset, width = half_width_size, height = half_height_size }, baseline); + } + } + + public override SizeRequestMode get_request_mode() { + return SizeRequestMode.CONSTANT_SIZE; + } + + public override void snapshot(Gtk.Snapshot snapshot) { + Graphene.Rect bounds = Graphene.Rect(); + bounds.init(0, 0, get_width(), get_height()); + Gsk.RoundedRect rounded_rect = Gsk.RoundedRect(); + rounded_rect.init_from_rect(bounds, (get_width() + get_height()) * 0.25f * radius_percent); + snapshot.push_rounded_clip(rounded_rect); + base.snapshot(snapshot); + snapshot.pop(); + } + + public override void dispose() { + model = null; + on_model_changed(); + base.dispose(); + } + + private class Tile : Gtk.Widget { + public ViewModel.AvatarPictureTileModel? model { get; set; } + public Gdk.RGBA background_color { get; set; default = Gdk.RGBA(){ red = 1.0f, green = 1.0f, blue = 1.0f, alpha = 0.0f }; } + public string display_text { get { return label.get_text(); } set { label.set_text(value); } } + public File? image_file { get { return picture.file; } set { picture.file = value; } } + + private Binding? background_color_binding; + private Binding? display_text_binding; + private Binding? image_file_binding; + + private Label label = new Label(""); + private Picture picture = new Picture(); + + construct { + label.insert_after(this, null); + label.attributes = new Pango.AttrList(); + label.attributes.insert(Pango.attr_foreground_new(uint16.MAX, uint16.MAX, uint16.MAX)); +#if GTK_4_8 && VALA_0_58 + picture.content_fit = Gtk.ContentFit.COVER; +#elif GTK_4_8 + picture.@set("content-fit", 2); +#endif + picture.insert_after(this, label); + this.notify["model"].connect(on_model_changed); + } + + private void on_model_changed() { + if (background_color_binding != null) background_color_binding.unbind(); + if (display_text_binding != null) display_text_binding.unbind(); + if (image_file_binding != null) image_file_binding.unbind(); + if (model != null) { + background_color_binding = model.bind_property("background-color", this, "background-color", BindingFlags.SYNC_CREATE); + display_text_binding = model.bind_property("display-text", this, "display-text", BindingFlags.SYNC_CREATE); + image_file_binding = model.bind_property("image-file", this, "image-file", BindingFlags.SYNC_CREATE); + } else { + background_color_binding = null; + display_text_binding = null; + image_file_binding = null; + } + } + + public override void dispose() { + if (background_color_binding != null) background_color_binding.unbind(); + if (display_text_binding != null) display_text_binding.unbind(); + if (image_file_binding != null) image_file_binding.unbind(); + background_color_binding = null; + display_text_binding = null; + image_file_binding = null; + label.unparent(); + picture.unparent(); + base.dispose(); + } + + public override void size_allocate(int width, int height, int baseline) { + int min, nat, bl_min, bl_nat; + picture.measure(Orientation.HORIZONTAL, -1, out min, out nat, out bl_min, out bl_nat); + if (nat > 0) { + picture.allocate(width, height, baseline, null); + label.visible = false; + } else { + picture.allocate(0, 0, 0, null); + label.attributes = new Pango.AttrList(); + label.attributes.insert(Pango.attr_foreground_new(uint16.MAX, uint16.MAX, uint16.MAX)); + label.attributes.insert(Pango.attr_scale_new(double.min((double)width, (double)height) * 0.05)); + label.margin_bottom = height/40; + label.visible = true; + label.allocate(width, height, baseline, null); + } + } + + public override void snapshot(Gtk.Snapshot snapshot) { + if (label.visible) { + Graphene.Rect bounds = Graphene.Rect(); + bounds.init(0, 0, get_width(), get_height()); + snapshot.append_node(new Gsk.ColorNode(background_color, bounds)); + } + base.snapshot(snapshot); + } + } +} \ No newline at end of file From 56195fd2b0bb2248bc111f55e7d6f982737c5e96 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Mon, 6 Mar 2023 19:37:21 +0100 Subject: [PATCH 09/27] Update XEPs in DOAP Fixes #1376 --- dino.doap | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- dino.doap.in | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/dino.doap b/dino.doap index 819777b0..396b0497 100644 --- a/dino.doap +++ b/dino.doap @@ -362,6 +362,12 @@ partial + + + + complete + + @@ -442,6 +448,13 @@ complete + + + + partial + 0.3 + + @@ -467,6 +480,13 @@ For use with XEP-0280 + + + + partial + 0.3 + + @@ -479,7 +499,6 @@ partial - Not for MUCs @@ -546,6 +565,12 @@ partial + + + + complete + + @@ -576,6 +601,21 @@ complete + + + + complete + 0.4 + + + + + + complete + 0.1.1 + 0.4 + + @@ -583,5 +623,13 @@ No support for embedded thumbnails + + + + complete + 0.2.0 + 0.4 + + diff --git a/dino.doap.in b/dino.doap.in index 563de1d4..c54742e6 100644 --- a/dino.doap.in +++ b/dino.doap.in @@ -182,6 +182,12 @@ partial + + + + complete + + @@ -262,6 +268,13 @@ complete + + + + partial + 0.3 + + @@ -287,6 +300,13 @@ For use with XEP-0280 + + + + partial + 0.3 + + @@ -299,7 +319,6 @@ partial - Not for MUCs @@ -366,6 +385,12 @@ partial + + + + complete + + @@ -396,6 +421,21 @@ complete + + + + complete + 0.4 + + + + + + complete + 0.1.1 + 0.4 + + @@ -403,5 +443,13 @@ No support for embedded thumbnails + + + + complete + 0.2.0 + 0.4 + + From 47a066987dec6b83c3964feb3571aee2d03d7f73 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Tue, 7 Mar 2023 09:46:09 -0600 Subject: [PATCH 10/27] DOAP: Add first supported version for more XEPs --- dino.doap | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- dino.doap.in | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/dino.doap b/dino.doap index 396b0497..b12ded1d 100644 --- a/dino.doap +++ b/dino.doap @@ -227,6 +227,7 @@ complete + 0.1 @@ -252,6 +253,7 @@ complete For use with XEP-0261 + 0.1 @@ -259,12 +261,14 @@ deprecated Migrating to XEP-0402 if supported by server + 0.1 complete + 0.1 @@ -272,6 +276,7 @@ partial Only for viewing avatars + 0.1 @@ -279,12 +284,14 @@ partial For use with XEP-0313 + 0.1 partial + 0.1 @@ -292,6 +299,7 @@ partial For use with XEP-0260 + 0.1 @@ -299,36 +307,42 @@ complete For file transfers using XEP-0363 + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 @@ -348,60 +362,70 @@ partial + 0.1 partial + 0.3 partial + 0.3 complete + 0.3 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 partial + 0.3 @@ -420,6 +444,7 @@ partial + 0.1 @@ -427,6 +452,7 @@ 1.0 complete + 0.1 @@ -434,18 +460,21 @@ partial No support for sending + 0.3 complete + 0.1 complete + 0.1 @@ -459,18 +488,21 @@ complete + 0.1 partial + 0.3 partial + 0.3 @@ -478,6 +510,7 @@ complete For use with XEP-0280 + 0.1 @@ -499,6 +532,7 @@ partial + 0.1 @@ -512,12 +546,14 @@ complete + 0.1 partial + 0.1 @@ -525,24 +561,28 @@ complete 0.3.1 + 0.3 complete + 0.1 complete + 0.1 complete + 0.1 @@ -550,6 +590,7 @@ partial Only for outgoing messages + 0.1 @@ -557,30 +598,35 @@ complete 0.3.0 + 0.1 partial + 0.1 complete + 0.5 partial + 0.1 complete + 0.1 @@ -593,12 +639,14 @@ complete + 0.1 complete + 0.2 @@ -621,11 +669,12 @@ partial No support for embedded thumbnails + 0.1 - + complete 0.2.0 0.4 diff --git a/dino.doap.in b/dino.doap.in index c54742e6..7e43d706 100644 --- a/dino.doap.in +++ b/dino.doap.in @@ -47,6 +47,7 @@ complete + 0.1 @@ -72,6 +73,7 @@ complete For use with XEP-0261 + 0.1 @@ -79,12 +81,14 @@ deprecated Migrating to XEP-0402 if supported by server + 0.1 complete + 0.1 @@ -92,6 +96,7 @@ partial Only for viewing avatars + 0.1 @@ -99,12 +104,14 @@ partial For use with XEP-0313 + 0.1 partial + 0.1 @@ -112,6 +119,7 @@ partial For use with XEP-0260 + 0.1 @@ -119,36 +127,42 @@ complete For file transfers using XEP-0363 + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 @@ -168,60 +182,70 @@ partial + 0.1 partial + 0.3 partial + 0.3 complete + 0.3 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 partial + 0.3 @@ -240,6 +264,7 @@ partial + 0.1 @@ -247,6 +272,7 @@ 1.0 complete + 0.1 @@ -254,18 +280,21 @@ partial No support for sending + 0.3 complete + 0.1 complete + 0.1 @@ -279,18 +308,21 @@ complete + 0.1 partial + 0.3 partial + 0.3 @@ -298,6 +330,7 @@ complete For use with XEP-0280 + 0.1 @@ -319,6 +352,7 @@ partial + 0.1 @@ -332,12 +366,14 @@ complete + 0.1 partial + 0.1 @@ -345,24 +381,28 @@ complete 0.3.1 + 0.3 complete + 0.1 complete + 0.1 complete + 0.1 @@ -370,6 +410,7 @@ partial Only for outgoing messages + 0.1 @@ -377,30 +418,35 @@ complete 0.3.0 + 0.1 partial + 0.1 complete + 0.5 partial + 0.1 complete + 0.1 @@ -413,12 +459,14 @@ complete + 0.1 complete + 0.2 @@ -441,11 +489,12 @@ partial No support for embedded thumbnails + 0.1 - + complete 0.2.0 0.4 From 748d507a3e3dd236a72bae4d76d0a6c3f557db1d Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sat, 18 Mar 2023 09:45:02 -0600 Subject: [PATCH 11/27] Add missing since to DOAP --- dino.doap | 9 +++++++++ dino.doap.in | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/dino.doap b/dino.doap index b12ded1d..0de627cf 100644 --- a/dino.doap +++ b/dino.doap @@ -234,18 +234,21 @@ complete + 0.1 complete + 0.1 partial + 0.1 @@ -349,6 +352,7 @@ deprecated + 0.1 Only to fetch Avatars from other users @@ -356,6 +360,7 @@ complete + 0.1 @@ -432,12 +437,14 @@ complete + 0.1 complete + 0.1 @@ -540,6 +547,7 @@ complete 1.0.0 + 0.3 @@ -633,6 +641,7 @@ complete + 0.1 diff --git a/dino.doap.in b/dino.doap.in index 7e43d706..7a175e63 100644 --- a/dino.doap.in +++ b/dino.doap.in @@ -54,18 +54,21 @@ complete + 0.1 complete + 0.1 partial + 0.1 @@ -169,6 +172,7 @@ deprecated + 0.1 Only to fetch Avatars from other users @@ -176,6 +180,7 @@ complete + 0.1 @@ -252,12 +257,14 @@ complete + 0.1 complete + 0.1 @@ -360,6 +367,7 @@ complete 1.0.0 + 0.3 @@ -453,6 +461,7 @@ complete + 0.1 From cb10110c57d569d8a9dfc85d1bec03ff38ef8172 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Mon, 20 Mar 2023 15:01:23 -0600 Subject: [PATCH 12/27] Fix C binding for gst_video_frame_get_data Fixes #1267 --- plugins/rtp/src/video_widget.vala | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/rtp/src/video_widget.vala b/plugins/rtp/src/video_widget.vala index af2a9f1d..0d66476b 100644 --- a/plugins/rtp/src/video_widget.vala +++ b/plugins/rtp/src/video_widget.vala @@ -1,4 +1,5 @@ private static extern unowned Gst.Video.Info gst_video_frame_get_video_info(Gst.Video.Frame frame); +[CCode (array_length_type = "size_t", type = "void*")] private static extern unowned uint8[] gst_video_frame_get_data(Gst.Video.Frame frame); public class Dino.Plugins.Rtp.Paintable : Gdk.Paintable, Object { From 3721027edb2d2f6f71cf655d643f7796864cfdbc Mon Sep 17 00:00:00 2001 From: Marvin W Date: Mon, 20 Mar 2023 15:37:18 -0600 Subject: [PATCH 13/27] Improve history sync - Ensure we fully fetch desired history if possible (previously, duplicates from offline message queue could hinder MAM sync) - Early drop illegal MAM messages so they don't pile up in the pending queue waiting for their query to end (which it never will if they were not requested in first place). Fixes #1386 --- libdino/src/service/history_sync.vala | 65 +++++-------------- .../xep/0313_message_archive_management.vala | 46 ++++++++++--- 2 files changed, 56 insertions(+), 55 deletions(-) diff --git a/libdino/src/service/history_sync.vala b/libdino/src/service/history_sync.vala index e83b3cb4..2444a133 100644 --- a/libdino/src/service/history_sync.vala +++ b/libdino/src/service/history_sync.vala @@ -163,7 +163,7 @@ public class Dino.HistorySync { if (current_row[db.mam_catchup.from_end]) return; debug("[%s] Fetching between ranges %s - %s", mam_server.to_string(), previous_row[db.mam_catchup.to_time].to_string(), current_row[db.mam_catchup.from_time].to_string()); - current_row = yield fetch_between_ranges(account, mam_server, previous_row, current_row); + current_row = yield fetch_between_ranges(account, mam_server, previous_row, current_row, cancellable); if (current_row == null) return; RowOption previous_row_opt = db.mam_catchup.select() @@ -214,13 +214,11 @@ public class Dino.HistorySync { return null; } - // If we get PageResult.Duplicate, we still want to update the db row to the latest message. - // Catchup finished within first page. Update latest db entry. if (latest_row_id != -1 && - page_result.page_result in new PageResult[] { PageResult.TargetReached, PageResult.NoMoreMessages, PageResult.Duplicate }) { + page_result.page_result in new PageResult[] { PageResult.TargetReached, PageResult.NoMoreMessages }) { - if (page_result.stanzas == null || page_result.stanzas.is_empty) return null; + if (page_result.stanzas == null) return null; string latest_mam_id = page_result.query_result.last; long latest_mam_time = (long) mam_times[account][latest_mam_id].to_unix(); @@ -272,7 +270,7 @@ public class Dino.HistorySync { ** Merges the `earlier_range` db row into the `later_range` db row. ** @return The resulting range comprising `earlier_range`, `later_rage`, and everything in between. null if fetching/merge failed. **/ - private async Row? fetch_between_ranges(Account account, Jid mam_server, Row earlier_range, Row later_range) { + private async Row? fetch_between_ranges(Account account, Jid mam_server, Row earlier_range, Row later_range, Cancellable? cancellable = null) { int later_range_id = (int) later_range[db.mam_catchup.id]; DateTime earliest_time = new DateTime.from_unix_utc(earlier_range[db.mam_catchup.to_time]); DateTime latest_time = new DateTime.from_unix_utc(later_range[db.mam_catchup.from_time]); @@ -282,9 +280,9 @@ public class Dino.HistorySync { earliest_time, earlier_range[db.mam_catchup.to_id], latest_time, later_range[db.mam_catchup.from_id]); - PageRequestResult page_result = yield fetch_query(account, query_params, later_range_id); + PageRequestResult page_result = yield fetch_query(account, query_params, later_range_id, cancellable); - if (page_result.page_result == PageResult.TargetReached) { + if (page_result.page_result == PageResult.TargetReached || page_result.page_result == PageResult.NoMoreMessages) { debug("[%s | %s] Merging range %i into %i", account.bare_jid.to_string(), mam_server.to_string(), earlier_range[db.mam_catchup.id], later_range_id); // Merge earlier range into later one. db.mam_catchup.update() @@ -330,9 +328,9 @@ public class Dino.HistorySync { PageRequestResult? page_result = null; do { page_result = yield get_mam_page(account, query_params, page_result, cancellable); - debug("Page result %s %b", page_result.page_result.to_string(), page_result.stanzas == null); + debug("[%s | %s] Page result %s (got stanzas: %s)", account.bare_jid.to_string(), query_params.mam_server.to_string(), page_result.page_result.to_string(), (page_result.stanzas != null).to_string()); - if (page_result.page_result == PageResult.Error || page_result.page_result == PageResult.Cancelled || page_result.stanzas == null) return page_result; + if (page_result.page_result == PageResult.Error || page_result.page_result == PageResult.Cancelled || page_result.query_result.first == null) return page_result; string earliest_mam_id = page_result.query_result.first; long earliest_mam_time = (long)mam_times[account][earliest_mam_id].to_unix(); @@ -357,7 +355,6 @@ public class Dino.HistorySync { MorePagesAvailable, TargetReached, NoMoreMessages, - Duplicate, Error, Cancelled } @@ -399,23 +396,25 @@ public class Dino.HistorySync { string query_id = query_params.query_id; string? after_id = query_params.start_id; + var stanzas_for_query = stanzas.has_key(query_id) && !stanzas[query_id].is_empty ? stanzas[query_id] : null; if (cancellable != null && cancellable.is_cancelled()) { - return new PageRequestResult(PageResult.Cancelled, query_result, stanzas[query_id]); + stanzas.unset(query_id); + return new PageRequestResult(PageResult.Cancelled, query_result, stanzas_for_query); } - if (stanzas.has_key(query_id) && !stanzas[query_id].is_empty) { + if (stanzas_for_query != null) { // Check it we reached our target (from_id) - foreach (Xmpp.MessageStanza message in stanzas[query_id]) { + foreach (Xmpp.MessageStanza message in stanzas_for_query) { Xmpp.MessageArchiveManagement.MessageFlag? mam_message_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message); if (mam_message_flag != null && mam_message_flag.mam_id != null) { if (after_id != null && mam_message_flag.mam_id == after_id) { // Successfully fetched the whole range yield send_messages_back_into_pipeline(account, query_id, cancellable); if (cancellable != null && cancellable.is_cancelled()) { - return new PageRequestResult(PageResult.Cancelled, query_result, stanzas[query_id]); + return new PageRequestResult(PageResult.Cancelled, query_result, stanzas_for_query); } - return new PageRequestResult(PageResult.TargetReached, query_result, stanzas[query_id]); + return new PageRequestResult(PageResult.TargetReached, query_result, stanzas_for_query); } } } @@ -423,37 +422,9 @@ public class Dino.HistorySync { // Message got filtered out by xmpp-vala, but succesful range fetch nevertheless yield send_messages_back_into_pipeline(account, query_id); if (cancellable != null && cancellable.is_cancelled()) { - return new PageRequestResult(PageResult.Cancelled, query_result, stanzas[query_id]); + return new PageRequestResult(PageResult.Cancelled, query_result, stanzas_for_query); } - return new PageRequestResult(PageResult.TargetReached, query_result, stanzas[query_id]); - } - - // Check for duplicates. Go through all messages and build a db query. - foreach (Xmpp.MessageStanza message in stanzas[query_id]) { - Xmpp.MessageArchiveManagement.MessageFlag? mam_message_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message); - if (mam_message_flag != null && mam_message_flag.mam_id != null) { - if (selection == null) { - selection = @"$(db.message.server_id) = ?"; - } else { - selection += @" OR $(db.message.server_id) = ?"; - } - selection_args += mam_message_flag.mam_id; - } - } - var duplicates_qry = db.message.select() - .with(db.message.account_id, "=", account.id) - .where(selection, selection_args); - // We don't want messages from different MAM servers to interfere with each other. - if (!query_params.mam_server.equals_bare(account.bare_jid)) { - duplicates_qry.with(db.message.counterpart_id, "=", db.get_jid_id(query_params.mam_server)); - } else { - duplicates_qry.with(db.message.type_, "=", Message.Type.CHAT); - } - var duplicates_count = duplicates_qry.count(); - if (duplicates_count > 0) { - // We got a duplicate although we thought we have to catch up. - // There was a server bug where prosody would send all messages if it didn't know the after ID that was given - page_result = PageResult.Duplicate; + return new PageRequestResult(PageResult.TargetReached, query_result, stanzas_for_query); } } @@ -461,7 +432,7 @@ public class Dino.HistorySync { if (cancellable != null && cancellable.is_cancelled()) { page_result = PageResult.Cancelled; } - return new PageRequestResult(page_result, query_result, stanzas.has_key(query_id) ? stanzas[query_id] : null); + return new PageRequestResult(page_result, query_result, stanzas_for_query); } private async void send_messages_back_into_pipeline(Account account, string query_id, Cancellable? cancellable = null) { diff --git a/xmpp-vala/src/module/xep/0313_message_archive_management.vala b/xmpp-vala/src/module/xep/0313_message_archive_management.vala index 1caa1bc3..2235e118 100644 --- a/xmpp-vala/src/module/xep/0313_message_archive_management.vala +++ b/xmpp-vala/src/module/xep/0313_message_archive_management.vala @@ -11,8 +11,8 @@ public class QueryResult { public bool error { get; set; default=false; } public bool malformed { get; set; default=false; } public bool complete { get; set; default=false; } - public string first { get; set; } - public string last { get; set; } + public string? first { get; set; } + public string? last { get; set; } } public class Module : XmppStreamModule { @@ -65,16 +65,17 @@ public class Module : XmppStreamModule { } StanzaNode query_node = new StanzaNode.build("query", NS_VER(stream)).add_self_xmlns().put_node(data_form.get_submit_node()); - if (queryid != null) { - query_node.put_attribute("queryid", queryid); - } + query_node.put_attribute("queryid", queryid); return query_node; } internal async QueryResult query_archive(XmppStream stream, string ns, Jid? mam_server, StanzaNode query_node, Cancellable? cancellable = null) { - var res = new QueryResult(); - if (stream.get_flag(Flag.IDENTITY) == null) { res.error = true; return res; } + var res = new QueryResult(); + Flag? flag = stream.get_flag(Flag.IDENTITY); + string? query_id = query_node.get_attribute("queryid"); + if (flag == null || query_id == null) { res.error = true; return res; } + flag.active_query_ids.add(query_id); // Build and send query Iq.Stanza iq = new Iq.Stanza.set(query_node) { to=mam_server }; @@ -93,6 +94,11 @@ public class Module : XmppStreamModule { if ((res.first == null) != (res.last == null)) { res.malformed = true; return res; } res.complete = fin_node.get_attribute_bool("complete", false, ns); + Idle.add(() => { + flag.active_query_ids.remove(query_id); + return Source.REMOVE; + }, Priority.LOW); + return res; } @@ -104,7 +110,8 @@ public class ReceivedPipelineListener : StanzaListener { public override string[] after_actions { get { return after_actions_const; } } public override async bool run(XmppStream stream, MessageStanza message) { - if (stream.get_flag(Flag.IDENTITY) == null) return false; + Flag? flag = stream.get_flag(Flag.IDENTITY); + if (flag == null) return false; StanzaNode? message_node = message.stanza.get_deep_subnode(NS_VER(stream) + ":result", StanzaForwarding.NS_URI + ":forwarded", Xmpp.NS_URI + ":message"); if (message_node != null) { @@ -112,6 +119,28 @@ public class ReceivedPipelineListener : StanzaListener { DateTime? datetime = DelayedDelivery.get_time_for_node(forward_node); string? mam_id = message.stanza.get_deep_attribute(NS_VER(stream) + ":result", NS_VER(stream) + ":id"); string? query_id = message.stanza.get_deep_attribute(NS_VER(stream) + ":result", NS_VER(stream) + ":queryid"); + + if (query_id == null) { + warning("Received MAM message without queryid from %s, ignoring", message.from.to_string()); + return true; + } + + if (!flag.active_query_ids.contains(query_id)) { + warning("Received MAM message from %s with unknown query id %s, ignoring", message.from.to_string(), query_id ?? ""); + return true; + } + Jid? inner_from = null; + try { + inner_from = new Jid(message_node.get_attribute("from")); + } catch (InvalidJidError e) { + warning("Received MAM message with invalid from attribute in forwarded message from %s, ignoring", message.from.to_string()); + return true; + } + if (!message.from.equals(stream.get_flag(Bind.Flag.IDENTITY).my_jid.bare_jid) && !message.from.equals_bare(inner_from)) { + warning("Received MAM message from %s illegally impersonating %s, ignoring", message.from.to_string(), inner_from.to_string()); + return true; + } + message.add_flag(new MessageFlag(message.from, datetime, mam_id, query_id)); message.stanza = message_node; @@ -124,6 +153,7 @@ public class ReceivedPipelineListener : StanzaListener { public class Flag : XmppStreamFlag { public static FlagIdentity IDENTITY = new FlagIdentity(NS_URI, "message_archive_management"); public bool cought_up { get; set; default=false; } + public Gee.Set active_query_ids { get; set; default = new HashSet(); } public string ns_ver; public Flag(string ns_ver) { From 4e1311dfa9944fc04089037783db6a0a6eef7345 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Mon, 20 Mar 2023 15:40:44 -0600 Subject: [PATCH 14/27] Improve database performance while reconnecting and syncing Also move some tasks to low priority idle queue so they won't block UI updates --- libdino/src/service/database.vala | 7 ++++++- .../conversation_selector_row.vala | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index 96b3b82d..6b3f5e6a 100644 --- a/libdino/src/service/database.vala +++ b/libdino/src/service/database.vala @@ -7,7 +7,7 @@ using Dino.Entities; namespace Dino { public class Database : Qlite.Database { - private const int VERSION = 25; + private const int VERSION = 26; public class AccountTable : Table { public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; @@ -93,6 +93,11 @@ public class Database : Qlite.Database { // deduplication index("message_account_counterpart_stanzaid_idx", {account_id, counterpart_id, stanza_id}); + index("message_account_counterpart_serverid_idx", {account_id, counterpart_id, server_id}); + + // message by marked + index("message_account_marked_idx", {account_id, marked}); + fts({body}); } } diff --git a/main/src/ui/conversation_selector/conversation_selector_row.vala b/main/src/ui/conversation_selector/conversation_selector_row.vala index 6ef61b3c..1bcf6962 100644 --- a/main/src/ui/conversation_selector/conversation_selector_row.vala +++ b/main/src/ui/conversation_selector/conversation_selector_row.vala @@ -225,7 +225,21 @@ public class ConversationSelectorRow : ListBoxRow { label.attributes = copy; } + private bool update_read_pending = false; + private bool update_read_pending_force = false; protected void update_read(bool force_update = false) { + if (force_update) update_read_pending_force = true; + if (update_read_pending) return; + update_read_pending = true; + Idle.add(() => { + update_read_pending = false; + update_read_pending_force = false; + update_read_idle(update_read_pending_force); + return Source.REMOVE; + }, Priority.LOW); + } + + private void update_read_idle(bool force_update = false) { int current_num_unread = stream_interactor.get_module(ChatInteraction.IDENTITY).get_num_unread(conversation); if (num_unread == current_num_unread && !force_update) return; num_unread = current_num_unread; From 57d47b95754dbdef5f568e233fcb90b181b3c75c Mon Sep 17 00:00:00 2001 From: Sebastian Krzyszkowiak Date: Mon, 13 Mar 2023 05:10:59 +0100 Subject: [PATCH 15/27] data: Set StartupNotify to true in .desktop file GTK handles startup notifications, so advertise it in desktop file. This allows splash screens and other startup indications in DEs to work. --- main/data/im.dino.Dino.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/data/im.dino.Dino.desktop b/main/data/im.dino.Dino.desktop index 90f3e38f..8c04dc34 100644 --- a/main/data/im.dino.Dino.desktop +++ b/main/data/im.dino.Dino.desktop @@ -5,7 +5,7 @@ GenericName=Jabber/XMPP Client Keywords=chat;talk;im;message;xmpp;jabber; Exec=dino %U Icon=im.dino.Dino -StartupNotify=false +StartupNotify=true Terminal=false Type=Application Categories=GTK;Network;Chat;InstantMessaging; From ecf94dd2e699302dabe1a0e0800c36e5455934f0 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Mon, 13 Mar 2023 11:19:43 +0100 Subject: [PATCH 16/27] Remove gspell 7e7dcedaf ported from GTK3 to GTK4. It also removed gspell from main/CMakeLists.txt. I assume that gspell is not needed anymore and we can thus remove the requirement from the CI and the cmake file as well. --- .github/workflows/build.yml | 2 +- cmake/FindGspell.cmake | 14 -------------- 2 files changed, 1 insertion(+), 15 deletions(-) delete mode 100644 cmake/FindGspell.cmake diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e9a7dd14..17ba5769 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: - uses: actions/checkout@v2 - run: sudo apt-get update - run: sudo apt-get remove libunwind-14-dev - - run: sudo apt-get install -y build-essential gettext cmake valac libgee-0.8-dev libsqlite3-dev libgtk-4-dev libnotify-dev libgpgme-dev libsoup2.4-dev libgcrypt20-dev libqrencode-dev libgspell-1-dev libnice-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libsrtp2-dev libwebrtc-audio-processing-dev libadwaita-1-dev + - run: sudo apt-get install -y build-essential gettext cmake valac libgee-0.8-dev libsqlite3-dev libgtk-4-dev libnotify-dev libgpgme-dev libsoup2.4-dev libgcrypt20-dev libqrencode-dev libnice-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libsrtp2-dev libwebrtc-audio-processing-dev libadwaita-1-dev - run: ./configure --with-tests --with-libsignal-in-tree - run: make - run: build/xmpp-vala-test diff --git a/cmake/FindGspell.cmake b/cmake/FindGspell.cmake deleted file mode 100644 index de29ed1d..00000000 --- a/cmake/FindGspell.cmake +++ /dev/null @@ -1,14 +0,0 @@ -include(PkgConfigWithFallback) -find_pkg_config_with_fallback(Gspell - PKG_CONFIG_NAME gspell-1 - LIB_NAMES gspell-1 - INCLUDE_NAMES gspell.h - INCLUDE_DIR_SUFFIXES gspell-1 gspell-1/gspell - DEPENDS GTK3 -) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(Gspell - REQUIRED_VARS Gspell_LIBRARY - VERSION_VAR Gspell_VERSION) - From 444275a99d659337bc808e5d6b75fc0ae40de499 Mon Sep 17 00:00:00 2001 From: Sebastian Krzyszkowiak Date: Mon, 13 Mar 2023 19:52:18 +0100 Subject: [PATCH 17/27] FreeDesktopNotifier: Set notification categories This provides notifications servers some context on how to handle the notification. --- main/src/ui/notifier_freedesktop.vala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/main/src/ui/notifier_freedesktop.vala b/main/src/ui/notifier_freedesktop.vala index 8af96975..3f390102 100644 --- a/main/src/ui/notifier_freedesktop.vala +++ b/main/src/ui/notifier_freedesktop.vala @@ -94,6 +94,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { HashTable hash_table = new HashTable(null, null); hash_table["image-data"] = yield get_conversation_icon(conversation); hash_table["desktop-entry"] = new Variant.string(Dino.Application.get_default().get_application_id()); + hash_table["category"] = new Variant.string("im.received"); string[] actions = new string[] {"default", "Open conversation"}; try { uint32 notification_id = yield dbus_notifications.notify("Dino", replace_id, "", conversation_display_name, body, actions, hash_table, -1); @@ -120,6 +121,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { hash_table["sound-name"] = new Variant.string("phone-incoming-call"); hash_table["urgency"] = new Variant.byte(2); hash_table["desktop-entry"] = new Variant.string(Dino.Application.get_default().get_application_id()); + hash_table["category"] = new Variant.string("im"); string[] actions = new string[] {"default", "Open conversation", "reject", _("Reject"), "accept", _("Accept")}; try { uint32 notification_id = yield dbus_notifications.notify("Dino", 0, "", summary, body, actions, hash_table, 0); @@ -158,6 +160,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { HashTable hash_table = new HashTable(null, null); hash_table["image-data"] = yield get_conversation_icon(conversation); hash_table["desktop-entry"] = new Variant.string(Dino.Application.get_default().get_application_id()); + hash_table["category"] = new Variant.string("im"); string[] actions = new string[] {"default", "Open conversation", "accept", _("Accept"), "deny", _("Deny")}; try { uint32 notification_id = yield dbus_notifications.notify("Dino", 0, "", summary, body, actions, hash_table, -1); @@ -197,6 +200,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { HashTable hash_table = new HashTable(null, null); hash_table["desktop-entry"] = new Variant.string(Dino.Application.get_default().get_application_id()); + hash_table["category"] = new Variant.string("im.error"); try { yield dbus_notifications.notify("Dino", 0, "im.dino.Dino", summary, body, new string[]{}, hash_table, -1); } catch (Error e) { @@ -217,6 +221,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { HashTable hash_table = new HashTable(null, null); hash_table["image-data"] = yield get_conversation_icon(direct_conversation); hash_table["desktop-entry"] = new Variant.string(Dino.Application.get_default().get_application_id()); + hash_table["category"] = new Variant.string("im"); string[] actions = new string[] {"default", "", "reject", _("Reject"), "accept", _("Accept")}; try { @@ -250,6 +255,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { HashTable hash_table = new HashTable(null, null); hash_table["image-data"] = yield get_conversation_icon(conversation); hash_table["desktop-entry"] = new Variant.string(Dino.Application.get_default().get_application_id()); + hash_table["category"] = new Variant.string("im"); string[] actions = new string[] {"deny", _("Deny"), "accept", _("Accept")}; try { From adb2b58b61457df1791ce1ed9b7bb75707d809fb Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 18 Mar 2023 17:23:46 +0200 Subject: [PATCH 18/27] Fix a crash if a message subnode is not found in a carbon Fixes #1392 --- xmpp-vala/src/module/xep/0280_message_carbons.vala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/xmpp-vala/src/module/xep/0280_message_carbons.vala b/xmpp-vala/src/module/xep/0280_message_carbons.vala index 5b2dcb78..f7863188 100644 --- a/xmpp-vala/src/module/xep/0280_message_carbons.vala +++ b/xmpp-vala/src/module/xep/0280_message_carbons.vala @@ -58,6 +58,10 @@ public class ReceivedPipelineListener : StanzaListener { warning("Received alleged carbon message from %s, ignoring", message.from.to_string()); return true; } + if (message_node == null) { + warning("Received a carbon message with no message subnode in jabber:client namespace from %s, ignoring", message.from.to_string()); + return true; + } if (received_node != null) { message.add_flag(new MessageFlag(MessageFlag.TYPE_RECEIVED)); } else if (sent_node != null) { From 6690d8e4a497eb1f18b43d6147676e08bb298dde Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 22 Mar 2023 12:29:58 -0600 Subject: [PATCH 19/27] Bind soup session lifetime to File provider/sender lifetime Required since libsoup 3.4. Fixes #1395 --- plugins/http-files/src/file_provider.vala | 7 +++---- plugins/http-files/src/file_sender.vala | 5 +++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/http-files/src/file_provider.vala b/plugins/http-files/src/file_provider.vala index 98c1d95c..34c3a48a 100644 --- a/plugins/http-files/src/file_provider.vala +++ b/plugins/http-files/src/file_provider.vala @@ -10,13 +10,16 @@ public class FileProvider : Dino.FileProvider, Object { private StreamInteractor stream_interactor; private Dino.Database dino_db; + private Soup.Session session; private static Regex http_url_regex = /^https?:\/\/([^\s#]*)$/; // Spaces are invalid in URLs and we can't use fragments for downloads private static Regex omemo_url_regex = /^aesgcm:\/\/(.*)#(([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44})$/; public FileProvider(StreamInteractor stream_interactor, Dino.Database dino_db) { this.stream_interactor = stream_interactor; this.dino_db = dino_db; + this.session = new Soup.Session(); + session.user_agent = @"Dino/$(Dino.get_short_version()) "; stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new ReceivedMessageListener(this)); } @@ -114,8 +117,6 @@ public class FileProvider : Dino.FileProvider, Object { HttpFileReceiveData? http_receive_data = receive_data as HttpFileReceiveData; if (http_receive_data == null) return file_meta; - var session = new Soup.Session(); - session.user_agent = @"Dino/$(Dino.get_short_version()) "; var head_message = new Soup.Message("HEAD", http_receive_data.url); head_message.request_headers.append("Accept-Encoding", "identity"); @@ -150,8 +151,6 @@ public class FileProvider : Dino.FileProvider, Object { HttpFileReceiveData? http_receive_data = receive_data as HttpFileReceiveData; if (http_receive_data == null) assert(false); - var session = new Soup.Session(); - session.user_agent = @"Dino/$(Dino.get_short_version()) "; var get_message = new Soup.Message("GET", http_receive_data.url); try { diff --git a/plugins/http-files/src/file_sender.vala b/plugins/http-files/src/file_sender.vala index a39d695b..957611d0 100644 --- a/plugins/http-files/src/file_sender.vala +++ b/plugins/http-files/src/file_sender.vala @@ -7,12 +7,15 @@ namespace Dino.Plugins.HttpFiles { public class HttpFileSender : FileSender, Object { private StreamInteractor stream_interactor; private Database db; + private Soup.Session session; private HashMap max_file_sizes = new HashMap(Account.hash_func, Account.equals_func); public HttpFileSender(StreamInteractor stream_interactor, Database db) { this.stream_interactor = stream_interactor; this.db = db; + this.session = new Soup.Session(); + session.user_agent = @"Dino/$(Dino.get_short_version()) "; stream_interactor.stream_negotiated.connect(on_stream_negotiated); stream_interactor.get_module(MessageProcessor.IDENTITY).build_message_stanza.connect(check_add_oob); } @@ -90,8 +93,6 @@ public class HttpFileSender : FileSender, Object { Xmpp.XmppStream? stream = stream_interactor.get_stream(file_transfer.account); if (stream == null) return; - var session = new Soup.Session(); - session.user_agent = @"Dino/$(Dino.get_short_version()) "; var put_message = new Soup.Message("PUT", file_send_data.url_up); #if SOUP_3_0 put_message.set_request_body(file_meta.mime_type, file_transfer.input_stream, (ssize_t) file_meta.size); From ef8fb0e94ce79d5fde2943e433ad0422eb7f70ec Mon Sep 17 00:00:00 2001 From: Marvin W Date: Thu, 23 Mar 2023 10:13:30 -0600 Subject: [PATCH 20/27] Check sender of bookmark:1 updates --- xmpp-vala/src/module/xep/0402_bookmarks2.vala | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/xmpp-vala/src/module/xep/0402_bookmarks2.vala b/xmpp-vala/src/module/xep/0402_bookmarks2.vala index 406f37f4..d1e53e6e 100644 --- a/xmpp-vala/src/module/xep/0402_bookmarks2.vala +++ b/xmpp-vala/src/module/xep/0402_bookmarks2.vala @@ -68,6 +68,11 @@ public class Module : BookmarksProvider, XmppStreamModule { } private void on_pupsub_item(XmppStream stream, Jid jid, string id, StanzaNode? node) { + if (!jid.equals(stream.get_flag(Bind.Flag.IDENTITY).my_jid.bare_jid)) { + warning("Received alleged bookmarks:1 item from %s, ignoring", jid.to_string()); + return; + } + Conference conference = parse_item_node(node, id); Flag? flag = stream.get_flag(Flag.IDENTITY); if (flag != null) { @@ -77,6 +82,11 @@ public class Module : BookmarksProvider, XmppStreamModule { } private void on_pupsub_retract(XmppStream stream, Jid jid, string id) { + if (!jid.equals(stream.get_flag(Bind.Flag.IDENTITY).my_jid.bare_jid)) { + warning("Received alleged bookmarks:1 retract from %s, ignoring", jid.to_string()); + return; + } + try { Jid jid_parsed = new Jid(id); Flag? flag = stream.get_flag(Flag.IDENTITY); From 65efaca6fdcbc788060a0ecc4f7e10fbf5b79669 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Thu, 23 Mar 2023 10:17:42 -0600 Subject: [PATCH 21/27] Fix images from another client in our account not being displayed right away --- libdino/src/service/file_manager.vala | 9 +++++++-- main/src/ui/conversation_content_view/file_widget.vala | 10 +++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/libdino/src/service/file_manager.vala b/libdino/src/service/file_manager.vala index 89ae1ce1..984fe5fd 100644 --- a/libdino/src/service/file_manager.vala +++ b/libdino/src/service/file_manager.vala @@ -276,8 +276,13 @@ public class FileManager : StreamInteractionModule, Object { file_transfer.ourpart = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account) ?? conversation.account.bare_jid; file_transfer.direction = from.equals(file_transfer.ourpart) ? FileTransfer.DIRECTION_SENT : FileTransfer.DIRECTION_RECEIVED; } else { - file_transfer.ourpart = conversation.account.full_jid; - file_transfer.direction = from.equals_bare(file_transfer.ourpart) ? FileTransfer.DIRECTION_SENT : FileTransfer.DIRECTION_RECEIVED; + if (from.equals_bare(conversation.account.bare_jid)) { + file_transfer.ourpart = from; + file_transfer.direction = FileTransfer.DIRECTION_SENT; + } else { + file_transfer.ourpart = conversation.account.full_jid; + file_transfer.direction = FileTransfer.DIRECTION_RECEIVED; + } } file_transfer.time = time; file_transfer.local_time = local_time; diff --git a/main/src/ui/conversation_content_view/file_widget.vala b/main/src/ui/conversation_content_view/file_widget.vala index 785acf7d..02c9407a 100644 --- a/main/src/ui/conversation_content_view/file_widget.vala +++ b/main/src/ui/conversation_content_view/file_widget.vala @@ -108,7 +108,7 @@ public class FileWidget : SizeRequestBox { } catch (Error e) { } } - if (state != State.DEFAULT) { + if (!show_image() && state != State.DEFAULT) { if (content != null) this.remove(content); FileDefaultWidget default_file_widget = new FileDefaultWidget(); default_widget_controller = new FileDefaultWidgetController(default_file_widget); @@ -121,8 +121,12 @@ public class FileWidget : SizeRequestBox { private bool show_image() { if (file_transfer.mime_type == null) return false; - if (file_transfer.state != FileTransfer.State.COMPLETE && - !(file_transfer.direction == FileTransfer.DIRECTION_SENT && file_transfer.state == FileTransfer.State.IN_PROGRESS)) { + + // If the image is being sent by this client, we already have the file + bool in_progress_from_us = file_transfer.direction == FileTransfer.DIRECTION_SENT && + file_transfer.ourpart.equals(file_transfer.account.full_jid) && + file_transfer.state == FileTransfer.State.IN_PROGRESS; + if (file_transfer.state != FileTransfer.State.COMPLETE && !in_progress_from_us) { return false; } From b617bf7cc4ac2aa083bcf889816d0479fa353e10 Mon Sep 17 00:00:00 2001 From: hrxi Date: Mon, 20 Mar 2023 02:00:27 +0100 Subject: [PATCH 22/27] Make members of `Plugins.Registry` public instead of internal They are being used from outside the library. --- libdino/src/plugin/registry.vala | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/libdino/src/plugin/registry.vala b/libdino/src/plugin/registry.vala index e2801508..6c0234ca 100644 --- a/libdino/src/plugin/registry.vala +++ b/libdino/src/plugin/registry.vala @@ -3,14 +3,14 @@ using Gee; namespace Dino.Plugins { public class Registry { - internal HashMap encryption_list_entries = new HashMap(); - internal HashMap call_encryption_entries = new HashMap(); - internal ArrayList account_settings_entries = new ArrayList(); - internal ArrayList contact_details_entries = new ArrayList(); - internal Map text_commands = new HashMap(); - internal Gee.List conversation_addition_populators = new ArrayList(); - internal Gee.List notification_populators = new ArrayList(); - internal Gee.Collection conversation_titlebar_entries = new Gee.TreeSet((a, b) => { + public HashMap encryption_list_entries = new HashMap(); + public HashMap call_encryption_entries = new HashMap(); + public ArrayList account_settings_entries = new ArrayList(); + public ArrayList contact_details_entries = new ArrayList(); + public Map text_commands = new HashMap(); + public Gee.List conversation_addition_populators = new ArrayList(); + public Gee.List notification_populators = new ArrayList(); + public Gee.Collection conversation_titlebar_entries = new Gee.TreeSet((a, b) => { return (int)(a.order - b.order); }); public VideoCallPlugin? video_call_plugin; From 5a90e793ddec723677f2b715b967f29f046e16d2 Mon Sep 17 00:00:00 2001 From: hrxi Date: Mon, 20 Mar 2023 02:01:22 +0100 Subject: [PATCH 23/27] First steps of meson support Basic configuration of qlite, xmpp-vala, the Dino library and the Dino application are supported. There's no support for the plugins. This e.g. enables using the Vala language server. --- libdino/CMakeLists.txt | 1 + libdino/meson.build | 86 ++++++++++++++++++++++ libdino/src/application.vala | 1 - libdino/src/version.vala | 6 ++ libdino/version.py | 36 ++++++++++ main/data/gresource.xml | 74 +++++++++++++++++++ main/meson.build | 104 +++++++++++++++++++++++++++ meson.build | 23 ++++++ meson_options.txt | 1 + qlite/meson.build | 22 ++++++ xmpp-vala/meson.build | 133 +++++++++++++++++++++++++++++++++++ 11 files changed, 486 insertions(+), 1 deletion(-) create mode 100644 libdino/meson.build create mode 100644 libdino/src/version.vala create mode 100644 libdino/version.py create mode 100644 main/data/gresource.xml create mode 100644 main/meson.build create mode 100644 meson.build create mode 100644 meson_options.txt create mode 100644 qlite/meson.build create mode 100644 xmpp-vala/meson.build diff --git a/libdino/CMakeLists.txt b/libdino/CMakeLists.txt index 5aa4035f..3c184cfd 100644 --- a/libdino/CMakeLists.txt +++ b/libdino/CMakeLists.txt @@ -9,6 +9,7 @@ find_packages(LIBDINO_PACKAGES REQUIRED vala_precompile(LIBDINO_VALA_C SOURCES src/application.vala + src/version.vala src/dbus/login1.vala src/dbus/notifications.vala diff --git a/libdino/meson.build b/libdino/meson.build new file mode 100644 index 00000000..0ebaff33 --- /dev/null +++ b/libdino/meson.build @@ -0,0 +1,86 @@ +# version_vala +dot_git = meson.current_source_dir() / '../.git' +version_file = meson.current_source_dir() / '../VERSION' +command = [prog_python, files('version.py'), version_file, '@OUTPUT@', '--git-repo', meson.current_source_dir()] +if prog_git.found() + command += ['--git', prog_git] +endif +depend_files = [] +if fs.exists(dot_git) + depend_files += [dot_git] +endif +if fs.exists(version_file) + depend_files += [version_file] +endif +version_vala = custom_target('libdino_version_vala', command: command, output: 'version.vala', depend_files: depend_files) + +# libdino +dependencies = [ + dep_gdk_pixbuf, + dep_gee, + dep_gio, + dep_glib, + dep_gmodule, + dep_qlite, + dep_xmpp_vala +] +sources = files( + 'src/application.vala', + 'src/dbus/login1.vala', + 'src/dbus/notifications.vala', + 'src/dbus/upower.vala', + 'src/entity/account.vala', + 'src/entity/call.vala', + 'src/entity/conversation.vala', + 'src/entity/encryption.vala', + 'src/entity/file_transfer.vala', + 'src/entity/message.vala', + 'src/entity/settings.vala', + 'src/plugin/interfaces.vala', + 'src/plugin/loader.vala', + 'src/plugin/registry.vala', + 'src/service/avatar_manager.vala', + 'src/service/blocking_manager.vala', + 'src/service/call_store.vala', + 'src/service/call_state.vala', + 'src/service/call_peer_state.vala', + 'src/service/calls.vala', + 'src/service/chat_interaction.vala', + 'src/service/connection_manager.vala', + 'src/service/content_item_store.vala', + 'src/service/conversation_manager.vala', + 'src/service/counterpart_interaction_manager.vala', + 'src/service/database.vala', + 'src/service/entity_capabilities_storage.vala', + 'src/service/entity_info.vala', + 'src/service/fallback_body.vala', + 'src/service/file_manager.vala', + 'src/service/file_transfer_storage.vala', + 'src/service/history_sync.vala', + 'src/service/jingle_file_transfers.vala', + 'src/service/message_correction.vala', + 'src/service/message_processor.vala', + 'src/service/message_storage.vala', + 'src/service/module_manager.vala', + 'src/service/muc_manager.vala', + 'src/service/notification_events.vala', + 'src/service/presence_manager.vala', + 'src/service/replies.vala', + 'src/service/reactions.vala', + 'src/service/registration.vala', + 'src/service/roster_manager.vala', + 'src/service/search_processor.vala', + 'src/service/stream_interactor.vala', + 'src/service/util.vala', + 'src/util/display_name.vala', + 'src/util/util.vala', + 'src/util/weak_map.vala', +) +sources += [version_vala] +c_args = [ + '-DDINO_SYSTEM_LIBDIR_NAME="@0@"'.format(get_option('prefix') / get_option('libdir')), + '-DDINO_SYSTEM_PLUGIN_DIR="@0@"'.format(get_option('prefix') / get_option('plugindir')), + '-DG_LOG_DOMAIN="libdino"', +] +lib_dino = library('dino', sources, c_args: c_args, include_directories: include_directories('src'), dependencies: dependencies) +dep_dino = declare_dependency(link_with: lib_dino, include_directories: include_directories('.', 'src')) diff --git a/libdino/src/application.vala b/libdino/src/application.vala index 490cd40c..5e58e364 100644 --- a/libdino/src/application.vala +++ b/libdino/src/application.vala @@ -2,7 +2,6 @@ using Dino.Entities; namespace Dino { -extern const string VERSION; public string get_version() { return VERSION; } public string get_short_version() { if (!VERSION.contains("~")) return VERSION; diff --git a/libdino/src/version.vala b/libdino/src/version.vala new file mode 100644 index 00000000..0fdc0145 --- /dev/null +++ b/libdino/src/version.vala @@ -0,0 +1,6 @@ +// Not used in Meson. +namespace Dino { + +extern const string VERSION; + +} diff --git a/libdino/version.py b/libdino/version.py new file mode 100644 index 00000000..d34db6a8 --- /dev/null +++ b/libdino/version.py @@ -0,0 +1,36 @@ +import argparse +import subprocess +VERSION_VALA = """\ +namespace Dino {{ + +public const string VERSION = "{}"; + +}} +""" + +def compute_version(file, git_repo, git): + try: + with open(file) as f: + return f.read().strip() + except FileNotFoundError: + pass + return subprocess.check_output([git, "describe", "--tags"], cwd=git_repo, text=True).strip() + +def generate_version_vala(version): + if "\\" in version or "\"" in version: + raise ValueError(f"invalid version {version!r}") + return VERSION_VALA.format(version) + +def main(): + p = argparse.ArgumentParser(description="Compute the Dino version") + p.add_argument("--git-repo", help="Path to checked out git repository") + p.add_argument("--git", help="Path to git executable", default="git") + p.add_argument("version_file", metavar="VERSION_FILE", help="Use this file's contents as version if the file exists") + p.add_argument("output", metavar="OUTPUT", help="Vala file to output to") + args = p.parse_args() + out = generate_version_vala(compute_version(args.version_file, args.git_repo, args.git)) + with open(args.output, "w") as f: + f.write(out) + +if __name__ == "__main__": + main() diff --git a/main/data/gresource.xml b/main/data/gresource.xml new file mode 100644 index 00000000..6d9febab --- /dev/null +++ b/main/data/gresource.xml @@ -0,0 +1,74 @@ + + + + add_conversation/add_contact_dialog.ui + add_conversation/add_groupchat_dialog.ui + add_conversation/conference_details_fragment.ui + add_conversation/list_row.ui + add_conversation/select_jid_fragment.ui + call_widget.ui + chat_input.ui + contact_details_dialog.ui + conversation_content_view/item_metadata_header.ui + conversation_content_view/view.ui + conversation_item_widget.ui + conversation_list_titlebar.ui + conversation_list_titlebar_csd.ui + conversation_row.ui + conversation_view.ui + dino-conversation-list-placeholder-arrow.svg + file_default_widget.ui + file_send_overlay.ui + global_search.ui + icons/scalable/actions/dino-account-plus-symbolic.svg + icons/scalable/actions/dino-emoticon-add-symbolic.svg + icons/scalable/actions/dino-emoticon-symbolic.svg + icons/scalable/actions/dino-qr-code-symbolic.svg + icons/scalable/apps/im.dino.Dino-symbolic.svg + icons/scalable/apps/im.dino.Dino.svg + icons/scalable/devices/dino-device-desktop-symbolic.svg + icons/scalable/devices/dino-device-phone-symbolic.svg + icons/scalable/devices/dino-phone-hangup-symbolic.svg + icons/scalable/devices/dino-phone-in-talk-symbolic.svg + icons/scalable/devices/dino-phone-missed-symbolic.svg + icons/scalable/devices/dino-phone-ring-symbolic.svg + icons/scalable/devices/dino-phone-symbolic.svg + icons/scalable/mimetypes/dino-file-document-symbolic.svg + icons/scalable/mimetypes/dino-file-download-symbolic.svg + icons/scalable/mimetypes/dino-file-image-symbolic.svg + icons/scalable/mimetypes/dino-file-music-symbolic.svg + icons/scalable/mimetypes/dino-file-symbolic.svg + icons/scalable/mimetypes/dino-file-table-symbolic.svg + icons/scalable/mimetypes/dino-file-video-symbolic.svg + icons/scalable/status/dino-double-tick-symbolic.svg + icons/scalable/status/dino-microphone-off-symbolic.svg + icons/scalable/status/dino-microphone-symbolic.svg + icons/scalable/status/dino-party-popper-symbolic.svg + icons/scalable/status/dino-security-high-symbolic.svg + icons/scalable/status/dino-status-away.svg + icons/scalable/status/dino-status-chat.svg + icons/scalable/status/dino-status-dnd.svg + icons/scalable/status/dino-status-online.svg + icons/scalable/status/dino-tick-symbolic.svg + icons/scalable/status/dino-video-off-symbolic.svg + icons/scalable/status/dino-video-symbolic.svg + manage_accounts/account_row.ui + manage_accounts/add_account_dialog.ui + manage_accounts/dialog.ui + menu_add.ui + menu_app.ui + menu_conversation.ui + menu_encryption.ui + message_item_widget_edit_mode.ui + occupant_list.ui + occupant_list_item.ui + quote.ui + search_autocomplete.ui + settings_dialog.ui + shortcuts.ui + style-dark.css + style.css + unified_main_content.ui + unified_window_placeholder.ui + + diff --git a/main/meson.build b/main/meson.build new file mode 100644 index 00000000..a38e15b8 --- /dev/null +++ b/main/meson.build @@ -0,0 +1,104 @@ +dependencies = [ + dep_dino, + dep_gee, + dep_glib, + dep_gmodule, + dep_gtk4, + dep_icu_uc, + dep_libadwaita, + dep_m, + dep_qlite, + dep_xmpp_vala, +] +sources = files( + 'src/main.vala', + 'src/ui/add_conversation/add_conference_dialog.vala', + 'src/ui/add_conversation/add_contact_dialog.vala', + 'src/ui/add_conversation/add_groupchat_dialog.vala', + 'src/ui/add_conversation/conference_details_fragment.vala', + 'src/ui/add_conversation/conference_list.vala', + 'src/ui/add_conversation/list_row.vala', + 'src/ui/add_conversation/roster_list.vala', + 'src/ui/add_conversation/select_contact_dialog.vala', + 'src/ui/add_conversation/select_jid_fragment.vala', + 'src/ui/application.vala', + 'src/ui/call_window/audio_settings_popover.vala', + 'src/ui/call_window/call_bottom_bar.vala', + 'src/ui/call_window/call_connection_details_window.vala', + 'src/ui/call_window/call_encryption_button.vala', + 'src/ui/call_window/call_window.vala', + 'src/ui/call_window/call_window_controller.vala', + 'src/ui/call_window/participant_widget.vala', + 'src/ui/call_window/video_settings_popover.vala', + 'src/ui/chat_input/chat_input_controller.vala', + 'src/ui/chat_input/chat_text_view.vala', + 'src/ui/chat_input/encryption_button.vala', + 'src/ui/chat_input/occupants_tab_completer.vala', + 'src/ui/chat_input/smiley_converter.vala', + 'src/ui/chat_input/view.vala', + 'src/ui/contact_details/blocking_provider.vala', + 'src/ui/contact_details/dialog.vala', + 'src/ui/contact_details/muc_config_form_provider.vala', + 'src/ui/contact_details/permissions_provider.vala', + 'src/ui/contact_details/settings_provider.vala', + 'src/ui/conversation_content_view/call_widget.vala', + 'src/ui/conversation_content_view/chat_state_populator.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_default_widget.vala', + 'src/ui/conversation_content_view/file_image_widget.vala', + 'src/ui/conversation_content_view/file_widget.vala', + 'src/ui/conversation_content_view/item_actions.vala', + 'src/ui/conversation_content_view/message_widget.vala', + 'src/ui/conversation_content_view/quote_widget.vala', + 'src/ui/conversation_content_view/reactions_widget.vala', + 'src/ui/conversation_content_view/subscription_notification.vala', + 'src/ui/conversation_list_titlebar.vala', + 'src/ui/conversation_selector/conversation_selector.vala', + 'src/ui/conversation_selector/conversation_selector_row.vala', + 'src/ui/conversation_titlebar/call_entry.vala', + 'src/ui/conversation_titlebar/conversation_titlebar.vala', + 'src/ui/conversation_titlebar/menu_entry.vala', + 'src/ui/conversation_titlebar/occupants_entry.vala', + 'src/ui/conversation_titlebar/search_entry.vala', + 'src/ui/conversation_view.vala', + 'src/ui/conversation_view_controller.vala', + 'src/ui/file_send_overlay.vala', + 'src/ui/global_search.vala', + 'src/ui/main_window.vala', + 'src/ui/main_window_controller.vala', + 'src/ui/manage_accounts/account_row.vala', + 'src/ui/manage_accounts/add_account_dialog.vala', + 'src/ui/manage_accounts/dialog.vala', + 'src/ui/notifier_freedesktop.vala', + 'src/ui/notifier_gnotifications.vala', + 'src/ui/occupant_menu/list.vala', + 'src/ui/occupant_menu/list_row.vala', + 'src/ui/occupant_menu/view.vala', + 'src/ui/settings_dialog.vala', + 'src/ui/util/accounts_combo_box.vala', + 'src/ui/util/config.vala', + 'src/ui/util/data_forms.vala', + 'src/ui/util/helper.vala', + 'src/ui/util/label_hybrid.vala', + 'src/ui/util/size_request_box.vala', + 'src/ui/util/sizing_bin.vala', + 'src/ui/widgets/avatar_picture.vala', + 'src/ui/widgets/date_separator.vala', + 'src/ui/widgets/fixed_ratio_picture.vala', + 'src/ui/widgets/natural_size_increase.vala', +) +sources += import('gnome').compile_resources( + 'dino-resources', + 'data/gresource.xml', + source_dir: 'data', +) + +c_args = [ + '-DG_LOG_DOMAIN="dino"', + '-DGETTEXT_PACKAGE="dino"', + '-DLOCALE_INSTALL_DIR="@0@"'.format(get_option('prefix') / get_option('localedir')), +] +exe_dino = executable('dino', sources, c_args: c_args, vala_args: ['--vapidir', meson.current_source_dir() / 'vapi'], dependencies: dependencies) diff --git a/meson.build b/meson.build new file mode 100644 index 00000000..aea22d57 --- /dev/null +++ b/meson.build @@ -0,0 +1,23 @@ +project('xmpp-vala', 'vala') + +fs = import('fs') +python = import('python') + +dep_gdk_pixbuf = dependency('gdk-pixbuf-2.0') +dep_gee = dependency('gee-0.8') +dep_gio = dependency('gio-2.0') +dep_glib = dependency('glib-2.0') +dep_gmodule = dependency('gmodule-2.0') +dep_gtk4 = dependency('gtk4') +dep_icu_uc = dependency('icu-uc') +dep_libadwaita = dependency('libadwaita-1') +dep_m = meson.get_compiler('c').find_library('m', required: false) +dep_sqlite3 = dependency('sqlite3', version: '>=3.24') + +prog_git = find_program('git', required: false) +prog_python = python.find_installation() + +subdir('qlite') +subdir('xmpp-vala') +subdir('libdino') +subdir('main') diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 00000000..6e47b7c8 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1 @@ +option('plugindir', type: 'string', value: 'lib/dino/plugins', description: 'Plugin directory for Dino plugins') diff --git a/qlite/meson.build b/qlite/meson.build new file mode 100644 index 00000000..714a4224 --- /dev/null +++ b/qlite/meson.build @@ -0,0 +1,22 @@ +dependencies = [ + dep_gee, + dep_glib, + dep_sqlite3, +] +sources = files( + 'src/column.vala', + 'src/database.vala', + 'src/delete_builder.vala', + 'src/insert_builder.vala', + 'src/query_builder.vala', + 'src/row.vala', + 'src/statement_builder.vala', + 'src/table.vala', + 'src/update_builder.vala', + 'src/upsert_builder.vala', +) +c_args = [ + '-DG_LOG_DOMAIN="qlite"', +] +lib_qlite = library('qlite', sources, c_args: c_args, vala_args: ['--vapidir', meson.current_source_dir() / 'vapi'], dependencies: dependencies) +dep_qlite = declare_dependency(link_with: lib_qlite, include_directories: include_directories('.')) diff --git a/xmpp-vala/meson.build b/xmpp-vala/meson.build new file mode 100644 index 00000000..3064339a --- /dev/null +++ b/xmpp-vala/meson.build @@ -0,0 +1,133 @@ +dependencies = [ + dep_gdk_pixbuf, + dep_gee, + dep_gio, + dep_glib, + dep_icu_uc, + dep_m, +] +sources = files( + 'src/core/direct_tls_xmpp_stream.vala', + 'src/core/io_xmpp_stream.vala', + 'src/core/module_flag.vala', + 'src/core/namespace_state.vala', + 'src/core/stanza_attribute.vala', + 'src/core/stanza_node.vala', + 'src/core/stanza_reader.vala', + 'src/core/stanza_writer.vala', + 'src/core/starttls_xmpp_stream.vala', + 'src/core/stream_connect.vala', + 'src/core/tls_xmpp_stream.vala', + 'src/core/xmpp_log.vala', + 'src/core/xmpp_stream.vala', + 'src/glib_fixes.vapi', + 'src/module/bind.vala', + 'src/module/bookmarks_provider.vala', + 'src/module/conference.vala', + 'src/module/iq/module.vala', + 'src/module/iq/stanza.vala', + 'src/module/jid.vala', + 'src/module/message/module.vala', + 'src/module/message/stanza.vala', + 'src/module/presence/flag.vala', + 'src/module/presence/module.vala', + 'src/module/presence/stanza.vala', + 'src/module/roster/flag.vala', + 'src/module/roster/item.vala', + 'src/module/roster/module.vala', + 'src/module/roster/versioning_module.vala', + 'src/module/sasl.vala', + 'src/module/session.vala', + 'src/module/stanza.vala', + 'src/module/stanza_error.vala', + 'src/module/stream_error.vala', + 'src/module/util.vala', + 'src/module/xep/0004_data_forms.vala', + 'src/module/xep/0030_service_discovery/flag.vala', + 'src/module/xep/0030_service_discovery/identity.vala', + 'src/module/xep/0030_service_discovery/info_result.vala', + 'src/module/xep/0030_service_discovery/item.vala', + 'src/module/xep/0030_service_discovery/items_result.vala', + 'src/module/xep/0030_service_discovery/module.vala', + 'src/module/xep/0045_muc/flag.vala', + 'src/module/xep/0045_muc/module.vala', + 'src/module/xep/0045_muc/status_code.vala', + 'src/module/xep/0047_in_band_bytestreams.vala', + 'src/module/xep/0048_bookmarks.vala', + 'src/module/xep/0048_conference.vala', + 'src/module/xep/0049_private_xml_storage.vala', + 'src/module/xep/0054_vcard/module.vala', + 'src/module/xep/0059_result_set_management.vala', + 'src/module/xep/0060_pubsub.vala', + 'src/module/xep/0065_socks5_bytestreams.vala', + 'src/module/xep/0066_out_of_band_data.vala', + 'src/module/xep/0077_in_band_registration.vala', + 'src/module/xep/0082_date_time_profiles.vala', + 'src/module/xep/0084_user_avatars.vala', + 'src/module/xep/0085_chat_state_notifications.vala', + 'src/module/xep/0115_entity_capabilities.vala', + 'src/module/xep/0166_jingle/component.vala', + 'src/module/xep/0166_jingle/content.vala', + 'src/module/xep/0166_jingle/content_description.vala', + 'src/module/xep/0166_jingle/content_node.vala', + 'src/module/xep/0166_jingle/content_security.vala', + 'src/module/xep/0166_jingle/content_transport.vala', + 'src/module/xep/0166_jingle/jingle_flag.vala', + 'src/module/xep/0166_jingle/jingle_module.vala', + 'src/module/xep/0166_jingle/jingle_structs.vala', + 'src/module/xep/0166_jingle/reason_element.vala', + 'src/module/xep/0166_jingle/session.vala', + 'src/module/xep/0166_jingle/session_info.vala', + 'src/module/xep/0167_jingle_rtp/content_parameters.vala', + 'src/module/xep/0167_jingle_rtp/content_type.vala', + 'src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala', + 'src/module/xep/0167_jingle_rtp/payload_type.vala', + 'src/module/xep/0167_jingle_rtp/session_info_type.vala', + 'src/module/xep/0167_jingle_rtp/stream.vala', + 'src/module/xep/0176_jingle_ice_udp/candidate.vala', + 'src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala', + 'src/module/xep/0176_jingle_ice_udp/transport_parameters.vala', + 'src/module/xep/0177_jingle_raw_udp.vala', + 'src/module/xep/0184_message_delivery_receipts.vala', + 'src/module/xep/0191_blocking_command.vala', + 'src/module/xep/0198_stream_management.vala', + 'src/module/xep/0199_ping.vala', + 'src/module/xep/0203_delayed_delivery.vala', + 'src/module/xep/0215_external_service_discovery.vala', + 'src/module/xep/0234_jingle_file_transfer.vala', + 'src/module/xep/0249_direct_muc_invitations.vala', + 'src/module/xep/0260_jingle_socks5_bytestreams.vala', + 'src/module/xep/0261_jingle_in_band_bytestreams.vala', + 'src/module/xep/0272_muji.vala', + 'src/module/xep/0280_message_carbons.vala', + 'src/module/xep/0297_stanza_forwarding.vala', + 'src/module/xep/0298_coin.vala', + 'src/module/xep/0308_last_message_correction.vala', + 'src/module/xep/0313_2_message_archive_management.vala', + 'src/module/xep/0313_message_archive_management.vala', + 'src/module/xep/0333_chat_markers.vala', + 'src/module/xep/0334_message_processing_hints.vala', + 'src/module/xep/0353_call_invite_message.vala', + 'src/module/xep/0353_jingle_message_initiation.vala', + 'src/module/xep/0359_unique_stable_stanza_ids.vala', + 'src/module/xep/0363_http_file_upload.vala', + 'src/module/xep/0380_explicit_encryption.vala', + 'src/module/xep/0384_omemo/omemo_decryptor.vala', + 'src/module/xep/0384_omemo/omemo_encryptor.vala', + 'src/module/xep/0391_jingle_encrypted_transports.vala', + 'src/module/xep/0392_consistent_color/consistent_color.vala', + 'src/module/xep/0392_consistent_color/hsluv.vala', + 'src/module/xep/0402_bookmarks2.vala', + 'src/module/xep/0410_muc_self_ping.vala', + 'src/module/xep/0421_occupant_ids.vala', + 'src/module/xep/0428_fallback_indication.vala', + 'src/module/xep/0444_reactions.vala', + 'src/module/xep/0461_replies.vala', + 'src/module/xep/pixbuf_storage.vala', + 'src/util.vala', +) +c_args = [ + '-DG_LOG_DOMAIN="xmpp-vala"', +] +lib_xmpp_vala = library('xmpp-vala', sources, c_args: c_args, vala_args: ['--vapidir', meson.current_source_dir() / 'vapi'], dependencies: dependencies) +dep_xmpp_vala = declare_dependency(link_with: lib_xmpp_vala, include_directories: include_directories('.')) From 32e535a79cc23c56f6ef602ef6271b14e3dc4ac4 Mon Sep 17 00:00:00 2001 From: hrxi Date: Mon, 20 Mar 2023 02:13:18 +0100 Subject: [PATCH 24/27] Add CI for the meson build --- .github/workflows/build.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 17ba5769..974f31c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,8 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v2 + with: + fetch-depth: 0 - run: sudo apt-get update - run: sudo apt-get remove libunwind-14-dev - run: sudo apt-get install -y build-essential gettext cmake valac libgee-0.8-dev libsqlite3-dev libgtk-4-dev libnotify-dev libgpgme-dev libsoup2.4-dev libgcrypt20-dev libqrencode-dev libnice-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libsrtp2-dev libwebrtc-audio-processing-dev libadwaita-1-dev @@ -12,3 +14,14 @@ jobs: - run: make - run: build/xmpp-vala-test - run: build/signal-protocol-vala-test + build-meson: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - run: sudo apt-get update + - run: sudo apt-get remove libunwind-14-dev + - run: sudo apt-get install -y build-essential gettext libadwaita-1-dev libgee-0.8-dev libgtk-4-dev libsqlite3-dev meson valac + - run: meson setup build + - run: meson compile -C build From b75b6062abc07b25d0dcd6717e168aa85be7cf4e Mon Sep 17 00:00:00 2001 From: Klemens Nanni Date: Thu, 2 Mar 2023 16:18:24 +0400 Subject: [PATCH 25/27] Always export symbols to fix startup on BSDs ``` $ dino (dino:38515): Gtk-ERROR **: 15:38:38.538: failed to add UI from resource /im/dino/Dino/unified_main_content.ui: .:26:1 Invalid object type 'DinoUiConversationSelector' Trace/BPT trap (core dumped) ``` This works on Linux because CMake itself links with `-rdynamic` by default as per its `Modules/Platform/Linux-*.cmake`. OpenBSD carries this as local patch, FreeBSD links with `--export-dynamics`. Just linking with `-rdynamic` also fixes it on OpenBSD, as expected. https://cmake.org/cmake/help/latest/prop_tgt/ENABLE_EXPORTS.html Fix #438. --- main/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 9ca7ce81..1a66597c 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -231,6 +231,7 @@ add_executable(dino ${MAIN_VALA_C} ${MAIN_GRESOURCES_TARGET}) add_dependencies(dino ${GETTEXT_PACKAGE}-translations) target_include_directories(dino PRIVATE src) target_link_libraries(dino libdino ${MAIN_PACKAGES}) +set_target_properties(dino PROPERTIES ENABLE_EXPORTS TRUE) if(WIN32) target_link_libraries(dino -mwindows) From d2ac7a8aeb97672496459444dc03bada3f2d6469 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Fri, 10 Feb 2023 23:59:36 +0100 Subject: [PATCH 26/27] Add Flatpak manifest --- im.dino.Dino.json | 75 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 im.dino.Dino.json diff --git a/im.dino.Dino.json b/im.dino.Dino.json new file mode 100644 index 00000000..b2344c61 --- /dev/null +++ b/im.dino.Dino.json @@ -0,0 +1,75 @@ +{ + "id": "im.dino.Dino", + "runtime": "org.gnome.Platform", + "runtime-version": "44", + "sdk": "org.gnome.Sdk", + "command": "dino", + "finish-args": [ + "--share=ipc", + "--socket=fallback-x11", + "--socket=wayland", + "--socket=pulseaudio", + "--socket=gpg-agent", + "--share=network", + "--device=all", + "--talk-name=org.freedesktop.Notifications" + ], + "modules": [ + { + "name": "libsignal-protocol-c", + "buildsystem": "cmake-ninja", + "config-opts": [ + "-DCMAKE_C_FLAGS=-fPIC" + ], + "cleanup": [ + "/include", + "/lib" + ], + "sources": [ + { + "type": "git", + "url": "https://github.com/mar-v-in/libsignal-protocol-c.git", + "tag": "v2.3.3.1" + } + ] + }, + { + "name": "qrencode", + "buildsystem": "cmake-ninja", + "cleanup": [ + "/bin", + "/include", + "/lib", + "/share/man" + ], + "config-opts": [ + "-DCMAKE_C_FLAGS=-fPIC" + ], + "sources": [ + { + "type": "archive", + "url": "https://fukuchi.org/works/qrencode/qrencode-4.1.1.tar.gz", + "sha512": "209bb656ae3f391b03c7b3ceb03e34f7320b0105babf48b619e7a299528b8828449e0e7696f0b5db0d99170a81709d0518e34835229a748701e7df784e58a9ce" + } + ] + }, + { + "name": "dino", + "buildsystem": "cmake-ninja", + "builddir": true, + "config-opts": [ + "-DSOUP_VERSION=3" + ], + "cleanup": [ + "/include", + "/share/vala" + ], + "sources": [ + { + "type": "dir", + "path": "." + } + ] + } + ] +} \ No newline at end of file From 9b83e5ccc933929b6f23ab0b0cb805f66ff4ab86 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Thu, 20 Apr 2023 18:36:48 +0200 Subject: [PATCH 27/27] Add Github CI job for Flatpak --- .github/workflows/build.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 974f31c9..a04b0f33 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,3 +25,15 @@ jobs: - run: sudo apt-get install -y build-essential gettext libadwaita-1-dev libgee-0.8-dev libgtk-4-dev libsqlite3-dev meson valac - run: meson setup build - run: meson compile -C build + build-flatpak: + runs-on: ubuntu-22.04 + container: + image: bilelmoussaoui/flatpak-github-actions:gnome-44 + options: --privileged + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: flatpak/flatpak-github-actions/flatpak-builder@v6.1 + with: + manifest-path: im.dino.Dino.json \ No newline at end of file