diff --git a/configure b/configure index b404fb179..ab22d8404 100755 --- a/configure +++ b/configure @@ -106,7 +106,7 @@ while true; do --enable-plugin ) if [ -z "$ENABLED_PLUGINS" ]; then ENABLED_PLUGINS="$2"; else ENABLED_PLUGINS="$ENABLED_PLUGINS;$2"; fi; shift; shift ;; --disable-plugin ) if [ -z "$DISABLED_PLUGINS" ]; then DISABLED_PLUGINS="$2"; else DISABLED_PLUGINS="$DISABLED_PLUGINS;$2"; fi; shift; shift ;; --valac ) VALA_EXECUTABLE="$2"; shift; shift ;; - --valac-flags ) VALAC_FLAGS="$2"; shift; shift ;; + --valac-flags ) VALACFLAGS="$2"; shift; shift ;; --lib-suffix ) LIB_SUFFIX="$2"; shift; shift ;; --with-libsoup3 ) USE_SOUP3=yes; shift ;; --disable-fast-vapi ) DISABLE_FAST_VAPI=yes; shift ;; diff --git a/dino.doap b/dino.doap index 0de627cf2..2064db5bf 100644 --- a/dino.doap +++ b/dino.doap @@ -3,7 +3,7 @@ Dino dino - Modern XMPP Chat Client + Modern XMPP chat client 現代化的 XMPP 用戶端聊天軟件 现代 XMPP 聊天客户端 Modern XMPP Sohbet İstemcisi diff --git a/dino.doap.in b/dino.doap.in index 7a175e638..2c1254520 100644 --- a/dino.doap.in +++ b/dino.doap.in @@ -9,7 +9,7 @@ Dino dino - Modern XMPP Chat Client + Modern XMPP chat client Dino is a modern open-source chat client for the desktop. It focuses on providing a clean and reliable Jabber/XMPP experience while having your privacy in mind. It supports end-to-end encryption with OMEMO and OpenPGP and allows configuring privacy-related features such as read receipts and typing notifications. diff --git a/im.dino.Dino.json b/im.dino.Dino.json index c754c88e2..a2b857f08 100644 --- a/im.dino.Dino.json +++ b/im.dino.Dino.json @@ -1,7 +1,7 @@ { "id": "im.dino.Dino", "runtime": "org.gnome.Platform", - "runtime-version": "44", + "runtime-version": "46", "sdk": "org.gnome.Sdk", "command": "dino", "finish-args": [ @@ -73,4 +73,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/libdino/CMakeLists.txt b/libdino/CMakeLists.txt index d52f91840..e4d786c9e 100644 --- a/libdino/CMakeLists.txt +++ b/libdino/CMakeLists.txt @@ -70,6 +70,7 @@ SOURCES src/util/display_name.vala src/util/util.vala src/util/weak_map.vala + src/util/weak_timeout.vala CUSTOM_VAPIS "${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi" "${CMAKE_BINARY_DIR}/exports/qlite.vapi" diff --git a/libdino/meson.build b/libdino/meson.build index 356c15d39..17804d230 100644 --- a/libdino/meson.build +++ b/libdino/meson.build @@ -76,6 +76,7 @@ sources = files( 'src/util/display_name.vala', 'src/util/util.vala', 'src/util/weak_map.vala', + 'src/util/weak_timeout.vala', ) sources += [version_vala] c_args = [ diff --git a/libdino/src/application.vala b/libdino/src/application.vala index 727b6131a..0fcee7310 100644 --- a/libdino/src/application.vala +++ b/libdino/src/application.vala @@ -39,12 +39,12 @@ public interface Application : GLib.Application { PresenceManager.start(stream_interactor); CounterpartInteractionManager.start(stream_interactor); BlockingManager.start(stream_interactor); + Calls.start(stream_interactor, db); ConversationManager.start(stream_interactor, db); MucManager.start(stream_interactor); AvatarManager.start(stream_interactor, db); RosterManager.start(stream_interactor, db); FileManager.start(stream_interactor, db); - Calls.start(stream_interactor, db); CallStore.start(stream_interactor, db); ContentItemStore.start(stream_interactor, db); ChatInteraction.start(stream_interactor); diff --git a/libdino/src/entity/conversation.vala b/libdino/src/entity/conversation.vala index 353daeaec..4115ae838 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 0b09e9b99..be275efc3 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/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala index cfe4d0cb1..dd25c5f58 100644 --- a/libdino/src/plugin/interfaces.vala +++ b/libdino/src/plugin/interfaces.vala @@ -55,6 +55,13 @@ public abstract class AccountSettingsEntry : Object { public abstract Object? get_widget(WidgetType type); } +public abstract class EncryptionPreferencesEntry : Object { + public abstract string id { get; } + public virtual Priority priority { get { return Priority.DEFAULT; } } + + public abstract Object? get_widget(Account account, WidgetType type); +} + public interface ContactDetailsProvider : Object { public abstract string id { get; } diff --git a/libdino/src/plugin/registry.vala b/libdino/src/plugin/registry.vala index 6c0234cad..7180aa149 100644 --- a/libdino/src/plugin/registry.vala +++ b/libdino/src/plugin/registry.vala @@ -6,6 +6,7 @@ public class Registry { public HashMap encryption_list_entries = new HashMap(); public HashMap call_encryption_entries = new HashMap(); public ArrayList account_settings_entries = new ArrayList(); + public ArrayList encryption_preferences_entries = new ArrayList(); public ArrayList contact_details_entries = new ArrayList(); public Map text_commands = new HashMap(); public Gee.List conversation_addition_populators = new ArrayList(); @@ -43,6 +44,18 @@ public class Registry { } } + public bool register_encryption_preferences_entry(EncryptionPreferencesEntry entry) { + lock(encryption_preferences_entries) { + foreach(var e in encryption_preferences_entries) { + if (e.id == entry.id) return false; + } + encryption_preferences_entries.add(entry); + // TODO: Order by priority +// encryption_preferences_entries.sort((a,b) => b.name.collate(a.name)); + return true; + } + } + public bool register_contact_details_entry(ContactDetailsProvider entry) { lock(contact_details_entries) { foreach(ContactDetailsProvider e in contact_details_entries) { diff --git a/libdino/src/service/avatar_manager.vala b/libdino/src/service/avatar_manager.vala index 1296856bd..f99f37d47 100644 --- a/libdino/src/service/avatar_manager.vala +++ b/libdino/src/service/avatar_manager.vala @@ -52,7 +52,7 @@ public class AvatarManager : StreamInteractionModule, Object { if (hash == null) return null; File file = File.new_for_path(Path.build_filename(folder, hash)); if (!file.query_exists()) { - fetch_and_store_for_jid(account, jid_); + fetch_and_store_for_jid.begin(account, jid_); return null; } else { return file; @@ -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(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(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}) diff --git a/libdino/src/service/call_peer_state.vala b/libdino/src/service/call_peer_state.vala index c7fa04da3..3a005e01d 100644 --- a/libdino/src/service/call_peer_state.vala +++ b/libdino/src/service/call_peer_state.vala @@ -9,7 +9,7 @@ public class Dino.PeerState : Object { public signal void connection_ready(); public signal void session_terminated(bool we_terminated, string? reason_name, string? reason_text); - public signal void encryption_updated(Xep.Jingle.ContentEncryption? audio_encryption, Xep.Jingle.ContentEncryption? video_encryption, bool same); + public signal void encryption_updated(Xep.Jingle.ContentEncryption? audio_encryption, Xep.Jingle.ContentEncryption? video_encryption); public StreamInteractor stream_interactor; public CallState call_state; @@ -412,7 +412,7 @@ public class Dino.PeerState : Object { if ((audio_encryptions != null && audio_encryptions.is_empty) || (video_encryptions != null && video_encryptions.is_empty)) { call.encryption = Encryption.NONE; - encryption_updated(null, null, true); + encryption_updated(null, null); return; } @@ -462,7 +462,7 @@ public class Dino.PeerState : Object { encryption_keys_same = true; } - encryption_updated(audio_encryption, video_encryption, encryption_keys_same); + encryption_updated(audio_encryption, video_encryption); } } diff --git a/libdino/src/service/calls.vala b/libdino/src/service/calls.vala index ebaf8d039..eca7e2231 100644 --- a/libdino/src/service/calls.vala +++ b/libdino/src/service/calls.vala @@ -61,8 +61,6 @@ namespace Dino { call_state.initiate_groupchat_call.begin(conversation.counterpart); } - conversation.last_active = call.time; - call_outgoing(call, call_state, conversation); return call_state; @@ -221,7 +219,6 @@ namespace Dino { Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(call.counterpart.bare_jid, account, Conversation.Type.CHAT); stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation); - conversation.last_active = call.time; var call_state = new CallState(call, stream_interactor); connect_call_state_signals(call_state); @@ -294,7 +291,6 @@ namespace Dino { Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(inviter_jid.bare_jid, account); if (conversation == null) return null; stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation); - conversation.last_active = call.time; CallState call_state = new CallState(call, stream_interactor); connect_call_state_signals(call_state); @@ -465,7 +461,6 @@ namespace Dino { Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(from_jid, to_jid, account, message_stanza.type_); if (conversation == null) return; - conversation.last_active = call_state.call.time; if (call_state.call.direction == Call.DIRECTION_INCOMING) { call_incoming(call_state.call, call_state, conversation, video_requested, multiparty); diff --git a/libdino/src/service/conversation_manager.vala b/libdino/src/service/conversation_manager.vala index 59ccbac4d..a757e8af5 100644 --- a/libdino/src/service/conversation_manager.vala +++ b/libdino/src/service/conversation_manager.vala @@ -29,6 +29,8 @@ public class ConversationManager : StreamInteractionModule, Object { stream_interactor.account_removed.connect(on_account_removed); stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new MessageListener(stream_interactor)); stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(handle_sent_message); + stream_interactor.get_module(Calls.IDENTITY).call_incoming.connect(handle_new_call); + stream_interactor.get_module(Calls.IDENTITY).call_outgoing.connect(handle_new_call); } public Conversation create_conversation(Jid jid, Account account, Conversation.Type? type = null) { @@ -46,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; @@ -194,6 +204,11 @@ public class ConversationManager : StreamInteractionModule, Object { } } + private void handle_new_call(Call call, CallState state, Conversation conversation) { + conversation.last_active = call.time; + start_conversation(conversation); + } + private void add_conversation(Conversation conversation) { if (!conversations[conversation.account].has_key(conversation.counterpart)) { conversations[conversation.account][conversation.counterpart] = new ArrayList(Conversation.equals_func); diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index dc1d68f33..eba8b7ca7 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/libdino/src/service/entity_info.vala b/libdino/src/service/entity_info.vala index d1217e819..83e27d4b6 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/history_sync.vala b/libdino/src/service/history_sync.vala index 0c0571bbd..8ab6d7bb7 100644 --- a/libdino/src/service/history_sync.vala +++ b/libdino/src/service/history_sync.vala @@ -388,9 +388,6 @@ public class Dino.HistorySync { page_result = PageResult.NoMoreMessages; } - string selection = null; - string[] selection_args = {}; - string query_id = query_params.query_id; string? after_id = query_params.start_id; diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala index 01687083e..baab37ceb 100644 --- a/libdino/src/service/message_processor.vala +++ b/libdino/src/service/message_processor.vala @@ -167,7 +167,6 @@ public class MessageProcessor : StreamInteractionModule, Object { new_message.counterpart = counterpart_override ?? (new_message.direction == Entities.Message.DIRECTION_SENT ? message.to : message.from); new_message.ourpart = new_message.direction == Entities.Message.DIRECTION_SENT ? message.from : message.to; - XmppStream? stream = stream_interactor.get_stream(account); Xmpp.MessageArchiveManagement.MessageFlag? mam_message_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message); EntityInfo entity_info = stream_interactor.get_module(EntityInfo.IDENTITY); if (mam_message_flag != null && mam_message_flag.mam_id != null) { diff --git a/libdino/src/service/muc_manager.vala b/libdino/src/service/muc_manager.vala index 119079f0e..6b52fe362 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/libdino/src/util/weak_timeout.vala b/libdino/src/util/weak_timeout.vala new file mode 100644 index 000000000..28894ed37 --- /dev/null +++ b/libdino/src/util/weak_timeout.vala @@ -0,0 +1,40 @@ +public class Dino.WeakTimeout { + // XXX: If you get an error saying your function doesn't match the delegate, make sure it's static! + // These are marked as "has_target=false" so you can't close over "this" and leak it in your lambda. + [CCode (has_target = false, instance_pos = 0)] + public delegate bool SourceFunc (T object); + + [CCode (has_target = false, instance_pos = 0)] + public delegate void SourceOnceFunc (T object); + + public static uint add(uint interval, T object, owned SourceFunc function, int priority = GLib.Priority.DEFAULT) { + var weak = WeakRef((Object)object); + return GLib.Timeout.add(interval, () => { + var strong = weak.get(); + if (strong == null) return false; + + return function(strong); + }, priority); + } + + public static uint add_once(uint interval, T object, owned SourceOnceFunc function, int priority = GLib.Priority.DEFAULT) { + var weak = WeakRef((Object)object); + return GLib.Timeout.add(interval, () => { + var strong = weak.get(); + if (strong == null) return false; + + function(strong); + return false; + }, priority); + } + + public static uint add_seconds(uint interval, T object, owned SourceFunc function, int priority = GLib.Priority.DEFAULT) { + return add(interval * 1000, object, (owned) function, priority); + } + + // This one doesn't have an upstream equivalent, but it seems pretty obvious to me + public static uint add_seconds_once(uint interval, T object, owned SourceOnceFunc function, int priority = GLib.Priority.DEFAULT) { + return add_once(interval * 1000, object, (owned) function, priority); + } + +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 437a84b91..2c5339072 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -39,6 +39,9 @@ set(RESOURCE_LIST icons/scalable/mimetypes/dino-file-table-symbolic.svg icons/scalable/mimetypes/dino-file-video-symbolic.svg + icons/scalable/status/dino-bell-large-none-symbolic.svg + icons/scalable/status/dino-bell-large-symbolic.svg + icons/scalable/status/dino-block-symbolic.svg icons/scalable/status/dino-double-tick-symbolic.svg icons/scalable/status/dino-party-popper-symbolic.svg icons/scalable/status/dino-security-high-symbolic.svg @@ -46,6 +49,7 @@ set(RESOURCE_LIST icons/scalable/status/dino-status-chat.svg icons/scalable/status/dino-status-dnd.svg icons/scalable/status/dino-status-online.svg + icons/scalable/status/dino-status-offline.svg icons/scalable/status/dino-tick-symbolic.svg icons/scalable/status/dino-video-off-symbolic.svg icons/scalable/status/dino-video-symbolic.svg @@ -56,6 +60,7 @@ set(RESOURCE_LIST add_conversation/list_row.ui add_conversation/select_jid_fragment.ui + account_picker_row.ui call_widget.ui chat_input.ui conversation_details.ui @@ -83,9 +88,11 @@ set(RESOURCE_LIST message_item_widget_edit_mode.ui occupant_list.ui occupant_list_item.ui + preferences_window.ui + preferences_window_account.ui + preferences_window_general.ui quote.ui search_autocomplete.ui - settings_dialog.ui unified_main_content.ui unified_window_placeholder.ui @@ -152,7 +159,6 @@ SOURCES src/ui/global_search.vala src/ui/notifier_freedesktop.vala src/ui/notifier_gnotifications.vala - src/ui/settings_dialog.vala src/ui/main_window.vala src/ui/main_window_controller.vala @@ -189,6 +195,7 @@ SOURCES src/ui/conversation_content_view/quote_widget.vala src/ui/conversation_content_view/reactions_widget.vala src/ui/conversation_content_view/subscription_notification.vala + src/ui/conversation_content_view/unread_indicator_populator.vala src/ui/chat_input/chat_input_controller.vala src/ui/chat_input/chat_text_view.vala @@ -232,10 +239,19 @@ SOURCES src/ui/widgets/fixed_ratio_picture.vala src/ui/widgets/natural_size_increase.vala + src/view_model/account_details.vala src/view_model/conversation_details.vala src/view_model/preferences_row.vala + src/view_model/preferences_window.vala src/windows/conversation_details.vala + + src/windows/preferences_window/account_preferences_subpage.vala + src/windows/preferences_window/accounts_preferences_page.vala + src/windows/preferences_window/encryption_preferences_page.vala + src/windows/preferences_window/general_preferences_page.vala + src/windows/preferences_window/preferences_window.vala + CUSTOM_VAPIS ${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi ${CMAKE_BINARY_DIR}/exports/qlite.vapi diff --git a/main/data/account_picker_row.ui b/main/data/account_picker_row.ui new file mode 100644 index 000000000..a67f7b3be --- /dev/null +++ b/main/data/account_picker_row.ui @@ -0,0 +1,32 @@ + + + + \ No newline at end of file diff --git a/main/data/add_conversation/list_row.ui b/main/data/add_conversation/list_row.ui index b8a971743..e7dc62ebe 100644 --- a/main/data/add_conversation/list_row.ui +++ b/main/data/add_conversation/list_row.ui @@ -1,12 +1,13 @@ - - 3 - 3 - 3 - 3 - 10 + + horizontal + 8 + 6 + 6 + 6 + 6 30 @@ -15,34 +16,33 @@ - - center + vertical + center - - 1 - end - 1 - 0 - - 0 - 0 - + + horizontal + 6 + + + end + 0 + + + + + 8 + + - 1 end - 1 0 - - 0 - 1 - diff --git a/main/data/add_conversation/select_jid_fragment.ui b/main/data/add_conversation/select_jid_fragment.ui index 787add9f7..9687fd103 100644 --- a/main/data/add_conversation/select_jid_fragment.ui +++ b/main/data/add_conversation/select_jid_fragment.ui @@ -2,68 +2,72 @@