From 4d7809bb12199a598b531ca3ca019a4bb5a867f7 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Sat, 26 Nov 2022 22:26:25 +0100 Subject: [PATCH 1/5] Fix compiler warnings --- libdino/src/service/calls.vala | 1 - libdino/src/service/history_sync.vala | 2 +- libdino/src/service/jingle_file_transfers.vala | 2 -- libdino/src/service/message_processor.vala | 2 +- libdino/src/service/muc_manager.vala | 4 ++-- main/src/ui/chat_input/encryption_button.vala | 1 - .../conversation_item_skeleton.vala | 3 +++ .../conversation_view.vala | 2 -- .../file_image_widget.vala | 1 - main/src/ui/conversation_view.vala | 18 ------------------ main/src/ui/conversation_view_controller.vala | 7 ++++--- plugins/omemo/src/ui/manage_key_dialog.vala | 1 - 12 files changed, 11 insertions(+), 33 deletions(-) diff --git a/libdino/src/service/calls.vala b/libdino/src/service/calls.vala index 35b88866..ebaf8d03 100644 --- a/libdino/src/service/calls.vala +++ b/libdino/src/service/calls.vala @@ -82,7 +82,6 @@ namespace Dino { return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart); } else { bool is_private = stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart); - EntityInfo entity_info = stream_interactor.get_module(EntityInfo.IDENTITY); return is_private && can_initiate_groupcall(conversation.account); } } diff --git a/libdino/src/service/history_sync.vala b/libdino/src/service/history_sync.vala index 08381e44..ed5a04af 100644 --- a/libdino/src/service/history_sync.vala +++ b/libdino/src/service/history_sync.vala @@ -315,7 +315,7 @@ public class Dino.HistorySync { latest_time, latest_id ); } - PageRequestResult page_result = yield fetch_query(account, query_params, range[db.mam_catchup.id]); + yield fetch_query(account, query_params, range[db.mam_catchup.id]); } /** diff --git a/libdino/src/service/jingle_file_transfers.vala b/libdino/src/service/jingle_file_transfers.vala index adf10034..624be607 100644 --- a/libdino/src/service/jingle_file_transfers.vala +++ b/libdino/src/service/jingle_file_transfers.vala @@ -115,8 +115,6 @@ public class JingleFileProvider : FileProvider, Object { } private void on_account_added(Account account) { - XmppStream stream = stream_interactor.get_stream(account); - stream_interactor.module_manager.get_module(account, Xmpp.Xep.JingleFileTransfer.Module.IDENTITY).file_incoming.connect((stream, jingle_file_transfer) => { Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(jingle_file_transfer.peer.bare_jid, account); if (conversation == null) return; diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala index bfecf340..a290132f 100644 --- a/libdino/src/service/message_processor.vala +++ b/libdino/src/service/message_processor.vala @@ -128,7 +128,7 @@ public class MessageProcessor : StreamInteractionModule, Object { // If it's a message from MAM, it's going to be processed by HistorySync which calls run_pipeline_announce later. if (history_sync.process(account, message_stanza)) return; - run_pipeline_announce(account, message_stanza); + run_pipeline_announce.begin(account, message_stanza); } public async void run_pipeline_announce(Account account, Xmpp.MessageStanza message_stanza) { diff --git a/libdino/src/service/muc_manager.vala b/libdino/src/service/muc_manager.vala index 28520ab9..36a5599f 100644 --- a/libdino/src/service/muc_manager.vala +++ b/libdino/src/service/muc_manager.vala @@ -387,7 +387,7 @@ public class MucManager : StreamInteractionModule, Object { } public string? get_own_occupant_id(Account account, Jid muc_jid) { - if (account in own_occupant_ids && muc_jid in own_occupant_ids[account]) { + if (own_occupant_ids.has_key(account) && own_occupant_ids[account].has_key(muc_jid)) { return own_occupant_ids[account][muc_jid]; } return null; @@ -421,7 +421,7 @@ public class MucManager : StreamInteractionModule, Object { } }); stream_interactor.module_manager.get_module(account, Xep.OccupantIds.Module.IDENTITY).received_own_occupant_id.connect( (stream, jid, occupant_id) => { - if (!(account in own_occupant_ids)) { + if (!own_occupant_ids.has_key(account)) { own_occupant_ids[account] = new HashMap(Jid.hash_bare_func, Jid.equals_bare_func); } own_occupant_ids[account][jid] = occupant_id; diff --git a/main/src/ui/chat_input/encryption_button.vala b/main/src/ui/chat_input/encryption_button.vala index 50497ee3..1f991338 100644 --- a/main/src/ui/chat_input/encryption_button.vala +++ b/main/src/ui/chat_input/encryption_button.vala @@ -11,7 +11,6 @@ public class EncryptionButton { private MenuButton menu_button; private Conversation? conversation; - private CheckButton? button_unencrypted; private string? current_icon; private StreamInteractor stream_interactor; private SimpleAction action; 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 21aca876..7113b3b7 100644 --- a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala +++ b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala @@ -43,6 +43,9 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, this.item = item; this.content_meta_item = item as ContentMetaItem; + item.bind_property("in-edit-mode", this, "item-in-edit-mode"); + this.notify["item-in-edit-mode"].connect(update_edit_mode); + Builder builder = new Builder.from_resource("/im/dino/Dino/conversation_item_widget.ui"); main_grid = (Grid) builder.get_object("main_grid"); main_grid.add_css_class("message-box"); diff --git a/main/src/ui/conversation_content_view/conversation_view.vala b/main/src/ui/conversation_content_view/conversation_view.vala index caeee09a..f0f6e118 100644 --- a/main/src/ui/conversation_content_view/conversation_view.vala +++ b/main/src/ui/conversation_content_view/conversation_view.vala @@ -136,7 +136,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug // Get widget under pointer int h = 0; Widget? w = null; - Plugins.MetaConversationItem? meta_item = null; foreach (Plugins.MetaConversationItem item in meta_items) { Widget widget = widgets[item]; h += widget.get_allocated_height() + widget.margin_top + widget.margin_bottom; @@ -404,7 +403,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug if (lower_item != null) { if (can_merge(item, lower_item)) { - ConversationItemSkeleton lower_skeleton = item_item_skeletons[lower_item]; item_skeleton.show_skeleton = false; } else { item_skeleton.show_skeleton = true; diff --git a/main/src/ui/conversation_content_view/file_image_widget.vala b/main/src/ui/conversation_content_view/file_image_widget.vala index 285e397e..097ac695 100644 --- a/main/src/ui/conversation_content_view/file_image_widget.vala +++ b/main/src/ui/conversation_content_view/file_image_widget.vala @@ -35,7 +35,6 @@ public class FileImageWidget : Box { pixbuf = pixbuf.apply_embedded_orientation(); image.load(pixbuf); - Picture picture = new Picture.for_pixbuf(pixbuf) { can_shrink=true, keep_aspect_ratio=true, halign=Align.START }; Idle.add(load_from_file.callback); return image; diff --git a/main/src/ui/conversation_view.vala b/main/src/ui/conversation_view.vala index 7c93c4ff..128b3bd8 100644 --- a/main/src/ui/conversation_view.vala +++ b/main/src/ui/conversation_view.vala @@ -28,9 +28,6 @@ public class ConversationView : Widget { // conversation_scrolled.set_child(list_view); // list_view.set_factory(get_item_factory()); -// conversation_scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify); -// conversation_scrolled.vadjustment.notify["value"].connect(on_value_notify); - } public void add_overlay_dialog(Widget widget) { @@ -54,21 +51,6 @@ public class ConversationView : Widget { white_revealer.visible = false; } } - - private void on_upper_notify() { - print("on_upper_notify\n"); - if (at_current_content) { - print("on_upper_notify2\n"); - // scroll down -// conversation_scrolled.vadjustment.value = conversation_scrolled.vadjustment.upper - conversation_scrolled.vadjustment.page_size; -// conversation_scrolled.scroll_child(ScrollType.END, false); - } - } - - private void on_value_notify() { - print("on_value_notify\n"); -// at_current_content = false; - } } } diff --git a/main/src/ui/conversation_view_controller.vala b/main/src/ui/conversation_view_controller.vala index 5844ef0a..da94b740 100644 --- a/main/src/ui/conversation_view_controller.vala +++ b/main/src/ui/conversation_view_controller.vala @@ -146,7 +146,7 @@ public class ConversationViewController : Object { view.conversation_frame.initialize_for_conversation(conversation); } - update_file_upload_status(); + update_file_upload_status.begin(); } public void unset_conversation() { @@ -159,6 +159,7 @@ public class ConversationViewController : Object { bool upload_available = yield stream_interactor.get_module(FileManager.IDENTITY).is_upload_available(conversation); chat_input_controller.set_file_upload_active(upload_available); + if (upload_available && overlay_dialog == null) { if (drop_event_controller.widget == null) { view.add_controller(drop_event_controller); @@ -246,13 +247,13 @@ public class ConversationViewController : Object { overlay.close.connect(() => { // We don't want drag'n'drop to be active while the overlay is active overlay_dialog = null; - update_file_upload_status(); + update_file_upload_status.begin(); }); view.add_overlay_dialog(overlay.get_widget()); overlay_dialog = overlay.get_widget(); - update_file_upload_status(); + update_file_upload_status.begin(); } private void send_file(File file) { diff --git a/plugins/omemo/src/ui/manage_key_dialog.vala b/plugins/omemo/src/ui/manage_key_dialog.vala index 19f16a59..b47b2008 100644 --- a/plugins/omemo/src/ui/manage_key_dialog.vala +++ b/plugins/omemo/src/ui/manage_key_dialog.vala @@ -6,7 +6,6 @@ namespace Dino.Plugins.Omemo { [GtkTemplate (ui = "/im/dino/Dino/omemo/manage_key_dialog.ui")] public class ManageKeyDialog : Gtk.Dialog { - [GtkChild] private unowned HeaderBar headerbar; [GtkChild] private unowned Stack manage_stack; [GtkChild] private unowned Button cancel_button; From dc52e7595cca06d0a2da7d11b3c88cb2f7ce529c Mon Sep 17 00:00:00 2001 From: fiaxh Date: Fri, 6 Jan 2023 13:19:42 +0100 Subject: [PATCH 2/5] Add support for XEP-0461 replies (with fallback) --- libdino/CMakeLists.txt | 2 + libdino/src/application.vala | 2 + libdino/src/entity/message.vala | 37 +++++ libdino/src/plugin/interfaces.vala | 2 +- libdino/src/service/content_item_store.vala | 4 +- libdino/src/service/database.vala | 36 ++++- libdino/src/service/fallback_body.vala | 67 +++++++++ libdino/src/service/message_correction.vala | 1 + libdino/src/service/message_processor.vala | 20 +++ libdino/src/service/message_storage.vala | 8 +- libdino/src/service/replies.vala | 130 ++++++++++++++++++ main/CMakeLists.txt | 2 + main/data/chat_input.ui | 17 ++- main/data/conversation_content_view/view.ui | 123 +++++++---------- main/data/quote.ui | 80 +++++++++++ main/data/theme.css | 11 ++ .../ui/chat_input/chat_input_controller.vala | 37 ++++- main/src/ui/chat_input/view.vala | 15 +- .../conversation_item_skeleton.vala | 22 +-- .../conversation_view.vala | 6 - .../message_widget.vala | 29 +++- .../quote_widget.vala | 73 ++++++++++ .../reactions_widget.vala | 3 - .../conversation_selector_row.vala | 2 +- main/src/ui/main_window_controller.vala | 15 ++ xmpp-vala/CMakeLists.txt | 2 + .../module/xep/0428_fallback_indication.vala | 67 +++++++++ xmpp-vala/src/module/xep/0461_replies.vala | 41 ++++++ 28 files changed, 742 insertions(+), 112 deletions(-) create mode 100644 libdino/src/service/fallback_body.vala create mode 100644 libdino/src/service/replies.vala create mode 100644 main/data/quote.ui create mode 100644 main/src/ui/conversation_content_view/quote_widget.vala create mode 100644 xmpp-vala/src/module/xep/0428_fallback_indication.vala create mode 100644 xmpp-vala/src/module/xep/0461_replies.vala diff --git a/libdino/CMakeLists.txt b/libdino/CMakeLists.txt index 99c1426f..5aa4035f 100644 --- a/libdino/CMakeLists.txt +++ b/libdino/CMakeLists.txt @@ -40,6 +40,7 @@ SOURCES 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 @@ -51,6 +52,7 @@ SOURCES 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 diff --git a/libdino/src/application.vala b/libdino/src/application.vala index 229a9de1..ce9ec14a 100644 --- a/libdino/src/application.vala +++ b/libdino/src/application.vala @@ -56,6 +56,8 @@ public interface Application : GLib.Application { MessageCorrection.start(stream_interactor, db); FileTransferStorage.start(stream_interactor, db); Reactions.start(stream_interactor, db); + Replies.start(stream_interactor, db); + FallbackBody.start(stream_interactor, db); create_actions(); diff --git a/libdino/src/entity/message.vala b/libdino/src/entity/message.vala index b11e2622..912639b1 100644 --- a/libdino/src/entity/message.vala +++ b/libdino/src/entity/message.vala @@ -67,6 +67,9 @@ public class Message : Object { } } public string? edit_to = null; + public int quoted_item_id = 0; + + private Gee.List fallbacks = null; private Database? db; @@ -105,6 +108,7 @@ public class Message : Object { if (real_jid_str != null) real_jid = new Jid(real_jid_str); edit_to = row[db.message_correction.to_stanza_id]; + quoted_item_id = row[db.reply.quoted_content_item_id]; notify.connect(on_update); } @@ -138,6 +142,32 @@ public class Message : Object { notify.connect(on_update); } + public Gee.List get_fallbacks() { + if (fallbacks != null) return fallbacks; + + var fallbacks_by_ns = new HashMap>(); + foreach (Qlite.Row row in db.body_meta.select().with(db.body_meta.message_id, "=", id)) { + if (row[db.body_meta.info_type] != Xep.FallbackIndication.NS_URI) continue; + + string ns_uri = row[db.body_meta.info]; + if (!fallbacks_by_ns.has_key(ns_uri)) { + fallbacks_by_ns[ns_uri] = new ArrayList(); + } + fallbacks_by_ns[ns_uri].add(new Xep.FallbackIndication.FallbackLocation(row[db.body_meta.from_char], row[db.body_meta.to_char])); + } + + var fallbacks = new ArrayList(); + foreach (string ns_uri in fallbacks_by_ns.keys) { + fallbacks.add(new Xep.FallbackIndication.Fallback(ns_uri, fallbacks_by_ns[ns_uri].to_array())); + } + this.fallbacks = fallbacks; + return fallbacks; + } + + public void set_fallbacks(Gee.List fallbacks) { + this.fallbacks = fallbacks; + } + public void set_type_string(string type) { switch (type) { case Xmpp.MessageStanza.TYPE_CHAT: @@ -210,6 +240,13 @@ public class Message : Object { .value(db.real_jid.real_jid, real_jid.to_string()) .perform(); } + + if (sp.get_name() == "quoted-item-id") { + db.reply.upsert() + .value(db.reply.message_id, id, true) + .value(db.reply.quoted_content_item_id, quoted_item_id) + .perform(); + } } } diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala index b3402457..6a30f6dc 100644 --- a/libdino/src/plugin/interfaces.vala +++ b/libdino/src/plugin/interfaces.vala @@ -148,7 +148,7 @@ public abstract class MetaConversationItem : Object { } public interface ConversationItemWidgetInterface: Object { - public abstract void set_widget(Object object, WidgetType type); + public abstract void set_widget(Object object, WidgetType type, int priority); } public delegate void MessageActionEvoked(Object button, Plugins.MetaConversationItem evoked_on, Object widget); diff --git a/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala index 6371e00b..6a9e691f 100644 --- a/libdino/src/service/content_item_store.vala +++ b/libdino/src/service/content_item_store.vala @@ -40,7 +40,7 @@ public class ContentItemStore : StreamInteractionModule, Object { collection_conversations.unset(conversation); } - public Gee.List get_items_from_query(QueryBuilder select, Conversation conversation) { + private Gee.List get_items_from_query(QueryBuilder select, Conversation conversation) { Gee.TreeSet items = new Gee.TreeSet(ContentItem.compare_func); foreach (var row in select) { @@ -58,7 +58,7 @@ public class ContentItemStore : StreamInteractionModule, Object { return ret; } - public ContentItem get_item(Conversation conversation, int id, int content_type, int foreign_id, DateTime time) throws Error { + private ContentItem get_item(Conversation conversation, int id, int content_type, int foreign_id, DateTime time) throws Error { switch (content_type) { case 1: Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(foreign_id, conversation); diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index 5f422d2f..bfd85f06 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 = 23; + private const int VERSION = 24; public class AccountTable : Table { public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; @@ -97,6 +97,20 @@ public class Database : Qlite.Database { } } + public class BodyMeta : Table { + public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; + public Column message_id = new Column.Integer("message_id"); + public Column from_char = new Column.Integer("from_char"); + public Column to_char = new Column.Integer("to_char"); + public Column info_type = new Column.Text("info_type"); + public Column info = new Column.Text("info"); + + internal BodyMeta(Database db) { + base(db, "body_meta"); + init({id, message_id, from_char, to_char, info_type, info}); + } + } + public class MessageCorrectionTable : Table { public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; public Column message_id = new Column.Integer("message_id") { unique=true }; @@ -109,6 +123,20 @@ public class Database : Qlite.Database { } } + public class ReplyTable : Table { + public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; + public Column message_id = new Column.Integer("message_id") { not_null = true, unique=true }; + public Column quoted_content_item_id = new Column.Integer("quoted_message_id"); + public Column quoted_message_stanza_id = new Column.Text("quoted_message_stanza_id"); + public Column quoted_message_from = new Column.Text("quoted_message_from"); + + internal ReplyTable(Database db) { + base(db, "reply"); + init({id, message_id, quoted_content_item_id, quoted_message_stanza_id, quoted_message_from}); + index("reply_quoted_message_stanza_id", {quoted_message_stanza_id}); + } + } + public class RealJidTable : Table { public Column message_id = new Column.Integer("message_id") { primary_key = true }; public Column real_jid = new Column.Text("real_jid"); @@ -337,6 +365,8 @@ public class Database : Qlite.Database { public EntityTable entity { get; private set; } public ContentItemTable content_item { get; private set; } public MessageTable message { get; private set; } + public BodyMeta body_meta { get; private set; } + public ReplyTable reply { get; private set; } public MessageCorrectionTable message_correction { get; private set; } public RealJidTable real_jid { get; private set; } public OccupantIdTable occupantid { get; private set; } @@ -364,7 +394,9 @@ public class Database : Qlite.Database { entity = new EntityTable(this); content_item = new ContentItemTable(this); message = new MessageTable(this); + body_meta = new BodyMeta(this); message_correction = new MessageCorrectionTable(this); + reply = new ReplyTable(this); occupantid = new OccupantIdTable(this); real_jid = new RealJidTable(this); file_transfer = new FileTransferTable(this); @@ -379,7 +411,7 @@ public class Database : Qlite.Database { reaction = new ReactionTable(this); settings = new SettingsTable(this); conversation_settings = new ConversationSettingsTable(this); - init({ account, jid, entity, content_item, message, message_correction, real_jid, occupantid, file_transfer, call, call_counterpart, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, reaction, settings, conversation_settings }); + init({ account, jid, entity, content_item, message, body_meta, message_correction, reply, real_jid, occupantid, file_transfer, call, call_counterpart, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, reaction, settings, conversation_settings }); try { exec("PRAGMA journal_mode = WAL"); diff --git a/libdino/src/service/fallback_body.vala b/libdino/src/service/fallback_body.vala new file mode 100644 index 00000000..cc9ba9a6 --- /dev/null +++ b/libdino/src/service/fallback_body.vala @@ -0,0 +1,67 @@ +using Gee; +using Qlite; + +using Xmpp; +using Xmpp.Xep; +using Dino.Entities; + +public class Dino.FallbackBody : StreamInteractionModule, Object { + public static ModuleIdentity IDENTITY = new ModuleIdentity("fallback-body"); + public string id { get { return IDENTITY.id; } } + + private StreamInteractor stream_interactor; + private Database db; + + private ReceivedMessageListener received_message_listener; + + public static void start(StreamInteractor stream_interactor, Database db) { + FallbackBody m = new FallbackBody(stream_interactor, db); + stream_interactor.add_module(m); + } + + private FallbackBody(StreamInteractor stream_interactor, Database db) { + this.stream_interactor = stream_interactor; + this.db = db; + this.received_message_listener = new ReceivedMessageListener(stream_interactor, db); + + stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(received_message_listener); + } + + private class ReceivedMessageListener : MessageListener { + + public string[] after_actions_const = new string[]{ "STORE" }; + public override string action_group { get { return "Quote"; } } + public override string[] after_actions { get { return after_actions_const; } } + + private StreamInteractor stream_interactor; + private Database db; + + public ReceivedMessageListener(StreamInteractor stream_interactor, Database db) { + this.stream_interactor = stream_interactor; + this.db = db; + } + + public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + Gee.List fallbacks = Xep.FallbackIndication.get_fallbacks(stanza); + if (fallbacks.is_empty) return false; + + foreach (var fallback in fallbacks) { + if (fallback.ns_uri != Xep.Replies.NS_URI) continue; + + foreach (var location in fallback.locations) { + db.body_meta.insert() + .value(db.body_meta.message_id, message.id) + .value(db.body_meta.info_type, Xep.FallbackIndication.NS_URI) + .value(db.body_meta.info, fallback.ns_uri) + .value(db.body_meta.from_char, location.from_char) + .value(db.body_meta.to_char, location.to_char) + .perform(); + } + + message.set_fallbacks(fallbacks); + } + + return false; + } + } +} \ No newline at end of file diff --git a/libdino/src/service/message_correction.vala b/libdino/src/service/message_correction.vala index d5d15578..2c9078ea 100644 --- a/libdino/src/service/message_correction.vala +++ b/libdino/src/service/message_correction.vala @@ -44,6 +44,7 @@ public class MessageCorrection : StreamInteractionModule, MessageListener { Message out_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(correction_text, conversation); out_message.edit_to = stanza_id; + out_message.quoted_item_id = old_message.quoted_item_id; outstanding_correction_nodes[out_message.stanza_id] = stanza_id; stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(out_message, conversation); diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala index a290132f..62822658 100644 --- a/libdino/src/service/message_processor.vala +++ b/libdino/src/service/message_processor.vala @@ -424,6 +424,26 @@ public class MessageProcessor : StreamInteractionModule, Object { } else { 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.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 })); + } + } + } + build_message_stanza(message, new_message, conversation); pre_message_send(message, new_message, conversation); if (message.marked == Entities.Message.Marked.UNSENT || message.marked == Entities.Message.Marked.WONTSEND) return; diff --git a/libdino/src/service/message_storage.vala b/libdino/src/service/message_storage.vala index a44c0b02..fbdbcf8a 100644 --- a/libdino/src/service/message_storage.vala +++ b/libdino/src/service/message_storage.vala @@ -42,6 +42,7 @@ public class MessageStorage : StreamInteractionModule, Object { .with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation)) .order_by(db.message.time, "DESC") .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) .limit(count); Gee.List ret = new LinkedList(Message.equals_func); @@ -92,6 +93,7 @@ public class MessageStorage : StreamInteractionModule, Object { RowOption row_option = db.message.select().with(db.message.id, "=", id) .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) .row(); return create_message_from_row_opt(row_option, conversation); @@ -111,7 +113,8 @@ public class MessageStorage : StreamInteractionModule, Object { .with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation)) .with(db.message.stanza_id, "=", stanza_id) .order_by(db.message.time, "DESC") - .outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id); + .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); @@ -138,7 +141,8 @@ public class MessageStorage : StreamInteractionModule, Object { .with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation)) .with(db.message.server_id, "=", server_id) .order_by(db.message.time, "DESC") - .outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id); + .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); diff --git a/libdino/src/service/replies.vala b/libdino/src/service/replies.vala new file mode 100644 index 00000000..6a9bced4 --- /dev/null +++ b/libdino/src/service/replies.vala @@ -0,0 +1,130 @@ +using Gee; +using Qlite; + +using Xmpp; +using Xmpp.Xep; +using Dino.Entities; + +public class Dino.Replies : StreamInteractionModule, Object { + public static ModuleIdentity IDENTITY = new ModuleIdentity("reply"); + public string id { get { return IDENTITY.id; } } + + private StreamInteractor stream_interactor; + private Database db; + private HashMap>> unmapped_replies = new HashMap>>(); + + private ReceivedMessageListener received_message_listener; + + public static void start(StreamInteractor stream_interactor, Database db) { + Replies m = new Replies(stream_interactor, db); + stream_interactor.add_module(m); + } + + private Replies(StreamInteractor stream_interactor, Database db) { + this.stream_interactor = stream_interactor; + this.db = db; + this.received_message_listener = new ReceivedMessageListener(stream_interactor, this); + + stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(received_message_listener); + } + + public ContentItem? get_quoted_content_item(Message message, Conversation conversation) { + if (message.quoted_item_id == 0) return null; + + RowOption row_option = db.reply.select().with(db.reply.message_id, "=", message.id).row(); + if (row_option.is_present()) { + return stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, row_option[db.reply.quoted_content_item_id]); + } + return null; + } + + public void set_message_is_reply_to(Message message, ContentItem reply_to) { + message.quoted_item_id = reply_to.id; + + db.reply.upsert() + .value(db.reply.message_id, message.id, true) + .value(db.reply.quoted_content_item_id, reply_to.id) + .value_null(db.reply.quoted_message_stanza_id) + .value_null(db.reply.quoted_message_from) + .perform(); + } + + private void on_incoming_message(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + // Check if a previous message was in reply to this one + string relevant_id = conversation.type_ == Conversation.Type.GROUPCHAT ? message.server_id : message.stanza_id; + + var reply_qry = db.reply.select(); + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + reply_qry.with(db.reply.quoted_message_stanza_id, "=", message.server_id); + } else { + reply_qry.with(db.reply.quoted_message_stanza_id, "=", message.stanza_id); + } + reply_qry.join_with(db.message, db.reply.message_id, db.message.id) + .with(db.message.account_id, "=", conversation.account.id) + .with(db.message.counterpart_id, "=", db.get_jid_id(conversation.counterpart)) + .with(db.message.time, ">", (long)message.time.to_unix()) + .order_by(db.message.time, "DESC"); + + foreach (Row reply_row in reply_qry) { + ContentItem? message_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_foreign(conversation, 1, message.id); + Message? reply_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(reply_row[db.message.id], conversation); + if (message_item != null && reply_message != null) { + set_message_is_reply_to(reply_message, message_item); + } + } + + // Handle if this message is a reply + 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); + if (quoted_content_item == null) return; + + set_message_is_reply_to(message, quoted_content_item); + } + + private class ReceivedMessageListener : MessageListener { + + public string[] after_actions_const = new string[]{ "STORE", "STORE_CONTENT_ITEM" }; + public override string action_group { get { return "Quote"; } } + public override string[] after_actions { get { return after_actions_const; } } + + private Replies outer; + + public ReceivedMessageListener(StreamInteractor stream_interactor, Replies outer) { + this.outer = outer; + } + + public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + outer.on_incoming_message(message, stanza, conversation); + return false; + } + } +} + +namespace Dino { + public string message_body_without_reply_fallback(Message message) { + string body = message.body; + foreach (var fallback in message.get_fallbacks()) { + if (fallback.ns_uri == Xep.Replies.NS_URI && message.quoted_item_id > 0) { + body = body[0:fallback.locations[0].from_char] + body[fallback.locations[0].to_char:body.length]; + } + } + return body; + } +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 4fc06339..88b52c63 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -82,6 +82,7 @@ set(RESOURCE_LIST message_item_widget_edit_mode.ui occupant_list.ui occupant_list_item.ui + quote.ui search_autocomplete.ui settings_dialog.ui shortcuts.ui @@ -157,6 +158,7 @@ SOURCES src/ui/conversation_content_view/file_image_widget.vala src/ui/conversation_content_view/file_widget.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 diff --git a/main/data/chat_input.ui b/main/data/chat_input.ui index 99b087aa..5e84c360 100644 --- a/main/data/chat_input.ui +++ b/main/data/chat_input.ui @@ -32,8 +32,21 @@ - - 7 + + vertical + + + 10 + 10 + 10 + False + + + + + 7 + + diff --git a/main/data/conversation_content_view/view.ui b/main/data/conversation_content_view/view.ui index a9aae318..f6819b94 100644 --- a/main/data/conversation_content_view/view.ui +++ b/main/data/conversation_content_view/view.ui @@ -3,95 +3,64 @@