diff --git a/libdino/src/entity/account.vala b/libdino/src/entity/account.vala index 23544b46..f0628ca4 100644 --- a/libdino/src/entity/account.vala +++ b/libdino/src/entity/account.vala @@ -16,6 +16,7 @@ public class Account : Object { public string? alias { get; set; } public bool enabled { get; set; default = false; } public string? roster_version { get; set; } + public DateTime mam_earliest_synced { get; set; default=new DateTime.from_unix_utc(0); } private Database? db; @@ -36,6 +37,7 @@ public class Account : Object { alias = row[db.account.alias]; enabled = row[db.account.enabled]; roster_version = row[db.account.roster_version]; + mam_earliest_synced = new DateTime.from_unix_utc(row[db.account.mam_earliest_synced]); notify.connect(on_update); } @@ -49,6 +51,7 @@ public class Account : Object { .value(db.account.alias, alias) .value(db.account.enabled, enabled) .value(db.account.roster_version, roster_version) + .value(db.account.mam_earliest_synced, (long)mam_earliest_synced.to_unix()) .perform(); notify.connect(on_update); @@ -88,9 +91,11 @@ public class Account : Object { update.set(db.account.enabled, enabled); break; case "roster-version": update.set(db.account.roster_version, roster_version); break; + case "mam-earliest-synced": + update.set(db.account.mam_earliest_synced, (long)mam_earliest_synced.to_unix()); break; } update.perform(); } } -} \ No newline at end of file +} diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala index 178ca1ab..dbed6a00 100644 --- a/libdino/src/plugin/interfaces.vala +++ b/libdino/src/plugin/interfaces.vala @@ -50,7 +50,7 @@ public abstract class ContactDetailsProvider : Object { public class ContactDetails : Object { public signal void save(); - public signal void add(string category, string label, string desc, Widget widget); + public signal void add(string category, string label, string? desc, Widget widget); } public abstract class ConversationTitlebarEntry : Object { diff --git a/libdino/src/service/chat_interaction.vala b/libdino/src/service/chat_interaction.vala index d7b8f839..3e8f4b24 100644 --- a/libdino/src/service/chat_interaction.vala +++ b/libdino/src/service/chat_interaction.vala @@ -124,6 +124,8 @@ public class ChatInteraction : StreamInteractionModule, Object { } private void on_message_received(Entities.Message message, Conversation conversation) { + if (Xep.MessageArchiveManagement.MessageFlag.get_flag(message.stanza) != null) return; + send_delivery_receipt(conversation, message); if (is_active_focus(conversation)) { check_send_read(); @@ -160,4 +162,4 @@ public class ChatInteraction : StreamInteractionModule, Object { } } -} \ No newline at end of file +} diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index b414b943..5a7eb46d 100644 --- a/libdino/src/service/database.vala +++ b/libdino/src/service/database.vala @@ -6,7 +6,7 @@ using Dino.Entities; namespace Dino { public class Database : Qlite.Database { - private const int VERSION = 3; + private const int VERSION = 4; public class AccountTable : Table { public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; @@ -16,10 +16,11 @@ public class Database : Qlite.Database { public Column alias = new Column.Text("alias"); public Column enabled = new Column.BoolInt("enabled"); public Column roster_version = new Column.Text("roster_version") { min_version=2 }; + public Column mam_earliest_synced = new Column.Long("mam_earliest_synced") { min_version=4 }; internal AccountTable(Database db) { base(db, "account"); - init({id, bare_jid, resourcepart, password, alias, enabled, roster_version}); + init({id, bare_jid, resourcepart, password, alias, enabled, roster_version, mam_earliest_synced}); } } diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala index 390199d5..85b62a42 100644 --- a/libdino/src/service/message_processor.vala +++ b/libdino/src/service/message_processor.vala @@ -44,6 +44,9 @@ public class MessageProcessor : StreamInteractionModule, Object { stream_interactor.module_manager.get_module(account, Xmpp.Message.Module.IDENTITY).received_message.connect( (stream, message) => { on_message_received(account, message); }); + stream_interactor.module_manager.get_module(account, Xmpp.Xep.MessageArchiveManagement.Module.IDENTITY).feature_available.connect( (stream) => { + stream.get_module(Xep.MessageArchiveManagement.Module.IDENTITY).query_archive(stream, null, account.mam_earliest_synced.add_minutes(-1), null); + }); } private void send_unsent_messages(Account account) { @@ -80,9 +83,15 @@ public class MessageProcessor : StreamInteractionModule, Object { new_message.counterpart = new_message.direction == Entities.Message.DIRECTION_SENT ? new Jid(message.to) : new Jid(message.from); new_message.ourpart = new_message.direction == Entities.Message.DIRECTION_SENT ? new Jid(message.from) : new Jid(message.to); new_message.stanza = message; - Xep.DelayedDelivery.MessageFlag? deleyed_delivery_flag = Xep.DelayedDelivery.MessageFlag.get_flag(message); - new_message.time = deleyed_delivery_flag != null ? deleyed_delivery_flag.datetime : new DateTime.now_local(); - new_message.local_time = new DateTime.now_local(); + + Xep.MessageArchiveManagement.MessageFlag? mam_message_flag = Xep.MessageArchiveManagement.MessageFlag.get_flag(message); + if (mam_message_flag != null) new_message.local_time = mam_message_flag.server_time; + if (new_message.local_time == null || new_message.local_time.compare(new DateTime.now_local()) > 0) new_message.local_time = new DateTime.now_local(); + + Xep.DelayedDelivery.MessageFlag? delayed_message_flag = Xep.DelayedDelivery.MessageFlag.get_flag(message); + if (delayed_message_flag != null) new_message.time = delayed_message_flag.datetime; + if (new_message.time == null || new_message.time.compare(new_message.local_time) > 0) new_message.time = new_message.local_time; + return new_message; } @@ -100,6 +109,12 @@ public class MessageProcessor : StreamInteractionModule, Object { } else { message_received(new_message, conversation); } + + Core.XmppStream? stream = stream_interactor.get_stream(conversation.account); + Xep.MessageArchiveManagement.Flag? mam_flag = stream != null ? stream.get_flag(Xep.MessageArchiveManagement.Flag.IDENTITY) : null; + if (Xep.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null || (mam_flag != null && mam_flag.cought_up == true)) { + conversation.account.mam_earliest_synced = new_message.local_time; + } } } } diff --git a/libdino/src/service/module_manager.vala b/libdino/src/service/module_manager.vala index 437ecaf0..c2acf9ed 100644 --- a/libdino/src/service/module_manager.vala +++ b/libdino/src/service/module_manager.vala @@ -64,6 +64,7 @@ public class ModuleManager { module_map[account].add(new Xep.Bookmarks.Module()); module_map[account].add(new Presence.Module()); module_map[account].add(new Xmpp.Message.Module()); + module_map[account].add(new Xep.MessageArchiveManagement.Module()); module_map[account].add(new Xep.MessageCarbons.Module()); module_map[account].add(new Xep.Muc.Module()); module_map[account].add(new Xep.Pubsub.Module()); diff --git a/main/src/ui/contact_details/muc_config_form_provider.vala b/main/src/ui/contact_details/muc_config_form_provider.vala index 41df4465..438f41c4 100644 --- a/main/src/ui/contact_details/muc_config_form_provider.vala +++ b/main/src/ui/contact_details/muc_config_form_provider.vala @@ -34,43 +34,46 @@ public class MucConfigFormProvider : Plugins.ContactDetailsProvider { public static void add_field(DataForms.DataForm.Field field, Plugins.ContactDetails contact_details) { string label = field.label ?? ""; string? desc = null; - switch (field.var) { - case "muc#roomconfig_roomname": - label = _("Name of the room"); - break; - case "muc#roomconfig_roomdesc": - label = _("Description of the room"); - break; - case "muc#roomconfig_persistentroom": - label = _("Persistent"); - desc = _("The room will persist after the last occupant exits"); - break; - case "muc#roomconfig_publicroom": - label = _("Publicly searchable"); - break; - case "muc#roomconfig_changesubject": - label = _("Occupants may change subject"); - break; - case "muc#roomconfig_whois": - label = _("Discover real JIDs"); - desc = _("Who may discover real JIDs?"); - break; - case "muc#roomconfig_roomsecret": - label = _("Password"); - desc = _("Password required to enter the room. Leave empty for none"); - break; - case "muc#roomconfig_moderatedroom": - label = _("Moderated"); - desc = _("Only occupants with voice may send messages"); - break; - case "muc#roomconfig_membersonly": - label = _("Members only"); - desc = _("Only members may enter the room"); - break; - case "muc#roomconfig_historylength": - label = _("Message history"); - desc = _("Maximum number of history messages returned by the room"); - break; + + if (field.var != null) { + switch (field.var) { + case "muc#roomconfig_roomname": + label = _("Name of the room"); + break; + case "muc#roomconfig_roomdesc": + label = _("Description of the room"); + break; + case "muc#roomconfig_persistentroom": + label = _("Persistent"); + desc = _("The room will persist after the last occupant exits"); + break; + case "muc#roomconfig_publicroom": + label = _("Publicly searchable"); + break; + case "muc#roomconfig_changesubject": + label = _("Occupants may change subject"); + break; + case "muc#roomconfig_whois": + label = _("Discover real JIDs"); + desc = _("Who may discover real JIDs?"); + break; + case "muc#roomconfig_roomsecret": + label = _("Password"); + desc = _("Password required to enter the room. Leave empty for none"); + break; + case "muc#roomconfig_moderatedroom": + label = _("Moderated"); + desc = _("Only occupants with voice may send messages"); + break; + case "muc#roomconfig_membersonly": + label = _("Members only"); + desc = _("Only members may enter the room"); + break; + case "muc#roomconfig_historylength": + label = _("Message history"); + desc = _("Maximum number of history messages returned by the room"); + break; + } } Widget? widget = get_widget(field); @@ -78,6 +81,7 @@ public class MucConfigFormProvider : Plugins.ContactDetailsProvider { } private static Widget? get_widget(DataForms.DataForm.Field field) { + if (field.type_ == null) return null; switch (field.type_) { case DataForms.DataForm.Type.BOOLEAN: DataForms.DataForm.BooleanField boolean_field = field as DataForms.DataForm.BooleanField; diff --git a/main/src/ui/notifications.vala b/main/src/ui/notifications.vala index 725a3e26..02f0b1de 100644 --- a/main/src/ui/notifications.vala +++ b/main/src/ui/notifications.vala @@ -35,7 +35,7 @@ public class Notifications : Object { if (!notifications.has_key(conversation)) { notifications[conversation] = new Notify.Notification("", null, null); - notifications[conversation].set_hint("transient", true); + notifications[conversation].set_hint("persistent", true); notifications[conversation].add_action("default", "Open", () => { conversation_selected(conversation); #if GDK3_WITH_X11 @@ -69,6 +69,7 @@ public class Notifications : Object { private void on_received_subscription_request(Jid jid, Account account) { Notify.Notification notification = new Notify.Notification(_("Subscription request"), jid.bare_jid.to_string(), null); notification.set_image_from_pixbuf((new AvatarGenerator(40, 40)).draw_jid(stream_interactor, jid, account)); + notification.set_hint("persistent", true); notification.add_action("accept", _("Accept"), () => { stream_interactor.get_module(PresenceManager.IDENTITY).approve_subscription(account, jid); @@ -102,4 +103,4 @@ public class Notifications : Object { } } -} \ No newline at end of file +} diff --git a/main/src/ui/occupant_menu/list.vala b/main/src/ui/occupant_menu/list.vala index 42ff3da6..aeea7c26 100644 --- a/main/src/ui/occupant_menu/list.vala +++ b/main/src/ui/occupant_menu/list.vala @@ -84,10 +84,12 @@ public class List : Box { private void header(ListBoxRow row, ListBoxRow? before_row) { ListRow c1 = row as ListRow; - Xmpp.Xep.Muc.Affiliation a1 = stream_interactor.get_module(MucManager.IDENTITY).get_affiliation(conversation.counterpart, c1.jid, c1.account); + Xmpp.Xep.Muc.Affiliation? a1 = stream_interactor.get_module(MucManager.IDENTITY).get_affiliation(conversation.counterpart, c1.jid, c1.account); + if (a1 == null) return; + if (before_row != null) { - ListRow c2 = before_row as ListRow; - Xmpp.Xep.Muc.Affiliation a2 = stream_interactor.get_module(MucManager.IDENTITY).get_affiliation(conversation.counterpart, c2.jid, c2.account); + ListRow c2 = (ListRow) before_row; + Xmpp.Xep.Muc.Affiliation? a2 = stream_interactor.get_module(MucManager.IDENTITY).get_affiliation(conversation.counterpart, c2.jid, c2.account); if (a1 != a2) { row.set_header(generate_header_widget(a1, false)); } else if (row.get_header() != null){ diff --git a/xmpp-vala/CMakeLists.txt b/xmpp-vala/CMakeLists.txt index 4c4f34d2..3306cf7e 100644 --- a/xmpp-vala/CMakeLists.txt +++ b/xmpp-vala/CMakeLists.txt @@ -60,6 +60,7 @@ SOURCES "src/module/xep/0184_message_delivery_receipts.vala" "src/module/xep/0203_delayed_delivery.vala" "src/module/xep/0280_message_carbons.vala" + "src/module/xep/0313_message_archive_management.vala" "src/module/xep/0333_chat_markers.vala" "src/module/xep/0368_srv_records_tls.vala" "src/module/xep/pixbuf_storage.vala" diff --git a/xmpp-vala/src/module/xep/0004_data_forms.vala b/xmpp-vala/src/module/xep/0004_data_forms.vala index a0e8cd43..57ff834a 100644 --- a/xmpp-vala/src/module/xep/0004_data_forms.vala +++ b/xmpp-vala/src/module/xep/0004_data_forms.vala @@ -21,8 +21,12 @@ public class DataForm { } public void submit() { + on_result(stream, get_submit_node()); + } + + public StanzaNode get_submit_node() { stanza_node.set_attribute("type", "submit"); - on_result(stream, stanza_node); + return stanza_node; } public enum Type { @@ -46,19 +50,24 @@ public class DataForm { } } - public abstract class Field { - public string label { + public class Field { + public StanzaNode node { get; set; } + public string? label { get { return node.get_attribute("label", NS_URI); } set { node.set_attribute("label", value); } + default = null; } - public StanzaNode node { get; set; } - public abstract Type type_ { get; internal set; } - public string var { + public virtual Type? type_ { get; internal set; default=null; } + public string? var { get { return node.get_attribute("var", NS_URI); } set { node.set_attribute("var", value); } } - public Field(StanzaNode node) { + public Field() { + this.node = new StanzaNode.build("field", NS_URI); + } + + public Field.from_node(StanzaNode node) { this.node = node; } @@ -103,73 +112,94 @@ public class DataForm { } public class BooleanField : Field { - public override Type type_ { get; internal set; default=Type.BOOLEAN; } public bool value { get { return get_value_string() == "1"; } set { set_value_string(value ? "1" : "0"); } } - public BooleanField(StanzaNode node) { base(node); } + public BooleanField(StanzaNode node) { + base.from_node(node); + type_ = Type.BOOLEAN; + } } public class FixedField : Field { - public override Type type_ { get; internal set; default=Type.FIXED; } public string value { owned get { return get_value_string(); } set { set_value_string(value); } } - public FixedField(StanzaNode node) { base(node); } + public FixedField(StanzaNode node) { + base.from_node(node); + type_ = Type.FIXED; + } } public class HiddenField : Field { - public override Type type_ { get; internal set; default=Type.HIDDEN; } - public HiddenField(StanzaNode node) { base(node); } + public HiddenField() { + base(); + type_ = Type.HIDDEN;; + node.put_attribute("type", "hidden"); + } + public HiddenField.from_node(StanzaNode node) { + base.from_node(node); + type_ = Type.HIDDEN;; + } } public class JidMultiField : Field { public Gee.List