From fe45ab575c687febc1f342b0882a7597bd6ae9dc Mon Sep 17 00:00:00 2001 From: fiaxh Date: Sun, 26 May 2024 17:21:04 +0200 Subject: Support avatar deletion --- libdino/src/service/avatar_manager.vala | 43 +++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 13 deletions(-) (limited to 'libdino/src/service') diff --git a/libdino/src/service/avatar_manager.vala b/libdino/src/service/avatar_manager.vala index 3bd38e72..f99f37d4 100644 --- a/libdino/src/service/avatar_manager.vala +++ b/libdino/src/service/avatar_manager.vala @@ -160,30 +160,32 @@ public class AvatarManager : StreamInteractionModule, Object { } } + public void unset_avatar(Account account) { + XmppStream stream = stream_interactor.get_stream(account); + if (stream == null) return; + Xmpp.Xep.UserAvatars.unset_avatar(stream); + } + private void on_account_added(Account account) { stream_interactor.module_manager.get_module(account, Xep.UserAvatars.Module.IDENTITY).received_avatar_hash.connect((stream, jid, id) => - on_user_avatar_received.begin(account, jid, id) + on_user_avatar_received(account, jid, id) ); + stream_interactor.module_manager.get_module(account, Xep.UserAvatars.Module.IDENTITY).avatar_removed.connect((stream, jid) => { + on_user_avatar_removed(account, jid); + }); stream_interactor.module_manager.get_module(account, Xep.VCard.Module.IDENTITY).received_avatar_hash.connect((stream, jid, id) => - on_vcard_avatar_received.begin(account, jid, id) + on_vcard_avatar_received(account, jid, id) ); foreach (var entry in get_avatar_hashes(account, Source.USER_AVATARS).entries) { - on_user_avatar_received.begin(account, entry.key, entry.value); + on_user_avatar_received(account, entry.key, entry.value); } foreach (var entry in get_avatar_hashes(account, Source.VCARD).entries) { - - // FIXME: remove. temporary to remove falsely saved avatars. - if (stream_interactor.get_module(MucManager.IDENTITY).is_groupchat(entry.key, account)) { - db.avatar.delete().with(db.avatar.jid_id, "=", db.get_jid_id(entry.key)).perform(); - continue; - } - - on_vcard_avatar_received.begin(account, entry.key, entry.value); + on_vcard_avatar_received(account, entry.key, entry.value); } } - private async void on_user_avatar_received(Account account, Jid jid_, string id) { + private void on_user_avatar_received(Account account, Jid jid_, string id) { Jid jid = jid_.bare_jid; if (!user_avatars.has_key(jid) || user_avatars[jid] != id) { @@ -193,7 +195,14 @@ public class AvatarManager : StreamInteractionModule, Object { received_avatar(jid, account); } - private async void on_vcard_avatar_received(Account account, Jid jid_, string id) { + private void on_user_avatar_removed(Account account, Jid jid_) { + Jid jid = jid_.bare_jid; + user_avatars.unset(jid); + remove_avatar_hash(account, jid, Source.USER_AVATARS); + received_avatar(jid, account); + } + + private void on_vcard_avatar_received(Account account, Jid jid_, string id) { bool is_gc = stream_interactor.get_module(MucManager.IDENTITY).might_be_groupchat(jid_.bare_jid, account); Jid jid = is_gc ? jid_ : jid_.bare_jid; @@ -215,6 +224,14 @@ public class AvatarManager : StreamInteractionModule, Object { .perform(); } + public void remove_avatar_hash(Account account, Jid jid, int type) { + db.avatar.delete() + .with(db.avatar.jid_id, "=", db.get_jid_id(jid)) + .with(db.avatar.account_id, "=", account.id) + .with(db.avatar.type_, "=", type) + .perform(); + } + public HashMap get_avatar_hashes(Account account, int type) { HashMap ret = new HashMap(Jid.hash_func, Jid.equals_func); foreach (Row row in db.avatar.select({db.avatar.jid_id, db.avatar.hash}) -- cgit v1.2.3-70-g09d2 From f1be90c02f26c942e67978fd6d10ff2feeec8f9e Mon Sep 17 00:00:00 2001 From: fiaxh Date: Sun, 26 May 2024 17:28:28 +0200 Subject: Add logic for OMEMO by default setting --- libdino/src/entity/conversation.vala | 2 +- libdino/src/entity/settings.vala | 18 ++++++++++++++ libdino/src/service/conversation_manager.vala | 8 +++++++ libdino/src/service/database.vala | 32 +++++++++++++++++++++++-- plugins/omemo/src/ui/encryption_list_entry.vala | 7 +++++- 5 files changed, 63 insertions(+), 4 deletions(-) (limited to 'libdino/src/service') diff --git a/libdino/src/entity/conversation.vala b/libdino/src/entity/conversation.vala index 353daeae..4115ae83 100644 --- a/libdino/src/entity/conversation.vala +++ b/libdino/src/entity/conversation.vala @@ -33,7 +33,7 @@ public class Conversation : Object { } } } - public Encryption encryption { get; set; default = Encryption.NONE; } + public Encryption encryption { get; set; default = Encryption.UNKNOWN; } public Message? read_up_to { get; set; } public int read_up_to_item { get; set; default=-1; } diff --git a/libdino/src/entity/settings.vala b/libdino/src/entity/settings.vala index 0b09e9b9..be275efc 100644 --- a/libdino/src/entity/settings.vala +++ b/libdino/src/entity/settings.vala @@ -79,6 +79,24 @@ public class Settings : Object { check_spelling_ = value; } } + + public Encryption get_default_encryption(Account account) { + string? setting = db.account_settings.get_value(account.id, "default-encryption"); + if (setting != null) { + return (Encryption) int.parse(setting); + } + return Encryption.NONE; + } + + public void set_default_encryption(Account account, Encryption encryption) { + db.account_settings.upsert() + .value(db.account_settings.key, "default-encryption", true) + .value(db.account_settings.account_id, account.id, true) + .value(db.account_settings.value, ((int)encryption).to_string()) + .perform(); + + + } } } diff --git a/libdino/src/service/conversation_manager.vala b/libdino/src/service/conversation_manager.vala index f966ccc7..a757e8af 100644 --- a/libdino/src/service/conversation_manager.vala +++ b/libdino/src/service/conversation_manager.vala @@ -48,6 +48,14 @@ public class ConversationManager : StreamInteractionModule, Object { // Create a new converation Conversation conversation = new Conversation(jid, account, type); + // Set encryption for conversation + if (type == Conversation.Type.CHAT || + (type == Conversation.Type.GROUPCHAT && stream_interactor.get_module(MucManager.IDENTITY).is_private_room(account, jid))) { + conversation.encryption = Application.get_default().settings.get_default_encryption(account); + } else { + conversation.encryption = Encryption.NONE; + } + add_conversation(conversation); conversation.persist(db); return conversation; diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index dc1d68f3..eba8b7ca 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 = 26; + private const int VERSION = 27; public class AccountTable : Table { public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; @@ -354,6 +354,29 @@ public class Database : Qlite.Database { } } + public class AccountSettingsTable : Table { + public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; + public Column account_id = new Column.Integer("account_id") { not_null = true }; + public Column key = new Column.Text("key") { not_null = true }; + public Column value = new Column.Text("value"); + + internal AccountSettingsTable(Database db) { + base(db, "account_settings"); + init({id, account_id, key, value}); + unique({account_id, key}, "REPLACE"); + } + + public string? get_value(int account_id, string key) { + var row_opt = select({value}) + .with(this.account_id, "=", account_id) + .with(this.key, "=", key) + .single() + .row(); + if (row_opt.is_present()) return row_opt[value]; + return null; + } + } + public class ConversationSettingsTable : Table { public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; public Column conversation_id = new Column.Integer("conversation_id") {not_null=true}; @@ -388,6 +411,7 @@ public class Database : Qlite.Database { public MamCatchupTable mam_catchup { get; private set; } public ReactionTable reaction { get; private set; } public SettingsTable settings { get; private set; } + public AccountSettingsTable account_settings { get; private set; } public ConversationSettingsTable conversation_settings { get; private set; } public Map jid_table_cache = new HashMap(); @@ -417,8 +441,9 @@ public class Database : Qlite.Database { mam_catchup = new MamCatchupTable(this); reaction = new ReactionTable(this); settings = new SettingsTable(this); + account_settings = new AccountSettingsTable(this); conversation_settings = new ConversationSettingsTable(this); - 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 }); + 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, account_settings, conversation_settings }); try { exec("PRAGMA journal_mode = WAL"); @@ -576,6 +601,9 @@ public class Database : Qlite.Database { foreach(Row row in account.select()) { try { Account account = new Account.from_row(this, row); + if (account_table_cache.has_key(account.id)) { + account = account_table_cache[account.id]; + } ret.add(account); account_table_cache[account.id] = account; } catch (InvalidJidError e) { diff --git a/plugins/omemo/src/ui/encryption_list_entry.vala b/plugins/omemo/src/ui/encryption_list_entry.vala index b262ef81..3bb76c52 100644 --- a/plugins/omemo/src/ui/encryption_list_entry.vala +++ b/plugins/omemo/src/ui/encryption_list_entry.vala @@ -53,7 +53,12 @@ public class EncryptionListEntry : Plugins.EncryptionListEntry, Object { Manager omemo_manager = plugin.app.stream_interactor.get_module(Manager.IDENTITY); if (muc_manager.is_private_room(conversation.account, conversation.counterpart)) { - foreach (Jid offline_member in muc_manager.get_offline_members(conversation.counterpart, conversation.account)) { + var offline_members = muc_manager.get_offline_members(conversation.counterpart, conversation.account); + if (offline_members == null) { + // We don't store offline members yet, and it'll be null if we're offline + return; + } + foreach (Jid offline_member in offline_members) { bool ok = yield omemo_manager.ensure_get_keys_for_jid(conversation.account, offline_member); if (!ok) { input_status_callback(new Plugins.InputFieldStatus("A member does not support OMEMO: %s".printf(offline_member.to_string()), Plugins.InputFieldStatus.MessageType.ERROR, Plugins.InputFieldStatus.InputState.NO_SEND)); -- cgit v1.2.3-70-g09d2 From c8b20d0f5f33fb8b9898d216c3b4c9280abf31da Mon Sep 17 00:00:00 2001 From: fiaxh Date: Sun, 26 May 2024 18:24:54 +0200 Subject: Store requested disco results with computed hash, use for offline determining of private MUCs --- libdino/src/service/entity_info.vala | 35 ++++++++++++++++++++++++++++++----- libdino/src/service/muc_manager.vala | 11 ++--------- qlite/src/upsert_builder.vala | 8 ++++++-- 3 files changed, 38 insertions(+), 16 deletions(-) (limited to 'libdino/src/service') diff --git a/libdino/src/service/entity_info.vala b/libdino/src/service/entity_info.vala index d1217e81..83e27d4b 100644 --- a/libdino/src/service/entity_info.vala +++ b/libdino/src/service/entity_info.vala @@ -90,6 +90,20 @@ public class EntityInfo : StreamInteractionModule, Object { return info_result.features.contains(feature); } + public bool has_feature_offline(Account account, Jid jid, string feature) { + int ret = has_feature_cached_int(account, jid, feature); + if (ret == -1) { + return db.entity.select() + .with(db.entity.account_id, "=", account.id) + .with(db.entity.jid_id, "=", db.get_jid_id(jid)) + .with(db.entity.resource, "=", jid.resourcepart ?? "") + .join_with(db.entity_feature, db.entity.caps_hash, db.entity_feature.entity) + .with(db.entity_feature.feature, "=", feature) + .count() > 0; + } + return ret == 1; + } + public bool has_feature_cached(Account account, Jid jid, string feature) { return has_feature_cached_int(account, jid, feature) == 1; } @@ -203,13 +217,24 @@ public class EntityInfo : StreamInteractionModule, Object { ServiceDiscovery.InfoResult? info_result = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).request_info(stream, jid); if (info_result == null) return null; - if (hash != null && EntityCapabilities.Module.compute_hash_for_info_result(info_result) == hash) { - store_features(hash, info_result.features); - store_identities(hash, info_result.identities); + var computed_hash = EntityCapabilities.Module.compute_hash_for_info_result(info_result); + + if (hash == null || computed_hash == hash) { + db.entity.upsert() + .value(db.entity.account_id, account.id, true) + .value(db.entity.jid_id, db.get_jid_id(jid), true) + .value(db.entity.resource, jid.resourcepart ?? "", true) + .value(db.entity.last_seen, (long)(new DateTime.now_local()).to_unix()) + .value(db.entity.caps_hash, computed_hash) + .perform(); + + store_features(computed_hash, info_result.features); + store_identities(computed_hash, info_result.identities); } else { - jid_features[jid] = info_result.features; - jid_identity[jid] = info_result.identities; + warning("Claimed entity caps hash from %s doesn't match computed one", jid.to_string()); } + jid_features[jid] = info_result.features; + jid_identity[jid] = info_result.identities; return info_result; } diff --git a/libdino/src/service/muc_manager.vala b/libdino/src/service/muc_manager.vala index 119079f0..6b52fe36 100644 --- a/libdino/src/service/muc_manager.vala +++ b/libdino/src/service/muc_manager.vala @@ -232,15 +232,8 @@ public class MucManager : StreamInteractionModule, Object { //the term `private room` is a short hand for members-only+non-anonymous rooms public bool is_private_room(Account account, Jid jid) { - XmppStream? stream = stream_interactor.get_stream(account); - if (stream == null) { - return false; - } - Xep.Muc.Flag? flag = stream.get_flag(Xep.Muc.Flag.IDENTITY); - if (flag == null) { - return false; - } - return flag.has_room_feature(jid, Xep.Muc.Feature.NON_ANONYMOUS) && flag.has_room_feature(jid, Xep.Muc.Feature.MEMBERS_ONLY); + var entity_info = stream_interactor.get_module(EntityInfo.IDENTITY); + return entity_info.has_feature_offline(account, jid, "muc_membersonly") && entity_info.has_feature_offline(account, jid, "muc_nonanonymous"); } public bool is_moderated_room(Account account, Jid jid) { diff --git a/qlite/src/upsert_builder.vala b/qlite/src/upsert_builder.vala index 7daf7109..79104972 100644 --- a/qlite/src/upsert_builder.vala +++ b/qlite/src/upsert_builder.vala @@ -26,9 +26,13 @@ public class UpsertBuilder : StatementBuilder { return this; } - public UpsertBuilder value_null(Column column) { + public UpsertBuilder value_null(Column column, bool key = false) { if (column.not_null) error("Can't set non-null column %s to null", column.name); - fields += new NullField(column); + if (key) { + keys += new NullField(column); + } else { + fields += new NullField(column); + } return this; } -- cgit v1.2.3-70-g09d2 From da4ded964f122ffef194d3f7d7cf7fd0fd71d8cf Mon Sep 17 00:00:00 2001 From: Matthew Fennell Date: Fri, 28 Jun 2024 15:33:59 +0100 Subject: Self-ping with server-given roomnick (#1594) * Self-ping with server-given roomnick (#1467) XEP-0045 describes how a server can modify a user's roomnick when joining a room. To do this, it assigns status code 210, and sets the "from" attribute to a JID with thew newly assigned nickname when sending the user's presence back to the client. This is used in IRC gateways such as biboumi. Since you can only have one nick per IRC server, if you try and join multiple IRC-bridged MUCs on the same IRC server, it will modify your nick to match the existing one you have on that server. Currently, when Dino performs a self-ping after joining a room, it uses the nick that the client first requested, instead of the nick returned from the server. This can lead to incorrect self-pings being sent out (until the client is restarted, and populates mucs_todo correctly). This happens because it adds the requested Jid to mucs_todo, before getting the given nick from the server. Therefore, this commit adds the jid that has been given from the server instead. If there is any error in requesting to join the MUC, it adds the requested nick, to match existing behaviour and allow the request to be retried via future self-pings. fixes #1467 * Minor coding style changes --------- Co-authored-by: fiaxh --- libdino/src/service/muc_manager.vala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'libdino/src/service') diff --git a/libdino/src/service/muc_manager.vala b/libdino/src/service/muc_manager.vala index 6b52fe36..111ace22 100644 --- a/libdino/src/service/muc_manager.vala +++ b/libdino/src/service/muc_manager.vala @@ -84,11 +84,6 @@ public class MucManager : StreamInteractionModule, Object { } mucs_joining[account].add(jid); - if (!mucs_todo.has_key(account)) { - mucs_todo[account] = new HashSet(Jid.hash_bare_func, Jid.equals_bare_func); - } - mucs_todo[account].add(jid.with_resource(nick_)); - Muc.JoinResult? res = yield stream.get_module(Xep.Muc.Module.IDENTITY).enter(stream, jid.bare_jid, nick_, password, history_since, receive_history, null); mucs_joining[account].remove(jid); @@ -127,6 +122,11 @@ public class MucManager : StreamInteractionModule, Object { enter_errors[jid] = res.muc_error; } + if (!mucs_todo.has_key(account)) { + mucs_todo[account] = new HashSet(Jid.hash_bare_func, Jid.equals_bare_func); + } + mucs_todo[account].add(jid.with_resource(res.nick ?? nick_)); + return res; } -- cgit v1.2.3-70-g09d2 From ceb921a0148f7fdc2a9df3e6b85143bf8c26c341 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Fri, 26 Jul 2024 19:10:35 +0200 Subject: Store reply message as sent, with fallback --- libdino/src/entity/message.vala | 36 ++++++++++++++++++++++- libdino/src/service/fallback_body.vala | 15 ++-------- libdino/src/service/message_correction.vala | 2 +- libdino/src/service/message_processor.vala | 36 +++++++++-------------- libdino/src/service/replies.vala | 15 ++-------- main/src/ui/chat_input/chat_input_controller.vala | 13 +++++++- 6 files changed, 66 insertions(+), 51 deletions(-) (limited to 'libdino/src/service') diff --git a/libdino/src/entity/message.vala b/libdino/src/entity/message.vala index 9d1cd43e..4e6c7f45 100644 --- a/libdino/src/entity/message.vala +++ b/libdino/src/entity/message.vala @@ -67,7 +67,7 @@ public class Message : Object { } } public string? edit_to = null; - public int quoted_item_id = 0; + public int quoted_item_id { get; private set; default=0; } private Gee.List fallbacks = null; @@ -142,6 +142,22 @@ public class Message : Object { notify.connect(on_update); } + public void set_quoted_item(int quoted_content_item_id) { + if (id == -1) { + warning("Message needs to be persisted before setting quoted item"); + return; + } + + this.quoted_item_id = quoted_content_item_id; + + db.reply.upsert() + .value(db.reply.message_id, id, true) + .value(db.reply.quoted_content_item_id, quoted_content_item_id) + .value_null(db.reply.quoted_message_stanza_id) + .value_null(db.reply.quoted_message_from) + .perform(); + } + public Gee.List get_fallbacks() { if (fallbacks != null) return fallbacks; @@ -165,7 +181,25 @@ public class Message : Object { } public void set_fallbacks(Gee.List fallbacks) { + if (id == -1) { + warning("Message needs to be persisted before setting fallbacks"); + return; + } + this.fallbacks = fallbacks; + + foreach (var fallback in fallbacks) { + foreach (var location in fallback.locations) { + db.body_meta.insert() + .value(db.body_meta.message_id, 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(); + } + } + } public void set_type_string(string type) { diff --git a/libdino/src/service/fallback_body.vala b/libdino/src/service/fallback_body.vala index 13323427..0ce89ade 100644 --- a/libdino/src/service/fallback_body.vala +++ b/libdino/src/service/fallback_body.vala @@ -46,20 +46,9 @@ public class Dino.FallbackBody : StreamInteractionModule, Object { 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); + if (fallback.ns_uri != Xep.Replies.NS_URI) continue; // TODO what if it's not } + message.set_fallbacks(fallbacks); return false; } diff --git a/libdino/src/service/message_correction.vala b/libdino/src/service/message_correction.vala index 8f9770d8..6d4137d4 100644 --- a/libdino/src/service/message_correction.vala +++ b/libdino/src/service/message_correction.vala @@ -44,7 +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; + out_message.set_quoted_item(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 baab37ce..620c93eb 100644 --- a/libdino/src/service/message_processor.vala +++ b/libdino/src/service/message_processor.vala @@ -406,8 +406,20 @@ public class MessageProcessor : StreamInteractionModule, Object { new_message.type_ = MessageStanza.TYPE_CHAT; } - string? fallback = get_fallback_body_set_infos(message, new_message, conversation); - new_message.body = fallback == null ? message.body : fallback + message.body; + if (message.quoted_item_id != 0) { + ContentItem? quoted_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, message.quoted_item_id); + if (quoted_content_item != null) { + Jid? quoted_sender = message.from; + string? quoted_stanza_id = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_id_for_content_item(conversation, quoted_content_item); + if (quoted_sender != null && quoted_stanza_id != null) { + Xep.Replies.set_reply_to(new_message, new Xep.Replies.ReplyTo(quoted_sender, quoted_stanza_id)); + } + + foreach (var fallback in message.get_fallbacks()) { + Xep.FallbackIndication.set_fallback(new_message, fallback); + } + } + } build_message_stanza(message, new_message, conversation); pre_message_send(message, new_message, conversation); @@ -456,26 +468,6 @@ 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 = FallbackBody.get_quoted_fallback_body(content_item); - - var fallback_location = new Xep.FallbackIndication.FallbackLocation(0, (int)fallback.char_count()); - 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/replies.vala b/libdino/src/service/replies.vala index 58d44b37..cc9f43cc 100644 --- a/libdino/src/service/replies.vala +++ b/libdino/src/service/replies.vala @@ -38,17 +38,6 @@ public class Dino.Replies : StreamInteractionModule, Object { 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 var reply_qry = db.reply.select(); @@ -67,7 +56,7 @@ public class Dino.Replies : StreamInteractionModule, Object { 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); + reply_message.set_quoted_item(message_item.id); } } @@ -78,7 +67,7 @@ public class Dino.Replies : StreamInteractionModule, Object { 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); + message.set_quoted_item(quoted_content_item.id); } private class ReceivedMessageListener : MessageListener { diff --git a/main/src/ui/chat_input/chat_input_controller.vala b/main/src/ui/chat_input/chat_input_controller.vala index d1c42d35..cf8e5a02 100644 --- a/main/src/ui/chat_input/chat_input_controller.vala +++ b/main/src/ui/chat_input/chat_input_controller.vala @@ -1,6 +1,7 @@ using Gee; using Gdk; using Gtk; +using Xmpp; using Dino.Entities; @@ -195,7 +196,17 @@ public class ChatInputController : Object { } Message out_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(text, conversation); if (quoted_content_item_bak != null) { - stream_interactor.get_module(Replies.IDENTITY).set_message_is_reply_to(out_message, quoted_content_item_bak); + out_message.set_quoted_item(quoted_content_item_bak.id); + + // Store body with fallback + string fallback = FallbackBody.get_quoted_fallback_body(quoted_content_item_bak); + out_message.body = fallback + out_message.body; + + // Store fallback location + var fallback_location = new Xep.FallbackIndication.FallbackLocation(0, (int)fallback.char_count()); + var fallback_list = new ArrayList(); + fallback_list.add(new Xep.FallbackIndication.Fallback(Xep.Replies.NS_URI, new Xep.FallbackIndication.FallbackLocation[] { fallback_location })); + out_message.set_fallbacks(fallback_list); } stream_interactor.get_module(MessageProcessor.IDENTITY).send_message(out_message, conversation); } -- cgit v1.2.3-70-g09d2 From b0ff90a14a5d127e17f2371f87e7bb659de3a68f Mon Sep 17 00:00:00 2001 From: fiaxh Date: Mon, 29 Jul 2024 13:16:54 +0200 Subject: Add initial message markup (XEP-0394) support --- libdino/CMakeLists.txt | 1 + libdino/meson.build | 1 + libdino/src/entity/message.vala | 52 +++++++-- libdino/src/service/message_correction.vala | 28 ++--- libdino/src/service/message_processor.vala | 34 ++++-- libdino/src/util/send_message.vala | 56 +++++++++ main/src/ui/chat_input/chat_input_controller.vala | 19 +-- main/src/ui/chat_input/chat_text_view.vala | 130 +++++++++++++++++++++ .../conversation_content_view/message_widget.vala | 105 ++++++++++++----- main/src/ui/util/helper.vala | 25 ++++ xmpp-vala/CMakeLists.txt | 1 + xmpp-vala/meson.build | 1 + xmpp-vala/src/module/xep/0394_message_markup.vala | 81 +++++++++++++ 13 files changed, 456 insertions(+), 78 deletions(-) create mode 100644 libdino/src/util/send_message.vala create mode 100644 xmpp-vala/src/module/xep/0394_message_markup.vala (limited to 'libdino/src/service') diff --git a/libdino/CMakeLists.txt b/libdino/CMakeLists.txt index e4d786c9..34cf9575 100644 --- a/libdino/CMakeLists.txt +++ b/libdino/CMakeLists.txt @@ -68,6 +68,7 @@ SOURCES src/service/util.vala src/util/display_name.vala + src/util/send_message.vala src/util/util.vala src/util/weak_map.vala src/util/weak_timeout.vala diff --git a/libdino/meson.build b/libdino/meson.build index 17804d23..559a81b5 100644 --- a/libdino/meson.build +++ b/libdino/meson.build @@ -74,6 +74,7 @@ sources = files( 'src/service/stream_interactor.vala', 'src/service/util.vala', 'src/util/display_name.vala', + 'src/util/send_message.vala', 'src/util/util.vala', 'src/util/weak_map.vala', 'src/util/weak_timeout.vala', diff --git a/libdino/src/entity/message.vala b/libdino/src/entity/message.vala index 4e6c7f45..e5aad25f 100644 --- a/libdino/src/entity/message.vala +++ b/libdino/src/entity/message.vala @@ -70,6 +70,7 @@ public class Message : Object { public int quoted_item_id { get; private set; default=0; } private Gee.List fallbacks = null; + private Gee.List markups = null; private Database? db; @@ -160,16 +161,53 @@ public class Message : Object { public Gee.List get_fallbacks() { if (fallbacks != null) return fallbacks; + fetch_body_meta(); + return fallbacks; + } + + public Gee.List get_markups() { + if (markups != null) return markups; + fetch_body_meta(); + + return markups; + } + + public void persist_markups(Gee.List markups, int message_id) { + this.markups = markups; + + foreach (var span in markups) { + foreach (var ty in span.types) { + db.body_meta.insert() + .value(db.body_meta.info_type, Xep.MessageMarkup.NS_URI) + .value(db.body_meta.message_id, message_id) + .value(db.body_meta.info, Xep.MessageMarkup.span_type_to_str(ty)) + .value(db.body_meta.from_char, span.start_char) + .value(db.body_meta.to_char, span.end_char) + .perform(); + } + } + } + + private void fetch_body_meta() { 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; + var markups = new ArrayList(); - string ns_uri = row[db.body_meta.info]; - if (!fallbacks_by_ns.has_key(ns_uri)) { - fallbacks_by_ns[ns_uri] = new ArrayList(); + foreach (Qlite.Row row in db.body_meta.select().with(db.body_meta.message_id, "=", id)) { + switch (row[db.body_meta.info_type]) { + case Xep.FallbackIndication.NS_URI: + 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])); + break; + case Xep.MessageMarkup.NS_URI: + var types = new ArrayList(); + types.add(Xep.MessageMarkup.str_to_span_type(row[db.body_meta.info])); + markups.add(new Xep.MessageMarkup.Span() { types=types, start_char=row[db.body_meta.from_char], end_char=row[db.body_meta.to_char] }); + break; } - 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(); @@ -177,7 +215,7 @@ public class Message : Object { fallbacks.add(new Xep.FallbackIndication.Fallback(ns_uri, fallbacks_by_ns[ns_uri].to_array())); } this.fallbacks = fallbacks; - return fallbacks; + this.markups = markups; } public void set_fallbacks(Gee.List fallbacks) { diff --git a/libdino/src/service/message_correction.vala b/libdino/src/service/message_correction.vala index 6d4137d4..e6401a05 100644 --- a/libdino/src/service/message_correction.vala +++ b/libdino/src/service/message_correction.vala @@ -39,27 +39,21 @@ public class MessageCorrection : StreamInteractionModule, MessageListener { }); } - public void send_correction(Conversation conversation, Message old_message, string correction_text) { - string stanza_id = old_message.edit_to ?? old_message.stanza_id; + public void set_correction(Conversation conversation, Message message, Message old_message) { + string reference_stanza_id = old_message.edit_to ?? old_message.stanza_id; - Message out_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(correction_text, conversation); - out_message.edit_to = stanza_id; - out_message.set_quoted_item(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); + outstanding_correction_nodes[message.stanza_id] = reference_stanza_id; db.message_correction.insert() - .value(db.message_correction.message_id, out_message.id) - .value(db.message_correction.to_stanza_id, stanza_id) - .perform(); + .value(db.message_correction.message_id, message.id) + .value(db.message_correction.to_stanza_id, reference_stanza_id) + .perform(); db.content_item.update() - .with(db.content_item.foreign_id, "=", old_message.id) - .with(db.content_item.content_type, "=", 1) - .set(db.content_item.foreign_id, out_message.id) - .perform(); - - on_received_correction(conversation, out_message.id); + .with(db.content_item.foreign_id, "=", old_message.id) + .with(db.content_item.content_type, "=", 1) + .set(db.content_item.foreign_id, message.id) + .perform(); } public bool is_own_correction_allowed(Conversation conversation, Message message) { @@ -145,7 +139,7 @@ public class MessageCorrection : StreamInteractionModule, MessageListener { return false; } - private void on_received_correction(Conversation conversation, int message_id) { + public void on_received_correction(Conversation conversation, int message_id) { ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_foreign(conversation, 1, message_id); if (content_item != null) { received_correction(content_item); diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala index 620c93eb..d8ea3e2d 100644 --- a/libdino/src/service/message_processor.vala +++ b/libdino/src/service/message_processor.vala @@ -38,6 +38,7 @@ public class MessageProcessor : StreamInteractionModule, Object { received_pipeline.connect(new FilterMessageListener()); received_pipeline.connect(new StoreMessageListener(this, stream_interactor)); received_pipeline.connect(new StoreContentItemListener(stream_interactor)); + received_pipeline.connect(new MarkupListener(stream_interactor)); stream_interactor.account_added.connect(on_account_added); @@ -45,18 +46,6 @@ public class MessageProcessor : StreamInteractionModule, Object { stream_interactor.stream_resumed.connect(send_unsent_chat_messages); } - public Entities.Message send_text(string text, Conversation conversation) { - Entities.Message message = create_out_message(text, conversation); - return send_message(message, conversation); - } - - public Entities.Message send_message(Entities.Message message, Conversation conversation) { - stream_interactor.get_module(ContentItemStore.IDENTITY).insert_message(message, conversation); - send_xmpp_message(message, conversation); - message_sent(message, conversation); - return message; - } - private void convert_sending_to_unsent_msgs(Account account) { db.message.update() .with(db.message.account_id, "=", account.id) @@ -344,6 +333,25 @@ public class MessageProcessor : StreamInteractionModule, Object { } } + private class MarkupListener : MessageListener { + + public string[] after_actions_const = new string[]{ "STORE" }; + public override string action_group { get { return "Markup"; } } + public override string[] after_actions { get { return after_actions_const; } } + + private StreamInteractor stream_interactor; + + public MarkupListener(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + Gee.List markups = MessageMarkup.get_spans(stanza); + message.persist_markups(markups, message.id); + return false; + } + } + private class StoreContentItemListener : MessageListener { public string[] after_actions_const = new string[]{ "DEDUPLICATE", "DECRYPT", "FILTER_EMPTY", "STORE", "CORRECTION", "MESSAGE_REINTERPRETING" }; @@ -421,6 +429,8 @@ public class MessageProcessor : StreamInteractionModule, Object { } } + MessageMarkup.add_spans(new_message, message.get_markups()); + 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/util/send_message.vala b/libdino/src/util/send_message.vala new file mode 100644 index 00000000..234a1644 --- /dev/null +++ b/libdino/src/util/send_message.vala @@ -0,0 +1,56 @@ +using Gee; + +using Dino.Entities; +using Xmpp; + +namespace Dino { + + public void send_message(Conversation conversation, string text, int reply_to_id, Message? correction_to, Gee.List markups) { + StreamInteractor stream_interactor = Application.get_default().stream_interactor; + + Message out_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(text, conversation); + + if (correction_to != null) { + string correction_to_stanza_id = correction_to.edit_to ?? correction_to.stanza_id; + out_message.edit_to = correction_to_stanza_id; + stream_interactor.get_module(MessageCorrection.IDENTITY).set_correction(conversation, out_message, correction_to); + } + + if (reply_to_id != 0) { + ContentItem reply_to = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, reply_to_id); + + out_message.set_quoted_item(reply_to.id); + + // Store body with fallback + string fallback = FallbackBody.get_quoted_fallback_body(reply_to); + out_message.body = fallback + out_message.body; + + // Store fallback location + var fallback_location = new Xep.FallbackIndication.FallbackLocation(0, (int)fallback.char_count()); + var fallback_list = new ArrayList(); + fallback_list.add(new Xep.FallbackIndication.Fallback(Xep.Replies.NS_URI, new Xep.FallbackIndication.FallbackLocation[] { fallback_location })); + out_message.set_fallbacks(fallback_list); + + // Adjust markups to new prefix + foreach (var span in markups) { + span.start_char += fallback.length; + span.end_char += fallback.length; + } + } + + if (!markups.is_empty) { + out_message.persist_markups(markups, out_message.id); + } + + + if (correction_to != null) { + stream_interactor.get_module(MessageCorrection.IDENTITY).on_received_correction(conversation, out_message.id); + stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(out_message, conversation); + return; + } + + stream_interactor.get_module(ContentItemStore.IDENTITY).insert_message(out_message, conversation); + stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(out_message, conversation); + stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent(out_message, conversation); + } +} \ No newline at end of file diff --git a/main/src/ui/chat_input/chat_input_controller.vala b/main/src/ui/chat_input/chat_input_controller.vala index cf8e5a02..07499aa4 100644 --- a/main/src/ui/chat_input/chat_input_controller.vala +++ b/main/src/ui/chat_input/chat_input_controller.vala @@ -3,6 +3,7 @@ using Gdk; using Gtk; using Xmpp; +using Xmpp; using Dino.Entities; namespace Dino.Ui { @@ -136,6 +137,7 @@ public class ChatInputController : Object { string text = chat_input.chat_text_view.text_view.buffer.text; ContentItem? quoted_content_item_bak = quoted_content_item; + var markups = chat_input.chat_text_view.get_markups(); // Reset input state. Has do be done before parsing commands, because those directly return. chat_input.chat_text_view.text_view.buffer.text = ""; @@ -194,21 +196,8 @@ public class ChatInputController : Object { break; } } - Message out_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(text, conversation); - if (quoted_content_item_bak != null) { - out_message.set_quoted_item(quoted_content_item_bak.id); - - // Store body with fallback - string fallback = FallbackBody.get_quoted_fallback_body(quoted_content_item_bak); - out_message.body = fallback + out_message.body; - - // Store fallback location - var fallback_location = new Xep.FallbackIndication.FallbackLocation(0, (int)fallback.char_count()); - var fallback_list = new ArrayList(); - fallback_list.add(new Xep.FallbackIndication.Fallback(Xep.Replies.NS_URI, new Xep.FallbackIndication.FallbackLocation[] { fallback_location })); - out_message.set_fallbacks(fallback_list); - } - stream_interactor.get_module(MessageProcessor.IDENTITY).send_message(out_message, conversation); + + Dino.send_message(conversation, text, quoted_content_item_bak != null ? quoted_content_item_bak.id : 0, null, markups); } private void on_text_input_changed() { diff --git a/main/src/ui/chat_input/chat_text_view.vala b/main/src/ui/chat_input/chat_text_view.vala index aa246d8d..c7429318 100644 --- a/main/src/ui/chat_input/chat_text_view.vala +++ b/main/src/ui/chat_input/chat_text_view.vala @@ -40,6 +40,10 @@ public class ChatTextView : Box { private uint wait_queue_resize; private SmileyConverter smiley_converter; + private TextTag italic_tag; + private TextTag bold_tag; + private TextTag strikethrough_tag; + construct { valign = Align.CENTER; scrolled_window.set_child(text_view); @@ -49,6 +53,15 @@ public class ChatTextView : Box { text_input_key_events.key_pressed.connect(on_text_input_key_press); text_view.add_controller(text_input_key_events); + italic_tag = text_view.buffer.create_tag("italic"); + italic_tag.style = Pango.Style.ITALIC; + + bold_tag = text_view.buffer.create_tag("bold"); + bold_tag.weight = Pango.Weight.BOLD; + + strikethrough_tag = text_view.buffer.create_tag("strikethrough"); + strikethrough_tag.strikethrough = true; + smiley_converter = new SmileyConverter(text_view); scrolled_window.vadjustment.changed.connect(on_upper_notify); @@ -60,6 +73,37 @@ public class ChatTextView : Box { }); } + public void set_text(Message message) { + // Get a copy of the markup spans, such that we can modify them + var markups = new ArrayList(); + foreach (var markup in message.get_markups()) { + markups.add(new Xep.MessageMarkup.Span() { types=markup.types, start_char=markup.start_char, end_char=markup.end_char }); + } + + text_view.buffer.text = Util.remove_fallbacks_adjust_markups(message.body, message.quoted_item_id > 0, message.get_fallbacks(), markups); + + foreach (var markup in markups) { + foreach (var ty in markup.types) { + TextTag tag = null; + switch (ty) { + case Xep.MessageMarkup.SpanType.EMPHASIS: + tag = italic_tag; + break; + case Xep.MessageMarkup.SpanType.STRONG_EMPHASIS: + tag = bold_tag; + break; + case Xep.MessageMarkup.SpanType.DELETED: + tag = strikethrough_tag; + break; + } + TextIter start_selection, end_selection; + text_view.buffer.get_iter_at_offset(out start_selection, markup.start_char); + text_view.buffer.get_iter_at_offset(out end_selection, markup.end_char); + text_view.buffer.apply_tag(tag, start_selection, end_selection); + } + } + } + public override void dispose() { base.dispose(); if (wait_queue_resize != 0) { @@ -95,6 +139,7 @@ public class ChatTextView : Box { } private bool on_text_input_key_press(EventControllerKey controller, uint keyval, uint keycode, Gdk.ModifierType state) { + // Enter pressed -> Send message (except if it was Shift+Enter) if (keyval in new uint[]{ Key.Return, Key.KP_Enter }) { // Allow the text view to process the event. Needed for IME. if (text_view.im_context_filter_keypress(controller.get_current_event())) { @@ -109,11 +154,96 @@ public class ChatTextView : Box { } return true; } + if (keyval == Key.Escape) { cancel_input(); } + + // Style text section bold (CTRL + b) or italic (CTRL + i) + if ((state & ModifierType.CONTROL_MASK) > 0) { + if (keyval in new uint[]{ Key.i, Key.b }) { + TextIter start_selection, end_selection; + text_view.buffer.get_selection_bounds(out start_selection, out end_selection); + + TextTag tag = null; + bool already_formatted = false; + var markup_types = get_markup_types_from_iter(start_selection); + if (keyval == Key.i) { + tag = italic_tag; + already_formatted = markup_types.contains(Xep.MessageMarkup.SpanType.EMPHASIS); + } else if (keyval == Key.b) { + tag = bold_tag; + already_formatted = markup_types.contains(Xep.MessageMarkup.SpanType.STRONG_EMPHASIS); + } else if (keyval == Key.s) { + tag = strikethrough_tag; + already_formatted = markup_types.contains(Xep.MessageMarkup.SpanType.DELETED); + } + if (tag != null) { + if (already_formatted) { + text_view.buffer.remove_tag(tag, start_selection, end_selection); + } else { + text_view.buffer.apply_tag(tag, start_selection, end_selection); + } + } + } + } + return false; } + + public Gee.List get_markups() { + var markups = new HashMap(); + markups[Xep.MessageMarkup.SpanType.EMPHASIS] = Xep.MessageMarkup.SpanType.EMPHASIS; + markups[Xep.MessageMarkup.SpanType.STRONG_EMPHASIS] = Xep.MessageMarkup.SpanType.STRONG_EMPHASIS; + markups[Xep.MessageMarkup.SpanType.DELETED] = Xep.MessageMarkup.SpanType.DELETED; + + var ended_groups = new ArrayList(); + Xep.MessageMarkup.Span current_span = null; + + TextIter iter; + text_view.buffer.get_start_iter(out iter); + int i = 0; + do { + var char_markups = get_markup_types_from_iter(iter); + + // Not the same set of markups as last character -> end all spans + if (current_span != null && (!char_markups.contains_all(current_span.types) || !current_span.types.contains_all(char_markups))) { + ended_groups.add(current_span); + current_span = null; + } + + if (char_markups.size > 0) { + if (current_span == null) { + current_span = new Xep.MessageMarkup.Span() { types=char_markups, start_char=i, end_char=i + 1 }; + } else { + current_span.end_char = i + 1; + } + } + + i++; + } while (iter.forward_char()); + + if (current_span != null) { + ended_groups.add(current_span); + } + + return ended_groups; + } + + private Gee.List get_markup_types_from_iter(TextIter iter) { + var ret = new ArrayList(); + + foreach (TextTag tag in iter.get_tags()) { + if (tag.style == Pango.Style.ITALIC) { + ret.add(Xep.MessageMarkup.SpanType.EMPHASIS); + } else if (tag.weight == Pango.Weight.BOLD) { + ret.add(Xep.MessageMarkup.SpanType.STRONG_EMPHASIS); + } else if (tag.strikethrough) { + ret.add(Xep.MessageMarkup.SpanType.DELETED); + } + } + return ret; + } } } diff --git a/main/src/ui/conversation_content_view/message_widget.vala b/main/src/ui/conversation_content_view/message_widget.vala index 11b38286..376ef4bd 100644 --- a/main/src/ui/conversation_content_view/message_widget.vala +++ b/main/src/ui/conversation_content_view/message_widget.vala @@ -26,8 +26,8 @@ public class MessageMetaItem : ContentMetaItem { AdditionalInfo additional_info = AdditionalInfo.NONE; ulong realize_id = -1; - ulong style_updated_id = -1; ulong marked_notify_handler_id = -1; + uint pending_timeout_id = -1; public Label label = new Label("") { use_markup=true, xalign=0, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=true, vexpand=true }; @@ -64,6 +64,7 @@ public class MessageMetaItem : ContentMetaItem { if (message.marked in Message.MARKED_RECEIVED) { binding.unbind(); this.disconnect(marked_notify_handler_id); + marked_notify_handler_id = -1; } }); } @@ -71,20 +72,72 @@ public class MessageMetaItem : ContentMetaItem { update_label(); } - private string generate_markup_text(ContentItem item) { + private void generate_markup_text(ContentItem item, Label label) { MessageItem message_item = item as MessageItem; Conversation conversation = message_item.conversation; Message message = message_item.message; - bool theme_dependent = false; + // Get a copy of the markup spans, such that we can modify them + var markups = new ArrayList(); + foreach (var markup in message.get_markups()) { + markups.add(new Xep.MessageMarkup.Span() { types=markup.types, start_char=markup.start_char, end_char=markup.end_char }); + } + + string markup_text = message.body; + + var attrs = new AttrList(); + label.set_attributes(attrs); - string markup_text = Dino.message_body_without_reply_fallback(message); + if (markup_text == null) return; // TODO remove + // Only process messages up to a certain size if (markup_text.length > 10000) { markup_text = markup_text.substring(0, 10000) + " [" + _("Message too long") + "]"; } - if (message.body.has_prefix("/me ")) { - markup_text = markup_text.substring(4); + + bool theme_dependent = false; + + markup_text = Util.remove_fallbacks_adjust_markups(markup_text, message.quoted_item_id > 0, message.get_fallbacks(), markups); + + var bold_attr = Pango.attr_weight_new(Pango.Weight.BOLD); + var italic_attr = Pango.attr_style_new(Pango.Style.ITALIC); + var strikethrough_attr = Pango.attr_strikethrough_new(true); + + // Prefix message with name instead of /me + if (markup_text.has_prefix("/me ")) { + string display_name = Util.get_participant_display_name(stream_interactor, conversation, message.from); + markup_text = display_name + " " + markup_text.substring(4); + + foreach (Xep.MessageMarkup.Span span in markups) { + int length = display_name.char_count() - 4 + 1; + span.start_char += length; + span.end_char += length; + } + + bold_attr.end_index = display_name.length; + italic_attr.end_index = display_name.length; + attrs.insert(bold_attr.copy()); + attrs.insert(italic_attr.copy()); + } + + foreach (var markup in markups) { + foreach (var ty in markup.types) { + Attribute attr = null; + switch (ty) { + case Xep.MessageMarkup.SpanType.EMPHASIS: + attr = Pango.attr_style_new(Pango.Style.ITALIC); + break; + case Xep.MessageMarkup.SpanType.STRONG_EMPHASIS: + attr = Pango.attr_weight_new(Pango.Weight.BOLD); + break; + case Xep.MessageMarkup.SpanType.DELETED: + attr = Pango.attr_strikethrough_new(true); + break; + } + attr.start_index = markup_text.index_of_nth_char(markup.start_char); + attr.end_index = markup_text.index_of_nth_char(markup.end_char); + attrs.insert(attr.copy()); + } } if (conversation.type_ == Conversation.Type.GROUPCHAT) { @@ -93,11 +146,6 @@ public class MessageMetaItem : ContentMetaItem { markup_text = Util.parse_add_markup_theme(markup_text, null, true, true, true, Util.is_dark_theme(this.label), ref theme_dependent); } - if (message.body.has_prefix("/me ")) { - string display_name = Util.get_participant_display_name(stream_interactor, conversation, message.from); - markup_text = @"$(Markup.escape_text(display_name)) " + markup_text + ""; - } - int only_emoji_count = Util.get_only_emoji_count(markup_text); if (only_emoji_count != -1) { string size_str = only_emoji_count < 5 ? "xx-large" : "large"; @@ -121,8 +169,10 @@ public class MessageMetaItem : ContentMetaItem { additional_info = AdditionalInfo.PENDING; } else { int time_diff = (- (int) message.time.difference(new DateTime.now_utc()) / 1000); - Timeout.add(10000 - time_diff, () => { + if (pending_timeout_id != -1) Source.remove(pending_timeout_id); + pending_timeout_id = Timeout.add(10000 - time_diff, () => { update_label(); + pending_timeout_id = -1; return false; }); } @@ -136,16 +186,14 @@ public class MessageMetaItem : ContentMetaItem { if (theme_dependent && realize_id == -1) { realize_id = label.realize.connect(update_label); -// style_updated_id = label.style_updated.connect(update_label); } else if (!theme_dependent && realize_id != -1) { label.disconnect(realize_id); - label.disconnect(style_updated_id); } - return markup_text; + label.label = markup_text; } public void update_label() { - label.label = generate_markup_text(content_item); + generate_markup_text(content_item, label); } public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) { @@ -209,16 +257,15 @@ public class MessageMetaItem : ContentMetaItem { 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(); - } + string text = edit_mode.chat_text_view.text_view.buffer.text; + var markups = edit_mode.chat_text_view.get_markups(); + Dino.send_message(message_item.conversation, text, message_item.message.quoted_item_id, message_item.message, markups); + in_edit_mode = false; outer.set_widget(label, Plugins.WidgetType.GTK4, 2); }); - edit_mode.chat_text_view.text_view.buffer.text = message.body; + edit_mode.chat_text_view.set_text(message); outer.set_widget(edit_mode, Plugins.WidgetType.GTK4, 2); edit_mode.chat_text_view.text_view.grab_focus(); @@ -227,11 +274,6 @@ public class MessageMetaItem : ContentMetaItem { } } - private void on_edit_send(string text) { - stream_interactor.get_module(MessageCorrection.IDENTITY).send_correction(message_item.conversation, message_item.message, text); - this.in_edit_mode = false; - } - private void on_received_correction(ContentItem content_item) { if (this.content_item.id == content_item.id) { this.content_item = content_item; @@ -251,6 +293,15 @@ public class MessageMetaItem : ContentMetaItem { 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 (marked_notify_handler_id != -1) { + this.disconnect(marked_notify_handler_id); + } + if (realize_id != -1) { + label.disconnect(realize_id); + } + if (pending_timeout_id != -1) { + Source.remove(pending_timeout_id); + } if (label != null) { label.unparent(); label.dispose(); diff --git a/main/src/ui/util/helper.vala b/main/src/ui/util/helper.vala index 63288fc2..45b96b94 100644 --- a/main/src/ui/util/helper.vala +++ b/main/src/ui/util/helper.vala @@ -297,6 +297,31 @@ public static string parse_add_markup_theme(string s_, string? highlight_word, b return s; } + // Modifies `markups`. + public string remove_fallbacks_adjust_markups(string text, bool contains_quote, Gee.List fallbacks, Gee.List markups) { + string processed_text = text; + + foreach (var fallback in fallbacks) { + if (fallback.ns_uri == Xep.Replies.NS_URI && contains_quote) { + foreach (var fallback_location in fallback.locations) { + processed_text = processed_text[0:processed_text.index_of_nth_char(fallback_location.from_char)] + + processed_text[processed_text.index_of_nth_char(fallback_location.to_char):processed_text.length]; + + int length = fallback_location.to_char - fallback_location.from_char; + foreach (Xep.MessageMarkup.Span span in markups) { + if (span.start_char > fallback_location.to_char) { + span.start_char -= length; + } + if (span.end_char > fallback_location.to_char) { + span.end_char -= length; + } + } + } + } + } + return processed_text; + } + /** * This is a heuristic to count emojis in a string {@link http://example.com/} * diff --git a/xmpp-vala/CMakeLists.txt b/xmpp-vala/CMakeLists.txt index cfbc0aaf..fa0b08ef 100644 --- a/xmpp-vala/CMakeLists.txt +++ b/xmpp-vala/CMakeLists.txt @@ -55,6 +55,7 @@ SOURCES "src/module/xep/0048_bookmarks.vala" "src/module/xep/0048_conference.vala" + "src/module/xep/0394_message_markup.vala" "src/module/xep/0402_bookmarks2.vala" "src/module/xep/0004_data_forms.vala" diff --git a/xmpp-vala/meson.build b/xmpp-vala/meson.build index be5e96a8..7d062db3 100644 --- a/xmpp-vala/meson.build +++ b/xmpp-vala/meson.build @@ -55,6 +55,7 @@ sources = files( 'src/module/xep/0047_in_band_bytestreams.vala', 'src/module/xep/0048_bookmarks.vala', 'src/module/xep/0048_conference.vala', + 'src/module/xep/0394_message_markup.vala', 'src/module/xep/0049_private_xml_storage.vala', 'src/module/xep/0054_vcard/module.vala', 'src/module/xep/0059_result_set_management.vala', diff --git a/xmpp-vala/src/module/xep/0394_message_markup.vala b/xmpp-vala/src/module/xep/0394_message_markup.vala new file mode 100644 index 00000000..32b441af --- /dev/null +++ b/xmpp-vala/src/module/xep/0394_message_markup.vala @@ -0,0 +1,81 @@ +using Gee; + +namespace Xmpp.Xep.MessageMarkup { + + public const string NS_URI = "urn:xmpp:markup:0"; + + public enum SpanType { + EMPHASIS, + STRONG_EMPHASIS, + DELETED, + } + + public class Span : Object { + public Gee.List types { get; set; } + public int start_char { get; set; } + public int end_char { get; set; } + } + + public Gee.List get_spans(MessageStanza stanza) { + var ret = new ArrayList(); + + foreach (StanzaNode span_node in stanza.stanza.get_deep_subnodes(NS_URI + ":markup", NS_URI + ":span")) { + int start_char = span_node.get_attribute_int("start", -1, NS_URI); + int end_char = span_node.get_attribute_int("end", -1, NS_URI); + if (start_char == -1 || end_char == -1) continue; + + var types = new ArrayList(); + foreach (StanzaNode span_subnode in span_node.get_all_subnodes()) { + types.add(str_to_span_type(span_subnode.name)); + } + ret.add(new Span() { types=types, start_char=start_char, end_char=end_char }); + } + return ret; + } + + public void add_spans(MessageStanza stanza, Gee.List spans) { + if (spans.is_empty) return; + + StanzaNode markup_node = new StanzaNode.build("markup", NS_URI).add_self_xmlns(); + + foreach (var span in spans) { + StanzaNode span_node = new StanzaNode.build("span", NS_URI) + .put_attribute("start", span.start_char.to_string(), NS_URI) + .put_attribute("end", span.end_char.to_string(), NS_URI); + + foreach (var type in span.types) { + span_node.put_node(new StanzaNode.build(span_type_to_str(type), NS_URI)); + } + markup_node.put_node(span_node); + } + + stanza.stanza.put_node(markup_node); + } + + public static string span_type_to_str(Xep.MessageMarkup.SpanType span_type) { + switch (span_type) { + case Xep.MessageMarkup.SpanType.EMPHASIS: + return "emphasis"; + case Xep.MessageMarkup.SpanType.STRONG_EMPHASIS: + return "strong"; + case Xep.MessageMarkup.SpanType.DELETED: + return "deleted"; + default: + return ""; + } + } + + public static Xep.MessageMarkup.SpanType str_to_span_type(string span_str) { + switch (span_str) { + case "emphasis": + return Xep.MessageMarkup.SpanType.EMPHASIS; + case "strong": + return Xep.MessageMarkup.SpanType.STRONG_EMPHASIS; + case "deleted": + return Xep.MessageMarkup.SpanType.DELETED; + default: + return Xep.MessageMarkup.SpanType.EMPHASIS; + } + } + +} \ No newline at end of file -- cgit v1.2.3-70-g09d2