From cb3b19b01deb8460627578b885339e7528411f6f Mon Sep 17 00:00:00 2001 From: fiaxh Date: Fri, 6 Jan 2023 16:14:47 +0100 Subject: [PATCH 01/29] Support replies and reactions to files --- libdino/src/service/content_item_store.vala | 95 ++++++++++++++++++- libdino/src/service/message_processor.vala | 51 ++++++---- libdino/src/service/message_storage.vala | 4 +- libdino/src/service/reactions.vala | 78 ++++++--------- libdino/src/service/replies.vala | 17 +--- libdino/src/service/util.vala | 1 + .../file_widget.vala | 33 ++++++- .../message_widget.vala | 4 +- .../quote_widget.vala | 3 +- xmpp-vala/src/module/message/module.vala | 6 +- xmpp-vala/src/module/xep/0444_reactions.vala | 4 +- xmpp-vala/src/util.vala | 8 ++ 12 files changed, 202 insertions(+), 102 deletions(-) diff --git a/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala index 6a9e691f..b3e32cf4 100644 --- a/libdino/src/service/content_item_store.vala +++ b/libdino/src/service/content_item_store.vala @@ -44,11 +44,8 @@ public class ContentItemStore : StreamInteractionModule, Object { Gee.TreeSet items = new Gee.TreeSet(ContentItem.compare_func); foreach (var row in select) { - int id = row[db.content_item.id]; - int content_type = row[db.content_item.content_type]; - int foreign_id = row[db.content_item.foreign_id]; - DateTime time = new DateTime.from_unix_utc(row[db.content_item.time]); - items.add(get_item(conversation, id, content_type, foreign_id, time)); + ContentItem content_item = get_item_from_row(row, conversation); + items.add(content_item); } Gee.List ret = new ArrayList(); @@ -58,6 +55,14 @@ public class ContentItemStore : StreamInteractionModule, Object { return ret; } + private ContentItem get_item_from_row(Row row, Conversation conversation) throws Error { + int id = row[db.content_item.id]; + int content_type = row[db.content_item.content_type]; + int foreign_id = row[db.content_item.foreign_id]; + DateTime time = new DateTime.from_unix_utc(row[db.content_item.time]); + return get_item(conversation, id, content_type, foreign_id, time); + } + private ContentItem get_item(Conversation conversation, int id, int content_type, int foreign_id, DateTime time) throws Error { switch (content_type) { case 1: @@ -112,6 +117,86 @@ public class ContentItemStore : StreamInteractionModule, Object { return item.size > 0 ? item[0] : null; } + public string? get_message_id_for_content_item(Conversation conversation, ContentItem content_item) { + Message? message = get_message_for_content_item(conversation, content_item); + if (message == null) return null; + + if (conversation.type_ == Conversation.Type.CHAT) { + return message.stanza_id; + } else { + return message.server_id; + } + } + + public Jid? get_message_sender_for_content_item(Conversation conversation, ContentItem content_item) { + Message? message = get_message_for_content_item(conversation, content_item); + if (message == null) return null; + return message.from; + } + + private Message? get_message_for_content_item(Conversation conversation, ContentItem content_item) { + FileItem? file_item = content_item as FileItem; + if (file_item != null) { + if (file_item.file_transfer.provider != 0 || file_item.file_transfer.info == null) return null; + + int message_db_id = int.parse(file_item.file_transfer.info); + return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(message_db_id, conversation); + } + MessageItem? message_item = content_item as MessageItem; + if (message_item != null) { + return message_item.message; + } + return null; + } + + public ContentItem? get_content_item_for_message_id(Conversation conversation, string message_id) { + Row? row = get_content_item_row_for_message_id(conversation, message_id); + if (row != null) { + return get_item_from_row(row, conversation); + } + return null; + } + + public int get_content_item_id_for_message_id(Conversation conversation, string message_id) { + Row? row = get_content_item_row_for_message_id(conversation, message_id); + if (row != null) { + return row[db.content_item.id]; + } + return -1; + } + + private Row? get_content_item_row_for_message_id(Conversation conversation, string message_id) { + var content_item_row = db.content_item.select(); + + Message? message = null; + if (conversation.type_ == Conversation.Type.CHAT) { + message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(message_id, conversation); + } else { + message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(message_id, conversation); + } + if (message == null) return null; + + RowOption file_transfer_row = db.file_transfer.select() + .with(db.file_transfer.account_id, "=", conversation.account.id) + .with(db.file_transfer.counterpart_id, "=", db.get_jid_id(conversation.counterpart)) + .with(db.file_transfer.info, "=", message.id.to_string()) + .order_by(db.file_transfer.time, "DESC") + .single().row(); + + if (file_transfer_row.is_present()) { + content_item_row.with(db.content_item.foreign_id, "=", file_transfer_row[db.file_transfer.id]) + .with(db.content_item.content_type, "=", 2); + } else { + content_item_row.with(db.content_item.foreign_id, "=", message.id) + .with(db.content_item.content_type, "=", 1); + } + RowOption content_item_row_option = content_item_row.single().row(); + if (content_item_row_option.is_present()) { + return content_item_row_option.inner; + } + return null; + } + public ContentItem? get_latest(Conversation conversation) { Gee.List items = get_n_latest(conversation, 1); if (items.size > 0) { diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala index 8d544b45..770ae0a6 100644 --- a/libdino/src/service/message_processor.vala +++ b/libdino/src/service/message_processor.vala @@ -425,24 +425,8 @@ public class MessageProcessor : StreamInteractionModule, Object { new_message.type_ = MessageStanza.TYPE_CHAT; } - if (message.quoted_item_id > 0) { - ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, message.quoted_item_id); - if (content_item != null && content_item.type_ == MessageItem.TYPE) { - Message? quoted_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(((MessageItem) content_item).message.id, conversation); - if (quoted_message != null) { - Xep.Replies.set_reply_to(new_message, new Xep.Replies.ReplyTo(quoted_message.from, quoted_message.stanza_id)); - - string body_with_fallback = "> " + Dino.message_body_without_reply_fallback(quoted_message); - body_with_fallback = body_with_fallback.replace("\n", "\n> "); - body_with_fallback += "\n"; - long fallback_length = body_with_fallback.length; - body_with_fallback += message.body; - new_message.body = body_with_fallback; - var fallback_location = new Xep.FallbackIndication.FallbackLocation(0, (int)fallback_length); - Xep.FallbackIndication.set_fallback(new_message, new Xep.FallbackIndication.Fallback(Xep.Replies.NS_URI, new Xep.FallbackIndication.FallbackLocation[] { fallback_location })); - } - } - } + string? fallback = get_fallback_body_set_infos(message, new_message, conversation); + new_message.body = fallback == null ? message.body : fallback + message.body; build_message_stanza(message, new_message, conversation); pre_message_send(message, new_message, conversation); @@ -487,6 +471,37 @@ public class MessageProcessor : StreamInteractionModule, Object { } }); } + + public string? get_fallback_body_set_infos(Entities.Message message, MessageStanza new_stanza, Conversation conversation) { + if (message.quoted_item_id == 0) return null; + + ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, message.quoted_item_id); + if (content_item == null) return null; + + Jid? quoted_sender = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_sender_for_content_item(conversation, content_item); + string? quoted_stanza_id = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_id_for_content_item(conversation, content_item); + if (quoted_sender != null && quoted_stanza_id != null) { + Xep.Replies.set_reply_to(new_stanza, new Xep.Replies.ReplyTo(quoted_sender, quoted_stanza_id)); + } + + string fallback = "> "; + + if (content_item.type_ == MessageItem.TYPE) { + Message? quoted_message = ((MessageItem) content_item).message; + fallback += Dino.message_body_without_reply_fallback(quoted_message); + fallback = fallback.replace("\n", "\n> "); + } else if (content_item.type_ == FileItem.TYPE) { + FileTransfer? quoted_file = ((FileItem) content_item).file_transfer; + fallback += quoted_file.file_name; + } + fallback += "\n"; + + long fallback_length = fallback.length; + var fallback_location = new Xep.FallbackIndication.FallbackLocation(0, (int)fallback_length); + Xep.FallbackIndication.set_fallback(new_stanza, new Xep.FallbackIndication.Fallback(Xep.Replies.NS_URI, new Xep.FallbackIndication.FallbackLocation[] { fallback_location })); + + return fallback; + } } public abstract class MessageListener : Xmpp.OrderedListener { diff --git a/libdino/src/service/message_storage.vala b/libdino/src/service/message_storage.vala index fbdbcf8a..3dadab7b 100644 --- a/libdino/src/service/message_storage.vala +++ b/libdino/src/service/message_storage.vala @@ -116,9 +116,7 @@ public class MessageStorage : StreamInteractionModule, Object { .outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id) .outer_join_with(db.reply, db.reply.message_id, db.message.id); - if (conversation.counterpart.resourcepart == null) { - query.with_null(db.message.counterpart_resource); - } else { + if (conversation.counterpart.resourcepart != null) { query.with(db.message.counterpart_resource, "=", conversation.counterpart.resourcepart); } diff --git a/libdino/src/service/reactions.vala b/libdino/src/service/reactions.vala index fa273f39..f65394bb 100644 --- a/libdino/src/service/reactions.vala +++ b/libdino/src/service/reactions.vala @@ -10,7 +10,6 @@ public class Dino.Reactions : StreamInteractionModule, Object { public string id { get { return IDENTITY.id; } } public signal void reaction_added(Account account, int content_item_id, Jid jid, string reaction); -// [Signal(detailed=true)] public signal void reaction_removed(Account account, int content_item_id, Jid jid, string reaction); private StreamInteractor stream_interactor; @@ -35,15 +34,19 @@ public class Dino.Reactions : StreamInteractionModule, Object { if (!reactions.contains(reaction)) { reactions.add(reaction); } - send_reactions(conversation, content_item, reactions); - reaction_added(conversation.account, content_item.id, conversation.account.bare_jid, reaction); + try { + send_reactions(conversation, content_item, reactions); + reaction_added(conversation.account, content_item.id, conversation.account.bare_jid, reaction); + } catch (SendError e) {} } public void remove_reaction(Conversation conversation, ContentItem content_item, string reaction) { Gee.List reactions = get_own_reactions(conversation, content_item); reactions.remove(reaction); - send_reactions(conversation, content_item, reactions); - reaction_removed(conversation.account, content_item.id, conversation.account.bare_jid, reaction); + try { + send_reactions(conversation, content_item, reactions); + reaction_removed(conversation.account, content_item.id, conversation.account.bare_jid, reaction); + } catch (SendError e) {} } public Gee.List get_item_reactions(Conversation conversation, ContentItem content_item) { @@ -80,35 +83,28 @@ public class Dino.Reactions : StreamInteractionModule, Object { return false; } - private void send_reactions(Conversation conversation, ContentItem content_item, Gee.List reactions) { - Message? message = null; + private void send_reactions(Conversation conversation, ContentItem content_item, Gee.List reactions) throws SendError { + string? message_id = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_id_for_content_item(conversation, content_item); + if (message_id == null) throw new SendError.Misc("No message for content_item"); - FileItem? file_item = content_item as FileItem; - if (file_item != null) { - int message_id = int.parse(file_item.file_transfer.info); - message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(message_id, conversation); - } - MessageItem? message_item = content_item as MessageItem; - if (message_item != null) { - message = message_item.message; - } + XmppStream? stream = stream_interactor.get_stream(conversation.account); + if (stream == null) throw new SendError.NoStream(""); - if (message == null) { - return; - } + var reactions_module = stream.get_module(Xmpp.Xep.Reactions.Module.IDENTITY); - XmppStream stream = stream_interactor.get_stream(conversation.account); - if (conversation.type_ == Conversation.Type.GROUPCHAT || conversation.type_ == Conversation.Type.GROUPCHAT_PM) { - if (conversation.type_ == Conversation.Type.GROUPCHAT) { - stream.get_module(Xmpp.Xep.Reactions.Module.IDENTITY).send_reaction(stream, conversation.counterpart, "groupchat", message.server_id ?? message.stanza_id, reactions); - } else if (conversation.type_ == Conversation.Type.GROUPCHAT_PM) { - stream.get_module(Xmpp.Xep.Reactions.Module.IDENTITY).send_reaction(stream, conversation.counterpart, "chat", message.server_id ?? message.stanza_id, reactions); - } + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + reactions_module.send_reaction.begin(stream, conversation.counterpart, "groupchat", message_id, reactions); // We save the reaction when it gets reflected back to us + } else if (conversation.type_ == Conversation.Type.GROUPCHAT_PM) { + reactions_module.send_reaction(stream, conversation.counterpart, "chat", message_id, reactions); } else if (conversation.type_ == Conversation.Type.CHAT) { - stream.get_module(Xmpp.Xep.Reactions.Module.IDENTITY).send_reaction(stream, conversation.counterpart, "chat", message.stanza_id, reactions); int64 now_millis = GLib.get_real_time () / 1000; - save_chat_reactions(conversation.account, conversation.account.bare_jid, content_item.id, now_millis, reactions); + reactions_module.send_reaction.begin(stream, conversation.counterpart, "chat", message_id, reactions, (_, res) => { + try { + reactions_module.send_reaction.end(res); + save_chat_reactions(conversation.account, conversation.account.bare_jid, content_item.id, now_millis, reactions); + } catch (SendError e) {} + }); } } @@ -251,11 +247,11 @@ public class Dino.Reactions : StreamInteractionModule, Object { Message reaction_message = yield stream_interactor.get_module(MessageProcessor.IDENTITY).parse_message_stanza(account, stanza); Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_for_message(reaction_message); - Message? message = get_message_for_reaction(conversation, message_id); + int content_item_id = stream_interactor.get_module(ContentItemStore.IDENTITY).get_content_item_id_for_message_id(conversation, message_id); var reaction_info = new ReactionInfo() { account=account, from_jid=from_jid, reactions=reactions, stanza=stanza, received_time=new DateTime.now() }; - if (message != null) { - process_reaction_for_message(message.id, reaction_info); + if (content_item_id != -1) { + process_reaction_for_message(content_item_id, reaction_info); return; } @@ -317,30 +313,12 @@ public class Dino.Reactions : StreamInteractionModule, Object { } } - private void process_reaction_for_message(int message_db_id, ReactionInfo reaction_info) { + private void process_reaction_for_message(int content_item_id, ReactionInfo reaction_info) { Account account = reaction_info.account; MessageStanza stanza = reaction_info.stanza; Jid from_jid = reaction_info.from_jid; Gee.List reactions = reaction_info.reactions; - RowOption file_transfer_row = db.file_transfer.select() - .with(db.file_transfer.account_id, "=", account.id) - .with(db.file_transfer.info, "=", message_db_id.to_string()) - .single().row(); // TODO better - - var content_item_row = db.content_item.select(); - - if (file_transfer_row.is_present()) { - content_item_row.with(db.content_item.foreign_id, "=", file_transfer_row[db.file_transfer.id]) - .with(db.content_item.content_type, "=", 2); - } else { - content_item_row.with(db.content_item.foreign_id, "=", message_db_id) - .with(db.content_item.content_type, "=", 1); - } - var content_item_row_opt = content_item_row.single().row(); - if (!content_item_row_opt.is_present()) return; - int content_item_id = content_item_row_opt[db.content_item.id]; - // Get reaction time DateTime? reaction_time = null; DelayedDelivery.MessageFlag? delayed_message_flag = DelayedDelivery.MessageFlag.get_flag(stanza); diff --git a/libdino/src/service/replies.vala b/libdino/src/service/replies.vala index 6a9bced4..97db70ee 100644 --- a/libdino/src/service/replies.vala +++ b/libdino/src/service/replies.vala @@ -77,22 +77,7 @@ public class Dino.Replies : StreamInteractionModule, Object { Xep.Replies.ReplyTo? reply_to = Xep.Replies.get_reply_to(stanza); if (reply_to == null) return; - Message? quoted_message = null; - if (conversation.type_ == Conversation.Type.GROUPCHAT) { - quoted_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(reply_to.to_message_id, conversation); - } else { - quoted_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(reply_to.to_message_id, conversation); - } - if (quoted_message == null) { - db.reply.upsert() - .value(db.reply.message_id, message.id, true) - .value(db.reply.quoted_message_stanza_id, reply_to.to_message_id) - .value(db.reply.quoted_message_from, reply_to.to_jid.to_string()) - .perform(); - return; - } - - ContentItem? quoted_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_foreign(conversation, 1, quoted_message.id); + ContentItem? quoted_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_content_item_for_message_id(conversation, reply_to.to_message_id); if (quoted_content_item == null) return; set_message_is_reply_to(message, quoted_content_item); diff --git a/libdino/src/service/util.vala b/libdino/src/service/util.vala index 3cbb48d9..1d04ffcf 100644 --- a/libdino/src/service/util.vala +++ b/libdino/src/service/util.vala @@ -1,4 +1,5 @@ using Dino.Entities; +using Qlite; namespace Dino { diff --git a/main/src/ui/conversation_content_view/file_widget.vala b/main/src/ui/conversation_content_view/file_widget.vala index 8dbc3dc8..52a26f33 100644 --- a/main/src/ui/conversation_content_view/file_widget.vala +++ b/main/src/ui/conversation_content_view/file_widget.vala @@ -10,19 +10,44 @@ namespace Dino.Ui { public class FileMetaItem : ConversationSummary.ContentMetaItem { private StreamInteractor stream_interactor; + private FileItem file_item; + private FileTransfer file_transfer; public FileMetaItem(ContentItem content_item, StreamInteractor stream_interactor) { base(content_item); this.stream_interactor = stream_interactor; + this.file_item = content_item as FileItem; + this.file_transfer = file_item.file_transfer; } public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) { - FileItem file_item = content_item as FileItem; - FileTransfer transfer = file_item.file_transfer; - return new FileWidget(stream_interactor, transfer); + return new FileWidget(stream_interactor, file_transfer); } - public override Gee.List? get_item_actions(Plugins.WidgetType type) { return null; } + public override Gee.List? get_item_actions(Plugins.WidgetType type) { + if (file_transfer.provider != 0 || file_transfer.info == null) return null; + + Gee.List actions = new ArrayList(); + + if (stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_id_for_content_item(file_item.conversation, content_item) != null) { + Plugins.MessageAction reply_action = new Plugins.MessageAction(); + reply_action.icon_name = "mail-reply-sender-symbolic"; + reply_action.callback = (button, content_meta_item_activated, widget) => { + GLib.Application.get_default().activate_action("quote", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(file_item.conversation.id), new GLib.Variant.int32(content_item.id) })); + }; + actions.add(reply_action); + + Plugins.MessageAction action2 = new Plugins.MessageAction(); + action2.icon_name = "dino-emoticon-add-symbolic"; + EmojiChooser chooser = new EmojiChooser(); + chooser.emoji_picked.connect((emoji) => { + stream_interactor.get_module(Reactions.IDENTITY).add_reaction(file_item.conversation, content_item, emoji); + }); + action2.popover = chooser; + actions.add(action2); + } + return actions; + } } public class FileWidget : SizeRequestBox { diff --git a/main/src/ui/conversation_content_view/message_widget.vala b/main/src/ui/conversation_content_view/message_widget.vala index f4e1d22c..fb4ba162 100644 --- a/main/src/ui/conversation_content_view/message_widget.vala +++ b/main/src/ui/conversation_content_view/message_widget.vala @@ -198,7 +198,7 @@ public class MessageMetaItem : ContentMetaItem { if (quoted_content_item != null) { var quote_model = new Quote.Model.from_content_item(quoted_content_item, message_item.conversation, stream_interactor); quote_model.jump_to.connect(() => { - GLib.Application.get_default().activate_action("jump-to-conversation-message", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(message_item.conversation.id), new GLib.Variant.int32(message_item.id) })); + GLib.Application.get_default().activate_action("jump-to-conversation-message", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(message_item.conversation.id), new GLib.Variant.int32(quoted_content_item.id) })); }); var quote_widget = Quote.get_widget(quote_model); outer.set_widget(quote_widget, Plugins.WidgetType.GTK4, 1); @@ -226,7 +226,7 @@ public class MessageMetaItem : ContentMetaItem { Plugins.MessageAction reply_action = new Plugins.MessageAction(); reply_action.icon_name = "mail-reply-sender-symbolic"; reply_action.callback = (button, content_meta_item_activated, widget) => { - GLib.Application.get_default().activate_action("quote", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(message_item.conversation.id), new GLib.Variant.int32(message_item.id) })); + GLib.Application.get_default().activate_action("quote", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(message_item.conversation.id), new GLib.Variant.int32(content_item.id) })); }; actions.add(reply_action); diff --git a/main/src/ui/conversation_content_view/quote_widget.vala b/main/src/ui/conversation_content_view/quote_widget.vala index f627c852..cfe2f153 100644 --- a/main/src/ui/conversation_content_view/quote_widget.vala +++ b/main/src/ui/conversation_content_view/quote_widget.vala @@ -27,7 +27,8 @@ namespace Dino.Ui.Quote { var message = ((MessageItem) content_item).message; this.message = Dino.message_body_without_reply_fallback(message); } else if (content_item.type_ == FileItem.TYPE) { - this.message = "[File]"; + var file_transfer = ((FileItem) content_item).file_transfer; + this.message = _("File") + ": " + file_transfer.file_name; } this.message_time = content_item.time; diff --git a/xmpp-vala/src/module/message/module.vala b/xmpp-vala/src/module/message/module.vala index 2eced5c1..ef39a663 100644 --- a/xmpp-vala/src/module/message/module.vala +++ b/xmpp-vala/src/module/message/module.vala @@ -17,7 +17,11 @@ namespace Xmpp { public async void send_message(XmppStream stream, MessageStanza message) throws IOStreamError { yield send_pipeline.run(stream, message); - yield stream.write_async(message.stanza); + try { + yield stream.write_async(message.stanza); + } catch (IOStreamError e) { + throw new SendError.IO(e.message); + } } public async void received_message_stanza_async(XmppStream stream, StanzaNode node) { diff --git a/xmpp-vala/src/module/xep/0444_reactions.vala b/xmpp-vala/src/module/xep/0444_reactions.vala index 3501ca42..8e8a1706 100644 --- a/xmpp-vala/src/module/xep/0444_reactions.vala +++ b/xmpp-vala/src/module/xep/0444_reactions.vala @@ -11,7 +11,7 @@ public class Module : XmppStreamModule { private ReceivedPipelineListener received_pipeline_listener = new ReceivedPipelineListener(); - public void send_reaction(XmppStream stream, Jid jid, string stanza_type, string message_id, Gee.List reactions) { + public async void send_reaction(XmppStream stream, Jid jid, string stanza_type, string message_id, Gee.List reactions) throws SendError { StanzaNode reactions_node = new StanzaNode.build("reactions", NS_URI).add_self_xmlns(); reactions_node.put_attribute("id", message_id); foreach (string reaction in reactions) { @@ -25,7 +25,7 @@ public class Module : XmppStreamModule { MessageProcessingHints.set_message_hint(message, MessageProcessingHints.HINT_STORE); - stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, message); + yield stream.get_module(MessageModule.IDENTITY).send_message(stream, message); } public override void attach(XmppStream stream) { diff --git a/xmpp-vala/src/util.vala b/xmpp-vala/src/util.vala index 6c0d0c9b..fda21f88 100644 --- a/xmpp-vala/src/util.vala +++ b/xmpp-vala/src/util.vala @@ -38,3 +38,11 @@ public long from_hex(string numeral) { } } + +namespace Xmpp { + public errordomain SendError { + IO, + NoStream, + Misc + } +} From 75500dc767f2cf657c0fbb5d2a4d4557183ed2e9 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Wed, 11 Jan 2023 19:54:02 +0100 Subject: [PATCH 02/29] Support pinning of conversations (locally) fixes #290 fixes #1330 --- libdino/src/entity/conversation.vala | 9 +++-- libdino/src/service/database.vala | 5 +-- main/data/conversation_row.ui | 33 +++++++++++++------ .../ui/contact_details/settings_provider.vala | 6 ++++ .../conversation_selector.vala | 8 +++++ .../conversation_selector_row.vala | 13 ++++++-- 6 files changed, 57 insertions(+), 17 deletions(-) diff --git a/libdino/src/entity/conversation.vala b/libdino/src/entity/conversation.vala index 9376dca9..353daeae 100644 --- a/libdino/src/entity/conversation.vala +++ b/libdino/src/entity/conversation.vala @@ -42,9 +42,10 @@ public class Conversation : Object { public enum Setting { DEFAULT, ON, OFF } public Setting send_typing { get; set; default = Setting.DEFAULT; } - public Setting send_marker { get; set; default = Setting.DEFAULT; } + public int pinned { get; set; default = 0; } + private Database? db; public Conversation(Jid jid, Account account, Type type) { @@ -74,6 +75,7 @@ public class Conversation : Object { notify_setting = (NotifySetting) row[db.conversation.notification]; send_typing = (Setting) row[db.conversation.send_typing]; send_marker = (Setting) row[db.conversation.send_marker]; + pinned = row[db.conversation.pinned]; notify.connect(on_update); } @@ -91,7 +93,8 @@ public class Conversation : Object { .value(db.conversation.active_last_changed, (long) active_last_changed.to_unix()) .value(db.conversation.notification, notify_setting) .value(db.conversation.send_typing, send_typing) - .value(db.conversation.send_marker, send_marker); + .value(db.conversation.send_marker, send_marker) + .value(db.conversation.pinned, pinned); if (read_up_to != null) { insert.value(db.conversation.read_up_to, read_up_to.id); } @@ -197,6 +200,8 @@ public class Conversation : Object { update.set(db.conversation.send_typing, send_typing); break; case "send-marker": update.set(db.conversation.send_marker, send_marker); break; + case "pinned": + update.set(db.conversation.pinned, pinned); break; } update.perform(); } diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index bfd85f06..96b3b82d 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 = 24; + private const int VERSION = 25; public class AccountTable : Table { public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; @@ -244,10 +244,11 @@ public class Database : Qlite.Database { public Column notification = new Column.Integer("notification") { min_version=3 }; public Column send_typing = new Column.Integer("send_typing") { min_version=3 }; public Column send_marker = new Column.Integer("send_marker") { min_version=3 }; + public Column pinned = new Column.Integer("pinned") { default="0", min_version=25 }; internal ConversationTable(Database db) { base(db, "conversation"); - init({id, account_id, jid_id, resource, active, active_last_changed, last_active, type_, encryption, read_up_to, read_up_to_item, notification, send_typing, send_marker}); + init({id, account_id, jid_id, resource, active, active_last_changed, last_active, type_, encryption, read_up_to, read_up_to_item, notification, send_typing, send_marker, pinned}); } } diff --git a/main/data/conversation_row.ui b/main/data/conversation_row.ui index fcfd22f0..7be699ba 100644 --- a/main/data/conversation_row.ui +++ b/main/data/conversation_row.ui @@ -88,20 +88,33 @@ - + slide-right 50 True + 15 - - False - False - 15 - 0.5 - - - - + + horizontal + 6 + + + False + False + 0.5 + + + + + + + + + view-pin-symbolic + 12 + False + + diff --git a/main/src/ui/contact_details/settings_provider.vala b/main/src/ui/contact_details/settings_provider.vala index 140ebcab..6c43dbfd 100644 --- a/main/src/ui/contact_details/settings_provider.vala +++ b/main/src/ui/contact_details/settings_provider.vala @@ -49,6 +49,12 @@ public class SettingsProvider : Plugins.ContactDetailsProvider, Object { combobox.active_id = get_notify_setting_id(conversation.notify_setting); combobox.changed.connect(() => { conversation.notify_setting = get_notify_setting(combobox.active_id); } ); } + + Switch pinned_switch = new Switch(); + string category = conversation.type_ == Conversation.Type.GROUPCHAT ? DETAILS_HEADLINE_ROOM : DETAILS_HEADLINE_CHAT; + contact_details.add(category, _("Pin conversation"), _("Pins the conversation to the top of the conversation list"), pinned_switch); + pinned_switch.state = conversation.pinned != 0; + pinned_switch.state_set.connect((state) => { conversation.pinned = state ? 1 : 0; return false; }); } private Conversation.Setting get_setting(string id) { diff --git a/main/src/ui/conversation_selector/conversation_selector.vala b/main/src/ui/conversation_selector/conversation_selector.vala index 609e1be1..8a4506f3 100644 --- a/main/src/ui/conversation_selector/conversation_selector.vala +++ b/main/src/ui/conversation_selector/conversation_selector.vala @@ -75,6 +75,8 @@ public class ConversationSelector : Widget { private void add_conversation(Conversation conversation) { ConversationSelectorRow row; if (!rows.has_key(conversation)) { + conversation.notify["pinned"].connect(list_box.invalidate_sort); + row = new ConversationSelectorRow(stream_interactor, conversation); rows[conversation] = row; list_box.append(row); @@ -119,6 +121,8 @@ public class ConversationSelector : Widget { private async void remove_conversation(Conversation conversation) { select_fallback_conversation(conversation); if (rows.has_key(conversation)) { + conversation.notify["pinned"].disconnect(list_box.invalidate_sort); + yield rows[conversation].colapse(); list_box.remove(rows[conversation]); rows.unset(conversation); @@ -149,6 +153,10 @@ public class ConversationSelector : Widget { if (cr1 != null && cr2 != null) { Conversation c1 = cr1.conversation; Conversation c2 = cr2.conversation; + + int pin_comp = c2.pinned - c1.pinned; + if (pin_comp != 0) return pin_comp; + if (c1.last_active == null) return -1; if (c2.last_active == null) return 1; int comp = c2.last_active.compare(c1.last_active); diff --git a/main/src/ui/conversation_selector/conversation_selector_row.vala b/main/src/ui/conversation_selector/conversation_selector_row.vala index bd2b0747..8355b104 100644 --- a/main/src/ui/conversation_selector/conversation_selector_row.vala +++ b/main/src/ui/conversation_selector/conversation_selector_row.vala @@ -21,7 +21,8 @@ public class ConversationSelectorRow : ListBoxRow { [GtkChild] protected unowned Button x_button; [GtkChild] protected unowned Revealer time_revealer; [GtkChild] protected unowned Revealer xbutton_revealer; - [GtkChild] protected unowned Revealer unread_count_revealer; + [GtkChild] protected unowned Revealer top_row_revealer; + [GtkChild] protected unowned Image pinned_image; [GtkChild] public unowned Revealer main_revealer; public Conversation conversation { get; private set; } @@ -102,8 +103,10 @@ public class ConversationSelectorRow : ListBoxRow { }); image.set_conversation(stream_interactor, conversation); conversation.notify["read-up-to-item"].connect(() => update_read()); + conversation.notify["pinned"].connect(() => { update_pinned_icon(); }); update_name_label(); + update_pinned_icon(); content_item_received(); } @@ -135,6 +138,10 @@ public class ConversationSelectorRow : ListBoxRow { name_label.label = Util.get_conversation_display_name(stream_interactor, conversation); } + private void update_pinned_icon() { + pinned_image.visible = conversation.pinned != 0; + } + protected void update_time_label(DateTime? new_time = null) { if (last_content_item != null) { time_label.visible = true; @@ -252,11 +259,11 @@ public class ConversationSelectorRow : ListBoxRow { StateFlags curr_flags = get_state_flags(); if ((curr_flags & StateFlags.PRELIGHT) != 0) { time_revealer.set_reveal_child(false); - unread_count_revealer.set_reveal_child(false); + top_row_revealer.set_reveal_child(false); xbutton_revealer.set_reveal_child(true); } else { time_revealer.set_reveal_child(true); - unread_count_revealer.set_reveal_child(true); + top_row_revealer.set_reveal_child(true); xbutton_revealer.set_reveal_child(false); } } From 860c72bfc93d252d45eb97e71cf9ff22985c7ef9 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Wed, 11 Jan 2023 19:49:27 +0100 Subject: [PATCH 03/29] Fix crash when removing jid from roster fixes #1332 --- main/src/ui/add_conversation/roster_list.vala | 9 +++++---- main/src/ui/add_conversation/select_contact_dialog.vala | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/main/src/ui/add_conversation/roster_list.vala b/main/src/ui/add_conversation/roster_list.vala index 25827d67..bb338ce5 100644 --- a/main/src/ui/add_conversation/roster_list.vala +++ b/main/src/ui/add_conversation/roster_list.vala @@ -14,7 +14,7 @@ protected class RosterList { private ulong[] handler_ids = new ulong[0]; private ListBox list_box = new ListBox(); - private HashMap> rows = new HashMap>(Account.hash_func, Account.equals_func); + private HashMap> rows = new HashMap>(Account.hash_func, Account.equals_func); public RosterList(StreamInteractor stream_interactor, Gee.List accounts) { this.stream_interactor = stream_interactor; @@ -47,14 +47,15 @@ protected class RosterList { private void on_updated_roster_item(Account account, Jid jid, Roster.Item roster_item) { on_removed_roster_item(account, jid, roster_item); ListRow row = new ListRow.from_jid(stream_interactor, roster_item.jid, account, accounts.size > 1); - rows[account][jid] = row; - list_box.append(row); + ListBoxRow list_box_row = new ListBoxRow() { child=row }; + rows[account][jid] = list_box_row; + list_box.append(list_box_row); list_box.invalidate_sort(); list_box.invalidate_filter(); } private void fetch_roster_items(Account account) { - rows[account] = new HashMap(Jid.hash_func, Jid.equals_func); + rows[account] = new HashMap(Jid.hash_func, Jid.equals_func); foreach (Roster.Item roster_item in stream_interactor.get_module(RosterManager.IDENTITY).get_roster(account)) { on_updated_roster_item(account, roster_item.jid, roster_item); } diff --git a/main/src/ui/add_conversation/select_contact_dialog.vala b/main/src/ui/add_conversation/select_contact_dialog.vala index 09ac1636..b18f4c10 100644 --- a/main/src/ui/add_conversation/select_contact_dialog.vala +++ b/main/src/ui/add_conversation/select_contact_dialog.vala @@ -80,7 +80,7 @@ public class SelectContactDialog : Gtk.Dialog { add_contact_dialog.present(); }); select_jid_fragment.remove_jid.connect((row) => { - ListRow list_row = roster_list_box.get_selected_row() as ListRow; + ListRow list_row = roster_list_box.get_selected_row().child as ListRow; stream_interactor.get_module(RosterManager.IDENTITY).remove_jid(list_row.account, list_row.jid); }); select_jid_fragment.notify["done"].connect(() => { From 73c0263f35a73b68d20d299ee7fe8c37b9a6ffeb Mon Sep 17 00:00:00 2001 From: fiaxh Date: Fri, 13 Jan 2023 11:23:09 +0100 Subject: [PATCH 04/29] Add debug outputs to summarize_whitespaces_to_space and don't assert_not_reached related #1335 --- libdino/src/service/content_item_store.vala | 5 +++++ main/src/ui/util/helper.vala | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala index b3e32cf4..c3d9d006 100644 --- a/libdino/src/service/content_item_store.vala +++ b/libdino/src/service/content_item_store.vala @@ -121,6 +121,8 @@ public class ContentItemStore : StreamInteractionModule, Object { Message? message = get_message_for_content_item(conversation, content_item); if (message == null) return null; + if (message.edit_to != null) return message.edit_to; + if (conversation.type_ == Conversation.Type.CHAT) { return message.stanza_id; } else { @@ -131,6 +133,9 @@ public class ContentItemStore : StreamInteractionModule, Object { public Jid? get_message_sender_for_content_item(Conversation conversation, ContentItem content_item) { Message? message = get_message_for_content_item(conversation, content_item); if (message == null) return null; + + // No need to look at edit_to, because it's the same sender JID. + return message.from; } diff --git a/main/src/ui/util/helper.vala b/main/src/ui/util/helper.vala index 58614bb8..ecf0ab25 100644 --- a/main/src/ui/util/helper.vala +++ b/main/src/ui/util/helper.vala @@ -419,7 +419,8 @@ public string summarize_whitespaces_to_space(string s) { try { return (/\s+/).replace_literal(s, -1, 0, " "); } catch (RegexError e) { - assert_not_reached(); + critical("RegexError when summarizing whitespaces in '%s': %s", s, e.message); + return s; } } From 05289e0b4dc9bc076955e27b30b386cb7f0604c7 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Mon, 16 Jan 2023 17:27:38 +0100 Subject: [PATCH 05/29] Fix reply cancelling fixes #1340 --- main/src/ui/chat_input/chat_input_controller.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/src/ui/chat_input/chat_input_controller.vala b/main/src/ui/chat_input/chat_input_controller.vala index c5693300..92a12bc9 100644 --- a/main/src/ui/chat_input/chat_input_controller.vala +++ b/main/src/ui/chat_input/chat_input_controller.vala @@ -74,7 +74,7 @@ public class ChatInputController : Object { quoted_content_item = content_item; var quote_model = new Quote.Model.from_content_item(content_item, conversation, stream_interactor) { can_abort = true }; quote_model.aborted.connect(() => { - content_item = null; + quoted_content_item = null; chat_input.unset_quoted_message(); }); chat_input.set_quoted_message(Quote.get_widget(quote_model)); @@ -83,8 +83,8 @@ public class ChatInputController : Object { } public void set_conversation(Conversation conversation) { - this.quoted_content_item = null; reset_input_field_status(); + this.quoted_content_item = null; chat_input.unset_quoted_message(); this.conversation = conversation; From 7da79864b384c9370a5937d480230e771834d91a Mon Sep 17 00:00:00 2001 From: fiaxh Date: Mon, 16 Jan 2023 17:37:53 +0100 Subject: [PATCH 06/29] Fix pin setting switch displaying --- main/src/ui/contact_details/settings_provider.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/ui/contact_details/settings_provider.vala b/main/src/ui/contact_details/settings_provider.vala index 6c43dbfd..8121e5b1 100644 --- a/main/src/ui/contact_details/settings_provider.vala +++ b/main/src/ui/contact_details/settings_provider.vala @@ -50,7 +50,7 @@ public class SettingsProvider : Plugins.ContactDetailsProvider, Object { combobox.changed.connect(() => { conversation.notify_setting = get_notify_setting(combobox.active_id); } ); } - Switch pinned_switch = new Switch(); + Switch pinned_switch = new Switch() { valign=Align.CENTER }; string category = conversation.type_ == Conversation.Type.GROUPCHAT ? DETAILS_HEADLINE_ROOM : DETAILS_HEADLINE_CHAT; contact_details.add(category, _("Pin conversation"), _("Pins the conversation to the top of the conversation list"), pinned_switch); pinned_switch.state = conversation.pinned != 0; From 7e0d1db1965555720db2bef7380e61c23ef6dbcd Mon Sep 17 00:00:00 2001 From: fiaxh Date: Mon, 16 Jan 2023 17:24:58 +0100 Subject: [PATCH 07/29] MAM: Fix latest range not being stored in db if it contained a duplicate --- libdino/src/service/history_sync.vala | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/libdino/src/service/history_sync.vala b/libdino/src/service/history_sync.vala index ed5a04af..c7bfee88 100644 --- a/libdino/src/service/history_sync.vala +++ b/libdino/src/service/history_sync.vala @@ -204,19 +204,18 @@ public class Dino.HistorySync { var query_params = new Xmpp.MessageArchiveManagement.V2.MamQueryParams.query_latest(mam_server, latest_message_time, latest_message_id); PageRequestResult page_result = yield get_mam_page(account, query_params, null); - - if (page_result.page_result == PageResult.Duplicate) { - // No new messages - return null; - } + debug("[%s | %s] Latest page result: %s", account.bare_jid.to_string(), mam_server.to_string(), page_result.page_result.to_string()); if (page_result.page_result == PageResult.Error) { - debug("[%s | %s] Failed fetching latest page %s", mam_server.to_string(), mam_server.to_string(), page_result.page_result.to_string()); 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 (page_result.page_result in new PageResult[] { PageResult.TargetReached, PageResult.NoMoreMessages } && latest_row_id != -1) { + if (latest_row_id != -1 && + page_result.page_result in new PageResult[] { PageResult.TargetReached, PageResult.NoMoreMessages, PageResult.Duplicate }) { + if (page_result.stanzas == null || page_result.stanzas.is_empty) return null; string latest_mam_id = page_result.query_result.last; @@ -258,7 +257,7 @@ public class Dino.HistorySync { .value(db.mam_catchup.server_jid, mam_server.to_string()) .value(db.mam_catchup.from_id, from_id) .value(db.mam_catchup.from_time, from_time) - .value(db.mam_catchup.from_end, false) + .value(db.mam_catchup.from_end, page_result.page_result == PageResult.NoMoreMessages) .value(db.mam_catchup.to_id, to_id) .value(db.mam_catchup.to_time, to_time) .perform(); From f6e73d85c00a60a719da95a048ba2c15712325c3 Mon Sep 17 00:00:00 2001 From: Teemu Ikonen Date: Thu, 8 Sep 2022 11:31:37 +0300 Subject: [PATCH 08/29] Add libadwaita to build system --- cmake/FindAdwaita.cmake | 11 +++++++++++ main/CMakeLists.txt | 1 + 2 files changed, 12 insertions(+) create mode 100644 cmake/FindAdwaita.cmake diff --git a/cmake/FindAdwaita.cmake b/cmake/FindAdwaita.cmake new file mode 100644 index 00000000..8202eea1 --- /dev/null +++ b/cmake/FindAdwaita.cmake @@ -0,0 +1,11 @@ +include(PkgConfigWithFallback) +find_pkg_config_with_fallback(Adwaita + PKG_CONFIG_NAME libadwaita-1 + LIB_NAMES libadwaita-1 + INCLUDE_NAMES adwaita.h + ) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Adwaita + REQUIRED_VARS Adwaita_LIBRARY + VERSION_VAR Adwaita_VERSION) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 88b52c63..c133a399 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -11,6 +11,7 @@ find_packages(MAIN_PACKAGES REQUIRED GObject GTK4 ICU + Adwaita ) set(RESOURCE_LIST From 1ef42b47d22d21600ccf1e2d8b4d80605448660d Mon Sep 17 00:00:00 2001 From: Teemu Ikonen Date: Thu, 8 Sep 2022 12:21:22 +0300 Subject: [PATCH 09/29] Use Adw.Application, make about dialog an Adw.AboutWindow --- main/src/ui/application.vala | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/main/src/ui/application.vala b/main/src/ui/application.vala index 2167145b..b810852c 100644 --- a/main/src/ui/application.vala +++ b/main/src/ui/application.vala @@ -4,7 +4,7 @@ using Dino.Entities; using Dino.Ui; using Xmpp; -public class Dino.Ui.Application : Gtk.Application, Dino.Application { +public class Dino.Ui.Application : Adw.Application, Dino.Application { private const string[] KEY_COMBINATION_QUIT = {"Q", null}; private const string[] KEY_COMBINATION_ADD_CHAT = {"T", null}; private const string[] KEY_COMBINATION_ADD_CONFERENCE = {"G", null}; @@ -272,25 +272,24 @@ public class Dino.Ui.Application : Gtk.Application, Dino.Application { case "0.3": version = @"$version - Theikenmeer"; break; } } - Gtk.AboutDialog dialog = new Gtk.AboutDialog(); - dialog.destroy_with_parent = true; - dialog.transient_for = window; - dialog.modal = true; - dialog.title = _("About Dino"); + Adw.AboutWindow about = new Adw.AboutWindow(); + about.destroy_with_parent = true; + about.transient_for = window; + about.modal = true; + about.title = _("About Dino"); - dialog.logo_icon_name = "im.dino.Dino"; - dialog.program_name = "Dino"; - dialog.version = version; - dialog.comments = "Dino. Communicating happiness."; - dialog.website = "https://dino.im/"; - dialog.website_label = "dino.im"; - dialog.copyright = "Copyright © 2016-2022 - Dino Team"; - dialog.license_type = License.GPL_3_0; + about.application_icon = "im.dino.Dino"; + about.application_name = "Dino"; + about.version = version; + about.comments = "Dino. Communicating happiness."; + about.website = "https://dino.im/"; + about.copyright = "Copyright © 2016-2022 - Dino Team"; + about.license_type = License.GPL_3_0; if (!use_csd()) { - dialog.set_titlebar(null); + about.set_titlebar(null); } - dialog.present(); + about.present(); } private void show_join_muc_dialog(Account? account, string jid) { From 2741bf21ae6d53324a512dacef65d540be840fe4 Mon Sep 17 00:00:00 2001 From: Teemu Ikonen Date: Mon, 12 Sep 2022 17:07:14 +0300 Subject: [PATCH 10/29] Convert main window layout to 2 vertical boxes Use Adw.Window as main window widget, add the now missing HeaderBars to MainWindowPlaceholder and MainWindow in the NoCSD case. --- main/data/unified_main_content.ui | 177 +++++++++++++----------- main/data/unified_window_placeholder.ui | 12 +- main/src/ui/main_window.vala | 32 ++--- 3 files changed, 118 insertions(+), 103 deletions(-) diff --git a/main/data/unified_main_content.ui b/main/data/unified_main_content.ui index 3fb7b6e5..f1294ab9 100644 --- a/main/data/unified_main_content.ui +++ b/main/data/unified_main_content.ui @@ -7,68 +7,22 @@ False 300 - + + vertical - - content - - - never - - - - - - - - - - - placeholder - - - 20 - 20 - 20 - 20 - 10 - start - start - - - start - - - - - - 1 - 70 - 50 - end - Click here to start a conversation or join a channel. - - - - - - - - - - - - - + + False content - + + never + 1 + + + + @@ -78,16 +32,17 @@ placeholder - vertical - 1 - 1 - center - center + 20 + 20 + 20 + 20 + 10 + start + start + 260 - - im.dino.Dino-symbolic - 144 - 30 + + start @@ -95,13 +50,16 @@ - You have no open chats + 1 + 70 + 50 + 0 + end + 0 + Click here to start a conversation or join a channel. - - - @@ -109,22 +67,79 @@ - - - - end - slide-left - + + + + + + vertical + + - - 400 + + + + content + + + + + + + + + placeholder + + + vertical + 1 + 1 + center + center + + + im.dino.Dino-symbolic + 144 + 30 + + + + + + You have no open chats + + + + + + + + + + + + + end + slide-left + + + + 400 + + + + - \ No newline at end of file + diff --git a/main/data/unified_window_placeholder.ui b/main/data/unified_window_placeholder.ui index 997d7220..91958077 100644 --- a/main/data/unified_window_placeholder.ui +++ b/main/data/unified_window_placeholder.ui @@ -2,13 +2,19 @@