From fe45ab575c687febc1f342b0882a7597bd6ae9dc Mon Sep 17 00:00:00 2001 From: fiaxh Date: Sun, 26 May 2024 17:21:04 +0200 Subject: Support avatar deletion --- plugins/omemo/src/protocol/stream_module.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'plugins/omemo/src') diff --git a/plugins/omemo/src/protocol/stream_module.vala b/plugins/omemo/src/protocol/stream_module.vala index b00ea5b8..4e97b1e6 100644 --- a/plugins/omemo/src/protocol/stream_module.vala +++ b/plugins/omemo/src/protocol/stream_module.vala @@ -30,8 +30,8 @@ public class StreamModule : XmppStreamModule { } public override void attach(XmppStream stream) { - stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NODE_DEVICELIST, true, - (stream, jid, id, node) => parse_device_list(stream, jid, id, node), null); + stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NODE_DEVICELIST, + (stream, jid, id, node) => parse_device_list(stream, jid, id, node), null, null); } public override void detach(XmppStream stream) { -- 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 'plugins/omemo/src') 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 21ae42762d8a57da5cb1ec40b46e7510fc3121ad Mon Sep 17 00:00:00 2001 From: fiaxh Date: Thu, 20 Jun 2024 12:05:20 +0200 Subject: Redesign and rewrite accounts and settings dialog into a combined one --- libdino/src/plugin/interfaces.vala | 7 + libdino/src/plugin/registry.vala | 13 + main/CMakeLists.txt | 15 +- main/data/account_picker_row.ui | 32 ++ main/data/gresource.xml | 4 + main/data/menu_app.ui | 8 +- main/data/preferences_window.ui | 32 ++ main/data/preferences_window_account.ui | 137 +++++++++ main/data/preferences_window_general.ui | 68 +++++ main/data/settings_dialog.ui | 74 ----- main/data/style-dark.css | 2 +- main/data/style.css | 4 +- main/meson.build | 8 +- main/src/ui/application.vala | 33 +- main/src/ui/notifier_freedesktop.vala | 7 +- main/src/ui/notifier_gnotifications.vala | 1 + main/src/ui/settings_dialog.vala | 30 -- main/src/view_model/account_details.vala | 21 ++ main/src/view_model/preferences_window.vala | 109 +++++++ .../account_preferences_subpage.vala | 264 ++++++++++++++++ .../accounts_preferences_page.vala | 75 +++++ .../encryption_preferences_page.vala | 59 ++++ .../general_preferences_page.vala | 39 +++ .../preferences_window/preferences_window.vala | 31 ++ plugins/omemo/CMakeLists.txt | 14 +- plugins/omemo/data/encryption_preferences_entry.ui | 81 +++++ plugins/omemo/data/gresource.xml | 1 + plugins/omemo/meson.build | 3 +- plugins/omemo/src/plugin.vala | 4 +- plugins/omemo/src/ui/account_settings_entry.vala | 58 ---- .../omemo/src/ui/encryption_preferences_entry.vala | 336 +++++++++++++++++++++ plugins/omemo/src/ui/util.vala | 46 +-- plugins/openpgp/CMakeLists.txt | 26 +- plugins/openpgp/data/account_settings_item.ui | 31 -- plugins/openpgp/meson.build | 2 +- plugins/openpgp/src/account_settings_entry.vala | 163 ---------- .../openpgp/src/encryption_preferences_entry.vala | 86 ++++++ plugins/openpgp/src/plugin.vala | 4 +- 38 files changed, 1483 insertions(+), 445 deletions(-) create mode 100644 main/data/account_picker_row.ui create mode 100644 main/data/preferences_window.ui create mode 100644 main/data/preferences_window_account.ui create mode 100644 main/data/preferences_window_general.ui delete mode 100644 main/data/settings_dialog.ui delete mode 100644 main/src/ui/settings_dialog.vala create mode 100644 main/src/view_model/account_details.vala create mode 100644 main/src/view_model/preferences_window.vala create mode 100644 main/src/windows/preferences_window/account_preferences_subpage.vala create mode 100644 main/src/windows/preferences_window/accounts_preferences_page.vala create mode 100644 main/src/windows/preferences_window/encryption_preferences_page.vala create mode 100644 main/src/windows/preferences_window/general_preferences_page.vala create mode 100644 main/src/windows/preferences_window/preferences_window.vala create mode 100644 plugins/omemo/data/encryption_preferences_entry.ui delete mode 100644 plugins/omemo/src/ui/account_settings_entry.vala create mode 100644 plugins/omemo/src/ui/encryption_preferences_entry.vala delete mode 100644 plugins/openpgp/data/account_settings_item.ui delete mode 100644 plugins/openpgp/src/account_settings_entry.vala create mode 100644 plugins/openpgp/src/encryption_preferences_entry.vala (limited to 'plugins/omemo/src') diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala index cfe4d0cb..dd25c5f5 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 6c0234ca..7180aa14 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/main/CMakeLists.txt b/main/CMakeLists.txt index ea4de99b..d5fc66be 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -59,6 +59,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 @@ -86,9 +87,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 @@ -155,7 +158,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 @@ -236,10 +238,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 00000000..a67f7b3b --- /dev/null +++ b/main/data/account_picker_row.ui @@ -0,0 +1,32 @@ + + + + \ No newline at end of file diff --git a/main/data/gresource.xml b/main/data/gresource.xml index 282838e0..f436ce68 100644 --- a/main/data/gresource.xml +++ b/main/data/gresource.xml @@ -1,6 +1,7 @@ + account_picker_row.ui add_conversation/add_contact_dialog.ui add_conversation/add_groupchat_dialog.ui add_conversation/conference_details_fragment.ui @@ -66,6 +67,9 @@ occupant_list.ui occupant_list_item.ui quote.ui + preferences_window.ui + preferences_window_account.ui + preferences_window_general.ui search_autocomplete.ui settings_dialog.ui style-dark.css diff --git a/main/data/menu_app.ui b/main/data/menu_app.ui index bb33ff65..9b85634d 100644 --- a/main/data/menu_app.ui +++ b/main/data/menu_app.ui @@ -3,13 +3,7 @@
- app.accounts - Accounts - -
-
- - app.settings + app.preferences Preferences diff --git a/main/data/preferences_window.ui b/main/data/preferences_window.ui new file mode 100644 index 00000000..d262dd76 --- /dev/null +++ b/main/data/preferences_window.ui @@ -0,0 +1,32 @@ + + + + + + \ No newline at end of file diff --git a/main/data/preferences_window_account.ui b/main/data/preferences_window_account.ui new file mode 100644 index 00000000..4280422d --- /dev/null +++ b/main/data/preferences_window_account.ui @@ -0,0 +1,137 @@ + + + + + \ No newline at end of file diff --git a/main/data/preferences_window_general.ui b/main/data/preferences_window_general.ui new file mode 100644 index 00000000..33d1a2c9 --- /dev/null +++ b/main/data/preferences_window_general.ui @@ -0,0 +1,68 @@ + + + + + diff --git a/main/data/settings_dialog.ui b/main/data/settings_dialog.ui deleted file mode 100644 index a8b24135..00000000 --- a/main/data/settings_dialog.ui +++ /dev/null @@ -1,74 +0,0 @@ - - - - diff --git a/main/data/style-dark.css b/main/data/style-dark.css index 3bd0add0..791ae9c8 100644 --- a/main/data/style-dark.css +++ b/main/data/style-dark.css @@ -1,3 +1,3 @@ -.dino-main .overlay-toolbar { +.overlay-toolbar { background-color: shade(@view_bg_color, 1.5); } \ No newline at end of file diff --git a/main/data/style.css b/main/data/style.css index a7a1d8df..5a70ba83 100644 --- a/main/data/style.css +++ b/main/data/style.css @@ -127,13 +127,13 @@ picture.avatar { /* Overlay Toolbar */ -.dino-main .overlay-toolbar { +.overlay-toolbar { padding: 2px; border-radius: 6px; border-spacing: 0; } -.dino-main .overlay-toolbar > * { +.overlay-toolbar > * { margin-top: 0; margin-bottom: 0; } diff --git a/main/meson.build b/main/meson.build index 1b5abcfc..95deabfd 100644 --- a/main/meson.build +++ b/main/meson.build @@ -77,7 +77,6 @@ sources = files( 'src/ui/occupant_menu/list.vala', 'src/ui/occupant_menu/list_row.vala', 'src/ui/occupant_menu/view.vala', - 'src/ui/settings_dialog.vala', 'src/ui/util/accounts_combo_box.vala', 'src/ui/util/config.vala', 'src/ui/util/data_forms.vala', @@ -89,8 +88,15 @@ sources = files( 'src/ui/widgets/date_separator.vala', '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/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', 'src/windows/conversation_details.vala', ) sources += gnome.compile_resources( diff --git a/main/src/ui/application.vala b/main/src/ui/application.vala index 2e785224..d0fde297 100644 --- a/main/src/ui/application.vala +++ b/main/src/ui/application.vala @@ -113,13 +113,17 @@ public class Dino.Ui.Application : Adw.Application, Dino.Application { } private void create_actions() { - SimpleAction accounts_action = new SimpleAction("accounts", null); - accounts_action.activate.connect(show_accounts_window); - add_action(accounts_action); - - SimpleAction settings_action = new SimpleAction("settings", null); - settings_action.activate.connect(show_settings_window); - add_action(settings_action); + SimpleAction preferences_action = new SimpleAction("preferences", null); + preferences_action.activate.connect(show_preferences_window); + add_action(preferences_action); + + SimpleAction preferences_account_action = new SimpleAction("preferences-account", VariantType.INT32); + preferences_account_action.activate.connect((variant) => { + Account? account = db.get_account_by_id(variant.get_int32()); + if (account == null) return; + show_preferences_account_window(account); + }); + add_action(preferences_account_action); SimpleAction about_action = new SimpleAction("about", null); about_action.activate.connect(show_about_window); @@ -233,17 +237,16 @@ public class Dino.Ui.Application : Adw.Application, Dino.Application { return Environment.get_variable("GTK_CSD") != "0"; } - private void show_accounts_window() { - ManageAccounts.Dialog dialog = new ManageAccounts.Dialog(stream_interactor, db); - dialog.set_transient_for(get_active_window()); - dialog.account_enabled.connect(add_connection); - dialog.account_disabled.connect(remove_connection); + private void show_preferences_window() { + Ui.PreferencesWindow dialog = new Ui.PreferencesWindow() { transient_for = window }; + dialog.model.populate(db, stream_interactor); dialog.present(); } - private void show_settings_window() { - SettingsDialog dialog = new SettingsDialog(); - dialog.set_transient_for(get_active_window()); + private void show_preferences_account_window(Account account) { + Ui.PreferencesWindow dialog = new Ui.PreferencesWindow() { transient_for = window }; + dialog.model.populate(db, stream_interactor); + dialog.accounts_page.account_chosen(account); dialog.present(); } diff --git a/main/src/ui/notifier_freedesktop.vala b/main/src/ui/notifier_freedesktop.vala index a1df5990..0d263dba 100644 --- a/main/src/ui/notifier_freedesktop.vala +++ b/main/src/ui/notifier_freedesktop.vala @@ -201,8 +201,13 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { HashTable hash_table = new HashTable(null, null); hash_table["desktop-entry"] = new Variant.string(Dino.Application.get_default().get_application_id()); hash_table["category"] = new Variant.string("im.error"); + string[] actions = new string[] {"default", "Open preferences"}; try { - yield dbus_notifications.notify("Dino", 0, "im.dino.Dino", summary, body, new string[]{}, hash_table, -1); + uint32 notification_id = yield dbus_notifications.notify("Dino", 0, "im.dino.Dino", summary, body, actions, hash_table, -1); + + add_action_listener(notification_id, "default", () => { + GLib.Application.get_default().activate_action("preferences-account", new Variant.int32(account.id)); + }); } catch (Error e) { warning("Failed showing connection error notification: %s", e.message); } diff --git a/main/src/ui/notifier_gnotifications.vala b/main/src/ui/notifier_gnotifications.vala index 4d36620d..462cdf70 100644 --- a/main/src/ui/notifier_gnotifications.vala +++ b/main/src/ui/notifier_gnotifications.vala @@ -102,6 +102,7 @@ namespace Dino.Ui { public async void notify_connection_error(Account account, ConnectionManager.ConnectionError error) { Notification notification = new Notification(_("Could not connect to %s").printf(account.bare_jid.domainpart)); + notification.set_default_action_and_target_value("app.preferences-account", new Variant.int32(account.id)); switch (error.source) { case ConnectionManager.ConnectionError.Source.SASL: notification.set_body("Wrong password"); diff --git a/main/src/ui/settings_dialog.vala b/main/src/ui/settings_dialog.vala deleted file mode 100644 index 3635879c..00000000 --- a/main/src/ui/settings_dialog.vala +++ /dev/null @@ -1,30 +0,0 @@ -using Gtk; - -namespace Dino.Ui { - -[GtkTemplate (ui = "/im/dino/Dino/settings_dialog.ui")] -class SettingsDialog : Adw.PreferencesWindow { - - [GtkChild] private unowned Switch typing_switch; - [GtkChild] private unowned Switch marker_switch; - [GtkChild] private unowned Switch notification_switch; - [GtkChild] private unowned Switch emoji_switch; - - Dino.Entities.Settings settings = Dino.Application.get_default().settings; - - public SettingsDialog() { - Object(); - - typing_switch.active = settings.send_typing; - marker_switch.active = settings.send_marker; - notification_switch.active = settings.notifications; - emoji_switch.active = settings.convert_utf8_smileys; - - typing_switch.notify["active"].connect(() => { settings.send_typing = typing_switch.active; } ); - marker_switch.notify["active"].connect(() => { settings.send_marker = marker_switch.active; } ); - notification_switch.notify["active"].connect(() => { settings.notifications = notification_switch.active; } ); - emoji_switch.notify["active"].connect(() => { settings.convert_utf8_smileys = emoji_switch.active; }); - } -} - -} diff --git a/main/src/view_model/account_details.vala b/main/src/view_model/account_details.vala new file mode 100644 index 00000000..a9ddeca5 --- /dev/null +++ b/main/src/view_model/account_details.vala @@ -0,0 +1,21 @@ +using Dino; +using Dino.Entities; +using Xmpp; +using Xmpp.Xep; + +public class Dino.Ui.ViewModel.AccountDetails : Object { + public Entities.Account account { get; set; } + public string bare_jid { owned get { return account.bare_jid.to_string(); } } + public CompatAvatarPictureModel avatar_model { get; set; } + public ConnectionManager.ConnectionState connection_state { get; set; } + public ConnectionManager.ConnectionError? connection_error { get; set; } + + public AccountDetails(Account account, StreamInteractor stream_interactor) { + var account_conv = new Conversation(account.bare_jid, account, Conversation.Type.CHAT); + + this.account = account; + this.avatar_model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(account_conv); + this.connection_state = stream_interactor.connection_manager.get_state(account); + this.connection_error = stream_interactor.connection_manager.get_error(account); + } +} \ No newline at end of file diff --git a/main/src/view_model/preferences_window.vala b/main/src/view_model/preferences_window.vala new file mode 100644 index 00000000..9cc5a80e --- /dev/null +++ b/main/src/view_model/preferences_window.vala @@ -0,0 +1,109 @@ +using Dino.Entities; +using Xmpp; +using Xmpp.Xep; +using Gee; + +public class Dino.Ui.ViewModel.PreferencesWindow : Object { + public signal void update(); + + public HashMap account_details = new HashMap(Account.hash_func, Account.equals_func); + public AccountDetails selected_account { get; set; } + public Gtk.SingleSelection active_accounts_selection { get; default=new Gtk.SingleSelection(new GLib.ListStore(typeof(ViewModel.AccountDetails))); } + + public StreamInteractor stream_interactor; + public Database db; + + public GeneralPreferencesPage general_page { get; set; default=new GeneralPreferencesPage(); } + + public void populate(Database db, StreamInteractor stream_interactor) { + this.db = db; + this.stream_interactor = stream_interactor; + + stream_interactor.connection_manager.connection_error.connect((account, error) => { + var account_detail = account_details[account]; + if (account_details != null) { + account_detail.connection_error = error; + } + }); + stream_interactor.connection_manager.connection_state_changed.connect((account, state) => { + var account_detail = account_details[account]; + if (account_details != null) { + account_detail.connection_state = state; + account_detail.connection_error = stream_interactor.connection_manager.get_error(account); + } + }); + stream_interactor.account_added.connect(update_data); + stream_interactor.account_removed.connect(update_data); + + bind_general_page(); + update_data(); + } + + private void update_data() { + // account_details should hold the correct set of accounts (add or remove some if needed), but do not override remaining ones (would destroy bindings) + var current_accounts = db.get_accounts(); + var remove_accounts = new ArrayList(); + foreach (var account in account_details.keys) { + if (!current_accounts.contains(account)) remove_accounts.add(account); + } + foreach (var account in remove_accounts) { + account_details.unset(account); + } + foreach (var account in current_accounts) { + if (!account_details.has_key(account)) { + account_details[account] = new AccountDetails(account, stream_interactor); + } + if (selected_account == null && account.enabled) selected_account = account_details[account]; + } + + // Update account picker model with currently active accounts + var list_model = (GLib.ListStore) active_accounts_selection.model; + list_model.remove_all(); + foreach (var account in stream_interactor.get_accounts()) { + list_model.append(new ViewModel.AccountDetails(account, stream_interactor)); + } + + update(); + } + + public void set_avatar_uri(Account account, string uri) { + stream_interactor.get_module(AvatarManager.IDENTITY).publish(account, uri); + } + + public void remove_avatar(Account account) { + stream_interactor.get_module(AvatarManager.IDENTITY).unset_avatar(account); + } + + public void remove_account(Account account) { + stream_interactor.disconnect_account.begin(account, () => { + account.remove(); + update_data(); + }); + } + + public void reconnect_account(Account account) { + stream_interactor.disconnect_account.begin(account, () => { + stream_interactor.connect_account(account); + }); + } + + public void enable_disable_account(Account account) { + if (account.enabled) { + account.enabled = false; + stream_interactor.disconnect_account.begin(account); + } else { + account.enabled = true; + stream_interactor.connect_account(account); + } + update_data(); + } + + private void bind_general_page() { + var settings = Dino.Application.get_default().settings; + settings.bind_property("send-typing", general_page, "send-typing", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + settings.bind_property("send-marker", general_page, "send-marker", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + settings.bind_property("notifications", general_page, "notifications", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + settings.bind_property("convert-utf8-smileys", general_page, "convert-emojis", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + } +} + diff --git a/main/src/windows/preferences_window/account_preferences_subpage.vala b/main/src/windows/preferences_window/account_preferences_subpage.vala new file mode 100644 index 00000000..462fc8e1 --- /dev/null +++ b/main/src/windows/preferences_window/account_preferences_subpage.vala @@ -0,0 +1,264 @@ +using Dino.Entities; +using Xmpp; +using Xmpp.Xep; +using Gee; +using Gtk; +using Gdk; + +[GtkTemplate (ui = "/im/dino/Dino/preferences_window_account.ui")] +public class Dino.Ui.AccountPreferencesSubpage : Gtk.Box { + + [GtkChild] public unowned Button back_button; + [GtkChild] public unowned AvatarPicture avatar; + [GtkChild] public unowned Adw.ActionRow xmpp_address; + [GtkChild] public unowned Adw.ActionRow local_alias; // TODO replace with EntryRow once we require Adw 1.2 + [GtkChild] public unowned Entry local_alias_entry; + [GtkChild] public unowned Adw.ActionRow connection_status; + [GtkChild] public unowned Button enter_password_button; + [GtkChild] public unowned Box avatar_menu_box; + [GtkChild] public unowned Button edit_avatar_button; + [GtkChild] public unowned Button remove_avatar_button; + [GtkChild] public unowned Widget button_container; + [GtkChild] public unowned Button remove_account_button; + [GtkChild] public unowned Button disable_account_button; + + public Account account { get { return model.selected_account.account; } } + public ViewModel.PreferencesWindow model { get; set; } + + private Binding[] bindings = new Binding[0]; + private ulong[] account_notify_ids = new ulong[0]; + private ulong alias_entry_changed = 0; + + construct { + button_container.layout_manager = new NaturalDirectionBoxLayout((BoxLayout)button_container.layout_manager); + back_button.clicked.connect(() => { + var window = (Adw.PreferencesWindow) this.get_root(); + window.close_subpage(); + }); + edit_avatar_button.clicked.connect(() => { + show_select_avatar(); + }); + remove_avatar_button.clicked.connect(() => { + model.remove_avatar(account); + }); + disable_account_button.clicked.connect(() => { + model.enable_disable_account(account); + }); + remove_account_button.clicked.connect(() => { + show_remove_account_dialog(); + }); + enter_password_button.clicked.connect(() => { + + var password = new PasswordEntry() { show_peek_icon=true }; +#if Adw_1_2 + var dialog = new Adw.MessageDialog((Window)this.get_root(), "Enter password for %s".printf(account.bare_jid.to_string()), null); + dialog.response.connect((response) => { + if (response == "connect") { + account.password = password.text; + model.reconnect_account(account); + } + }); + dialog.set_default_response("connect"); + dialog.set_extra_child(password); + dialog.add_response("cancel", _("Cancel")); + dialog.add_response("connect", _("Connect")); +#else + Gtk.MessageDialog dialog = new Gtk.MessageDialog ( + (Window)this.get_root(), Gtk.DialogFlags.DESTROY_WITH_PARENT | Gtk.DialogFlags.MODAL, + Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, + "Enter password for %s", account.bare_jid.to_string()); + Button ok_button = dialog.get_widget_for_response(ResponseType.OK) as Button; + ok_button.label = _("Connect"); + + dialog.response.connect((response) => { + if (response == ResponseType.OK) { + account.password = password.text; + model.reconnect_account(account); + } + dialog.close(); + }); + dialog.get_content_area().append(password); +#endif + + dialog.present(); + }); + + this.notify["model"].connect(() => { + model.notify["selected-account"].connect(() => { + foreach (var binding in bindings) { + binding.unbind(); + } + + avatar.model = model.selected_account.avatar_model; + xmpp_address.subtitle = account.bare_jid.to_string(); + + if (alias_entry_changed != 0) local_alias_entry.disconnect(alias_entry_changed); + local_alias_entry.text = account.alias ?? ""; + alias_entry_changed = local_alias_entry.changed.connect(() => { + account.alias = local_alias_entry.text; + }); + + bindings += account.bind_property("enabled", disable_account_button, "label", BindingFlags.SYNC_CREATE, (binding, from, ref to) => { + bool enabled_bool = (bool) from; + to = enabled_bool ? _("Disable account") : _("Enable account"); + return true; + }); + bindings += account.bind_property("enabled", avatar_menu_box, "visible", BindingFlags.SYNC_CREATE); + bindings += account.bind_property("enabled", connection_status, "visible", BindingFlags.SYNC_CREATE); + bindings += model.selected_account.bind_property("connection-state", connection_status, "subtitle", BindingFlags.SYNC_CREATE, (binding, from, ref to) => { + to = get_status_label(); + return true; + }); + bindings += model.selected_account.bind_property("connection-error", connection_status, "subtitle", BindingFlags.SYNC_CREATE, (binding, from, ref to) => { + to = get_status_label(); + return true; + }); + bindings += model.selected_account.bind_property("connection-error", enter_password_button, "visible", BindingFlags.SYNC_CREATE, (binding, from, ref to) => { + var error = (ConnectionManager.ConnectionError) from; + to = error != null && error.source == ConnectionManager.ConnectionError.Source.SASL; + return true; + }); + + model.selected_account.notify["connection-error"].connect(() => { + // TODO doesn't work + if (model.selected_account.connection_error != null) { + connection_status.add_css_class("error"); + } else { + connection_status.remove_css_class("error"); + } + }); + if (model.selected_account.connection_error != null) { + connection_status.add_css_class("error"); + } else { + connection_status.remove_css_class("error"); + } + }); + }); + } + + private void show_select_avatar() { + FileChooserNative chooser = new FileChooserNative(_("Select avatar"), (Window)this.get_root(), FileChooserAction.OPEN, _("Select"), _("Cancel")); + FileFilter filter = new FileFilter(); + foreach (PixbufFormat pixbuf_format in Pixbuf.get_formats()) { + foreach (string mime_type in pixbuf_format.get_mime_types()) { + filter.add_mime_type(mime_type); + } + } + filter.set_filter_name(_("Images")); + chooser.add_filter(filter); + + filter = new FileFilter(); + filter.set_filter_name(_("All files")); + filter.add_pattern("*"); + chooser.add_filter(filter); + + chooser.response.connect(() => { + string uri = chooser.get_file().get_path(); + model.set_avatar_uri(account, uri); + }); + + chooser.show(); + } + + private void show_remove_account_dialog() { + Gtk.MessageDialog msg = new Gtk.MessageDialog ( + (Window)this.get_root(), Gtk.DialogFlags.DESTROY_WITH_PARENT | Gtk.DialogFlags.MODAL, + Gtk.MessageType.WARNING, Gtk.ButtonsType.OK_CANCEL, + _("Remove account %s?"), account.bare_jid.to_string()); + msg.secondary_text = "You won't be able to access your conversation history anymore."; // TODO remove history! + Button ok_button = msg.get_widget_for_response(ResponseType.OK) as Button; + ok_button.label = _("Remove"); + ok_button.add_css_class("destructive-action"); + msg.response.connect((response) => { + if (response == ResponseType.OK) { + model.remove_account(account); + // Close the account subpage + var window = (Adw.PreferencesWindow) this.get_root(); + window.close_subpage(); +// window.pop_subpage(); + } + msg.close(); + }); + msg.present(); + } + + private string get_status_label() { + string? error_label = get_connection_error_description(); + if (error_label != null) return error_label; + + ConnectionManager.ConnectionState state = model.selected_account.connection_state; + switch (state) { + case ConnectionManager.ConnectionState.CONNECTING: + return _("Connecting…"); + case ConnectionManager.ConnectionState.CONNECTED: + return _("Connected"); + case ConnectionManager.ConnectionState.DISCONNECTED: + return _("Disconnected"); + } + assert_not_reached(); + } + + private string? get_connection_error_description() { + ConnectionManager.ConnectionError? error = model.selected_account.connection_error; + if (error == null) return null; + + switch (error.source) { + case ConnectionManager.ConnectionError.Source.SASL: + return _("Wrong password"); + case ConnectionManager.ConnectionError.Source.TLS: + return _("Invalid TLS certificate"); + } + if (error.identifier != null) { + return _("Error") + ": " + error.identifier; + } else { + return _("Error"); + } + } +} + +public class Dino.Ui.NaturalDirectionBoxLayout : LayoutManager { + private BoxLayout original; + private BoxLayout alternative; + + public NaturalDirectionBoxLayout(BoxLayout original) { + this.original = original; + if (original.orientation == Orientation.HORIZONTAL) { + this.alternative = new BoxLayout(Orientation.VERTICAL); + this.alternative.spacing = this.original.spacing / 2; + } + } + + public override SizeRequestMode get_request_mode(Widget widget) { + return original.orientation == Orientation.HORIZONTAL ? SizeRequestMode.HEIGHT_FOR_WIDTH : SizeRequestMode.WIDTH_FOR_HEIGHT; + } + + public override void allocate(Widget widget, int width, int height, int baseline) { + int blind_minimum, blind_natural, blind_minimum_baseline, blind_natural_baseline; + original.measure(widget, original.orientation, -1, out blind_minimum, out blind_natural, out blind_minimum_baseline, out blind_natural_baseline); + int for_size = (original.orientation == Orientation.HORIZONTAL ? width : height); + if (for_size >= blind_minimum) { + original.allocate(widget, width, height, baseline); + } else { + alternative.allocate(widget, width, height, baseline); + } + } + + public override void measure(Widget widget, Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { + if (for_size == -1) { + original.measure(widget, orientation, -1, out minimum, out natural, out minimum_baseline, out natural_baseline); + int alt_minimum, alt_natural, alt_minimum_baseline, alt_natural_baseline; + alternative.measure(widget, orientation, -1, out alt_minimum, out alt_natural, out alt_minimum_baseline, out alt_natural_baseline); + if (alt_minimum < minimum && alt_minimum != -1) minimum = alt_minimum; + if (alt_minimum_baseline < minimum_baseline && alt_minimum_baseline != -1) minimum = alt_minimum_baseline; + } else { + Orientation other_orientation = orientation == Orientation.HORIZONTAL ? Orientation.VERTICAL : Orientation.HORIZONTAL; + int blind_minimum, blind_natural, blind_minimum_baseline, blind_natural_baseline; + original.measure(widget, other_orientation, -1, out blind_minimum, out blind_natural, out blind_minimum_baseline, out blind_natural_baseline); + if (for_size >= blind_minimum) { + original.measure(widget, orientation, for_size, out minimum, out natural, out minimum_baseline, out natural_baseline); + } else { + alternative.measure(widget, orientation, for_size, out minimum, out natural, out minimum_baseline, out natural_baseline); + } + } + } +} \ No newline at end of file diff --git a/main/src/windows/preferences_window/accounts_preferences_page.vala b/main/src/windows/preferences_window/accounts_preferences_page.vala new file mode 100644 index 00000000..7f4c2f1b --- /dev/null +++ b/main/src/windows/preferences_window/accounts_preferences_page.vala @@ -0,0 +1,75 @@ +using Dino.Entities; +using Gee; +using Gtk; + +public class Dino.Ui.PreferencesWindowAccounts : Adw.PreferencesPage { + + public signal void account_chosen(Account account); + + public Adw.PreferencesGroup active_accounts; + public Adw.PreferencesGroup disabled_accounts; + + public ViewModel.PreferencesWindow model { get; set; } + + construct { + this.title = _("Accounts"); + this.icon_name = "system-users-symbolic"; + + this.notify["model"].connect(() => { + model.update.connect(refresh); + }); + } + + private void refresh() { + if (active_accounts != null) this.remove(active_accounts); + if (disabled_accounts != null) this.remove(disabled_accounts); + + active_accounts = new Adw.PreferencesGroup() { title=_("Accounts")}; + disabled_accounts = new Adw.PreferencesGroup() { title=_("Disabled accounts")}; + Button add_account_button = new Button.from_icon_name("list-add-symbolic"); + add_account_button.add_css_class("flat"); + add_account_button.tooltip_text = _("Add Account"); + active_accounts.header_suffix = add_account_button; + + this.add(active_accounts); + this.add(disabled_accounts); + + add_account_button.clicked.connect(() => { + Ui.ManageAccounts.AddAccountDialog add_account_dialog = new Ui.ManageAccounts.AddAccountDialog(model.stream_interactor, model.db); + add_account_dialog.set_transient_for((Window)this.get_root()); + add_account_dialog.added.connect((account) => { + refresh(); + }); + add_account_dialog.present(); + }); + + disabled_accounts.visible = false; // Only display disabled section if it contains accounts + var enabled_account_added = false; + + foreach (ViewModel.AccountDetails account_details in model.account_details.values) { + var row = new Adw.ActionRow() { + title = account_details.bare_jid.to_string() + }; + row.add_prefix(new AvatarPicture() { valign=Align.CENTER, height_request=35, width_request=35, model = account_details.avatar_model }); + row.add_suffix(new Image.from_icon_name("go-next-symbolic")); + row.activatable = true; + + if (account_details.account.enabled) { + active_accounts.add(row); + enabled_account_added = true; + } else { + disabled_accounts.add(row); + disabled_accounts.visible = true; + } + + row.activated.connect(() => { + account_chosen(account_details.account); + }); + } + + // We always have to show the active accounts group for the add new button. Display placeholder if there are no active accounts + if (!enabled_account_added) { + active_accounts.add(new Adw.ActionRow() { title=_("No active accounts") }); + } + } +} diff --git a/main/src/windows/preferences_window/encryption_preferences_page.vala b/main/src/windows/preferences_window/encryption_preferences_page.vala new file mode 100644 index 00000000..2222584d --- /dev/null +++ b/main/src/windows/preferences_window/encryption_preferences_page.vala @@ -0,0 +1,59 @@ +using Dino.Entities; +using Xmpp; +using Xmpp.Xep; +using Gee; +using Gtk; + +//[GtkTemplate (ui = "/im/dino/Dino/preferences_window_encryption.ui")] +public class Dino.Ui.PreferencesWindowEncryption : Adw.PreferencesPage { + + private DropDown drop_down = null; + private Adw.PreferencesGroup accounts_group = new Adw.PreferencesGroup(); + private ArrayList added_widgets = new ArrayList(); + + public ViewModel.PreferencesWindow model { get; set; } + + construct { + this.add(accounts_group); + + this.notify["model"].connect(() => { + this.model.update.connect(() => { + if (drop_down != null) { + accounts_group.remove(drop_down); + drop_down = null; + } + + if (model.active_accounts_selection.get_n_items() > 0) { + drop_down = new DropDown(model.active_accounts_selection, null) { halign=Align.CENTER }; + drop_down.factory = new BuilderListItemFactory.from_resource(null, "/im/dino/Dino/account_picker_row.ui"); + + drop_down.notify["selected-item"].connect(() => { + var account_details = (ViewModel.AccountDetails) drop_down.selected_item; + if (account_details == null) return; + set_account(account_details.account); + }); + + drop_down.selected = 0; + set_account(((ViewModel.AccountDetails)model.active_accounts_selection.get_item(0)).account); + } else { + drop_down = new DropDown.from_strings(new string[] { _("No active accounts")}) { halign=Align.CENTER }; + } + accounts_group.add(drop_down); + }); + }); + } + + public void set_account(Account account) { + foreach (var widget in added_widgets) { + this.remove(widget); + } + added_widgets.clear(); + + Application app = GLib.Application.get_default() as Application; + foreach (Plugins.EncryptionPreferencesEntry e in app.plugin_registry.encryption_preferences_entries) { + var widget = (Adw.PreferencesGroup) e.get_widget(account, Plugins.WidgetType.GTK4); + this.add(widget); + this.added_widgets.add(widget); + } + } +} \ No newline at end of file diff --git a/main/src/windows/preferences_window/general_preferences_page.vala b/main/src/windows/preferences_window/general_preferences_page.vala new file mode 100644 index 00000000..7aa6c2bd --- /dev/null +++ b/main/src/windows/preferences_window/general_preferences_page.vala @@ -0,0 +1,39 @@ +using Gtk; + +public class Dino.Ui.ViewModel.GeneralPreferencesPage : Object { + public bool send_typing { get; set; } + public bool send_marker { get; set; } + public bool notifications { get; set; } + public bool convert_emojis { get; set; } +} + +[GtkTemplate (ui = "/im/dino/Dino/preferences_window_general.ui")] +public class Dino.Ui.GeneralPreferencesPage : Adw.PreferencesPage { + [GtkChild] private unowned Switch typing_switch; + [GtkChild] private unowned Switch marker_switch; + [GtkChild] private unowned Switch notification_switch; + [GtkChild] private unowned Switch emoji_switch; + + public ViewModel.GeneralPreferencesPage model { get; set; default = new ViewModel.GeneralPreferencesPage(); } + private Binding[] model_bindings = new Binding[0]; + + construct { + this.notify["model"].connect(on_model_changed); + } + + private void on_model_changed() { + foreach (Binding binding in model_bindings) { + binding.unbind(); + } + if (model != null) { + model_bindings = new Binding[] { + model.bind_property("send-typing", typing_switch, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL), + model.bind_property("send-marker", marker_switch, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL), + model.bind_property("notifications", notification_switch, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL), + model.bind_property("convert-emojis", emoji_switch, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL) + }; + } else { + model_bindings = new Binding[0]; + } + } +} diff --git a/main/src/windows/preferences_window/preferences_window.vala b/main/src/windows/preferences_window/preferences_window.vala new file mode 100644 index 00000000..e34261e9 --- /dev/null +++ b/main/src/windows/preferences_window/preferences_window.vala @@ -0,0 +1,31 @@ +using Gdk; +using Dino.Entities; +using Xmpp; +using Xmpp.Xep; +using Gee; +using Gtk; + +[GtkTemplate (ui = "/im/dino/Dino/preferences_window.ui")] +public class Dino.Ui.PreferencesWindow : Adw.PreferencesWindow { + [GtkChild] public unowned Dino.Ui.PreferencesWindowAccounts accounts_page; + [GtkChild] public unowned Dino.Ui.PreferencesWindowEncryption encryption_page; + [GtkChild] public unowned Dino.Ui.GeneralPreferencesPage general_page; + public Dino.Ui.AccountPreferencesSubpage account_page = new Dino.Ui.AccountPreferencesSubpage(); + + [GtkChild] public unowned ViewModel.PreferencesWindow model { get; } + + construct { + this.default_height = 500; + this.default_width = 700; + this.can_navigate_back = true; // remove once we require Adw > 1.4 + this.bind_property("model", accounts_page, "model", BindingFlags.SYNC_CREATE); + this.bind_property("model", account_page, "model", BindingFlags.SYNC_CREATE); + this.bind_property("model", encryption_page, "model", BindingFlags.SYNC_CREATE); + + accounts_page.account_chosen.connect((account) => { + model.selected_account = model.account_details[account]; + this.present_subpage(account_page); +// this.present_subpage(new Adw.NavigationPage(account_page, "Account: %s".printf(account.bare_jid.to_string()))); + }); + } +} \ No newline at end of file diff --git a/plugins/omemo/CMakeLists.txt b/plugins/omemo/CMakeLists.txt index 7ecaa0b8..9e290390 100644 --- a/plugins/omemo/CMakeLists.txt +++ b/plugins/omemo/CMakeLists.txt @@ -3,8 +3,10 @@ find_package(Gettext) include(${GETTEXT_USE_FILE}) gettext_compile(${GETTEXT_PACKAGE} SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/po TARGET_NAME ${GETTEXT_PACKAGE}-translations) +find_package(Adwaita REQUIRED) find_package(Qrencode REQUIRED) find_packages(OMEMO_PACKAGES REQUIRED + Adwaita Gee GLib GModule @@ -19,6 +21,7 @@ find_package(SignalProtocol 2.3.2 REQUIRED) set(RESOURCE_LIST contact_details_dialog.ui + encryption_preferences_entry.ui manage_key_dialog.ui ) @@ -31,6 +34,13 @@ compile_gresources( PREFIX /im/dino/Dino/omemo SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/data ) +set(OMEMO_DEFINITIONS) +if(Adwaita_VERSION VERSION_GREATER_EQUAL "1.2") + set(OMEMO_DEFINITIONS ${OMEMO_DEFINITIONS} Adw_1_2) +endif() + +message(STATUS ${Adwaita_VERSION}) +message(STATUS ${Adw_1_2}) vala_precompile(OMEMO_VALA_C SOURCES @@ -65,7 +75,6 @@ SOURCES src/signal/store.vala src/signal/util.vala - src/ui/account_settings_entry.vala src/ui/bad_messages_populator.vala src/ui/call_encryption_entry.vala src/ui/contact_details_provider.vala @@ -73,6 +82,7 @@ SOURCES src/ui/device_notification_populator.vala src/ui/own_notifications.vala src/ui/encryption_list_entry.vala + src/ui/encryption_preferences_entry.vala src/ui/manage_key_dialog.vala src/ui/util.vala CUSTOM_VAPIS @@ -86,6 +96,8 @@ PACKAGES ${OMEMO_PACKAGES} GRESOURCES ${OMEMO_GRESOURCES_XML} +DEFINITIONS + ${OMEMO_DEFINITIONS} GENERATE_VAPI omemo GENERATE_HEADER diff --git a/plugins/omemo/data/encryption_preferences_entry.ui b/plugins/omemo/data/encryption_preferences_entry.ui new file mode 100644 index 00000000..7ca26224 --- /dev/null +++ b/plugins/omemo/data/encryption_preferences_entry.ui @@ -0,0 +1,81 @@ + + + + + + left + + + 10 + 10 + 10 + 10 + + + False + + + + + + diff --git a/plugins/omemo/data/gresource.xml b/plugins/omemo/data/gresource.xml index 616dcdc1..673c3df5 100644 --- a/plugins/omemo/data/gresource.xml +++ b/plugins/omemo/data/gresource.xml @@ -2,6 +2,7 @@ contact_details_dialog.ui + encryption_preferences_entry.ui manage_key_dialog.ui diff --git a/plugins/omemo/meson.build b/plugins/omemo/meson.build index 57eec2ce..05d7c265 100644 --- a/plugins/omemo/meson.build +++ b/plugins/omemo/meson.build @@ -1,5 +1,6 @@ subdir('po') dependencies = [ + dep_libadwaita, dep_crypto_vala, dep_dino, dep_gee, @@ -40,13 +41,13 @@ sources = files( 'src/signal/store.vala', 'src/signal/util.vala', 'src/trust_level.vala', - 'src/ui/account_settings_entry.vala', 'src/ui/bad_messages_populator.vala', 'src/ui/call_encryption_entry.vala', 'src/ui/contact_details_dialog.vala', 'src/ui/contact_details_provider.vala', 'src/ui/device_notification_populator.vala', 'src/ui/encryption_list_entry.vala', + 'src/ui/encryption_preferences_entry.vala', 'src/ui/manage_key_dialog.vala', 'src/ui/own_notifications.vala', 'src/ui/util.vala', diff --git a/plugins/omemo/src/plugin.vala b/plugins/omemo/src/plugin.vala index 643428a8..dfbe0780 100644 --- a/plugins/omemo/src/plugin.vala +++ b/plugins/omemo/src/plugin.vala @@ -30,7 +30,6 @@ public class Plugin : RootInterface, Object { public Dino.Application app; public Database db; public EncryptionListEntry list_entry; - public AccountSettingsEntry settings_entry; public ContactDetailsProvider contact_details_provider; public DeviceNotificationPopulator device_notification_populator; public OwnNotifications own_notifications; @@ -43,13 +42,12 @@ public class Plugin : RootInterface, Object { this.app = app; this.db = new Database(Path.build_filename(Application.get_storage_dir(), "omemo.db")); this.list_entry = new EncryptionListEntry(this); - this.settings_entry = new AccountSettingsEntry(this); this.contact_details_provider = new ContactDetailsProvider(this); this.device_notification_populator = new DeviceNotificationPopulator(this, this.app.stream_interactor); this.trust_manager = new TrustManager(this.app.stream_interactor, this.db); this.app.plugin_registry.register_encryption_list_entry(list_entry); - this.app.plugin_registry.register_account_settings_entry(settings_entry); + this.app.plugin_registry.register_encryption_preferences_entry(new OmemoPreferencesEntry(this)); this.app.plugin_registry.register_contact_details_entry(contact_details_provider); this.app.plugin_registry.register_notification_populator(device_notification_populator); this.app.plugin_registry.register_conversation_addition_populator(new BadMessagesPopulator(this.app.stream_interactor, this)); diff --git a/plugins/omemo/src/ui/account_settings_entry.vala b/plugins/omemo/src/ui/account_settings_entry.vala deleted file mode 100644 index 8736260b..00000000 --- a/plugins/omemo/src/ui/account_settings_entry.vala +++ /dev/null @@ -1,58 +0,0 @@ -using Dino.Entities; -using Gtk; - -namespace Dino.Plugins.Omemo { - -public class AccountSettingsEntry : Plugins.AccountSettingsEntry { - private Plugin plugin; - private Account account; - - private Box box = new Box(Orientation.HORIZONTAL, 0); - private Label fingerprint = new Label("...") { xalign=0 }; - private Button btn = new Button.from_icon_name("view-list-symbolic") { has_frame=false, valign=Align.CENTER, visible=false }; - - public override string id { get { return "omemo_identity_key"; }} - - public override string name { get { return "OMEMO"; }} - - public AccountSettingsEntry(Plugin plugin) { - this.plugin = plugin; - - Border border = new Button().get_style_context().get_padding(); - fingerprint.margin_top = border.top + 1; - fingerprint.margin_start = border.left + 1; - fingerprint.visible = true; - box.append(fingerprint); - - btn.clicked.connect(() => { - activated(); - ContactDetailsDialog dialog = new ContactDetailsDialog(plugin, account, account.bare_jid); - dialog.set_transient_for((Window) box.get_root()); - dialog.present(); - }); - // TODO expand=false? - box.append(btn); - } - - public override Object? get_widget(WidgetType type) { - if (type != WidgetType.GTK4) return null; - return box; - } - - public override void set_account(Account account) { - this.account = account; - btn.visible = false; - Qlite.Row? row = plugin.db.identity.row_with(plugin.db.identity.account_id, account.id).inner; - if (row == null) { - fingerprint.set_markup("%s\n%s".printf(_("Own fingerprint"), _("Will be generated on first connection"))); - } else { - string res = fingerprint_markup(fingerprint_from_base64(((!)row)[plugin.db.identity.identity_key_public_base64])); - fingerprint.set_markup("%s\n%s".printf(_("Own fingerprint"), res)); - btn.visible = true; - } - } - - public override void deactivate() { } -} - -} \ No newline at end of file diff --git a/plugins/omemo/src/ui/encryption_preferences_entry.vala b/plugins/omemo/src/ui/encryption_preferences_entry.vala new file mode 100644 index 00000000..7997f04d --- /dev/null +++ b/plugins/omemo/src/ui/encryption_preferences_entry.vala @@ -0,0 +1,336 @@ +using Qlite; +using Qrencode; +using Gee; +using Xmpp; +using Dino.Entities; +using Gtk; + +namespace Dino.Plugins.Omemo { + +public class OmemoPreferencesEntry : Plugins.EncryptionPreferencesEntry { + + OmemoPreferencesWidget widget; + Plugin plugin; + + public OmemoPreferencesEntry(Plugin plugin) { + this.plugin = plugin; + } + + public override Object? get_widget(Account account, WidgetType type) { + if (type != WidgetType.GTK4) return null; + var widget = new OmemoPreferencesWidget(plugin); + widget.set_account(account); + return widget; + } + + public override string id { get { return "omemo_preferences_entryption"; }} +} + +[GtkTemplate (ui = "/im/dino/Dino/omemo/encryption_preferences_entry.ui")] +public class OmemoPreferencesWidget : Adw.PreferencesGroup { + private Plugin plugin; + private Account account; + private Jid jid; + private int identity_id = 0; + private Signal.Store store; + private Set displayed_ids = new HashSet(); + + [GtkChild] private unowned Adw.ActionRow automatically_accept_new_row; + [GtkChild] private Switch automatically_accept_new_switch; + [GtkChild] private unowned Adw.ActionRow encrypt_by_default_row; + [GtkChild] private Switch encrypt_by_default_switch; + [GtkChild] private unowned Label new_keys_label; + + [GtkChild] private unowned Adw.PreferencesGroup keys_preferences_group; + [GtkChild] private unowned ListBox new_keys_listbox; + [GtkChild] private unowned Picture qrcode_picture; + [GtkChild] private unowned Popover qrcode_popover; + + private ArrayList keys_preferences_group_children = new ArrayList(); + + construct { + // If we set the strings in the .ui file, they don't get translated + encrypt_by_default_row.title = _("OMEMO by default"); + encrypt_by_default_row.subtitle = _("Enable OMEMO encryption for new conversations"); + automatically_accept_new_row.title = _("Encrypt to new devices"); + automatically_accept_new_row.subtitle = _("Automatically encrypt to new devices from this contact."); + new_keys_label.label = _("New keys"); + } + + public OmemoPreferencesWidget(Plugin plugin) { + this.plugin = plugin; + this.account = account; + this.jid = jid; + } + + public void set_account(Account account) { + this.account = account; + this.jid = account.bare_jid; + + automatically_accept_new_switch.set_active(plugin.db.trust.get_blind_trust(identity_id, jid.bare_jid.to_string(), true)); + automatically_accept_new_switch.state_set.connect(on_auto_accept_toggled); + + encrypt_by_default_switch.set_active(plugin.app.settings.get_default_encryption(account) != Encryption.NONE); + encrypt_by_default_switch.state_set.connect(on_omemo_by_default_toggled); + + identity_id = plugin.db.identity.get_id(account.id); + if (identity_id < 0) return; + Dino.Application? app = Application.get_default() as Dino.Application; + if (app != null) { + store = app.stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).store; + } + + redraw_key_list(); + + // Check for unknown devices + fetch_unknown_bundles(); + } + + private void redraw_key_list() { + // Remove current widgets + foreach (var widget in keys_preferences_group_children) { + keys_preferences_group.remove(widget); + } + keys_preferences_group_children.clear(); + + // Dialog opened from the account settings menu + // Show the fingerprint for this device separately with buttons for a qrcode and to copy + if(jid.equals(account.bare_jid)) { + automatically_accept_new_row.subtitle = _("New encryption keys from your other devices will be accepted automatically."); + add_own_fingerprint(); + } + + //Show the normal devicelist + var own_id = plugin.db.identity.row_with(plugin.db.identity.account_id, account.id)[plugin.db.identity.device_id]; + foreach (Row device in plugin.db.identity_meta.get_known_devices(identity_id, jid.to_string())) { + if(jid.equals(account.bare_jid) && device[plugin.db.identity_meta.device_id] == own_id) { + // If this is our own account, don't show this device twice (did it separately already) + continue; + } + add_fingerprint(device, (TrustLevel) device[plugin.db.identity_meta.trust_level]); + } + + //Show any new devices for which the user must decide whether to accept or reject + foreach (Row device in plugin.db.identity_meta.get_new_devices(identity_id, jid.to_string())) { + add_new_fingerprint(device); + } + } + + private static string escape_for_iri_path_segment(string s) { + // from RFC 3986, 2.2. Reserved Characters: + string SUB_DELIMS = "!$&'()*+,;="; + // from RFC 3986, 3.3. Path (pchar without unreserved and pct-encoded): + string ALLOWED_RESERVED_CHARS = SUB_DELIMS + ":@"; + return GLib.Uri.escape_string(s, ALLOWED_RESERVED_CHARS, true); + } + + private void fetch_unknown_bundles() { + Dino.Application app = Application.get_default() as Dino.Application; + XmppStream? stream = app.stream_interactor.get_stream(account); + if (stream == null) return; + StreamModule? module = stream.get_module(StreamModule.IDENTITY); + if (module == null) return; + module.bundle_fetched.connect_after((bundle_jid, device_id, bundle) => { + if (bundle_jid.equals(jid) && !displayed_ids.contains(device_id)) { + redraw_key_list(); + } + }); + foreach (Row device in plugin.db.identity_meta.get_unknown_devices(identity_id, jid.to_string())) { + try { + module.fetch_bundle(stream, new Jid(device[plugin.db.identity_meta.address_name]), device[plugin.db.identity_meta.device_id], false); + } catch (InvalidJidError e) { + warning("Ignoring device with invalid Jid: %s", e.message); + } + } + } + + private void add_own_fingerprint() { + string own_b64 = plugin.db.identity.row_with(plugin.db.identity.account_id, account.id)[plugin.db.identity.identity_key_public_base64]; + string fingerprint = fingerprint_from_base64(own_b64); + + var own_action_box = new Box(Orientation.HORIZONTAL, 6); + var show_qrcode_button = new MenuButton() { icon_name="dino-qr-code-symbolic", valign=Align.CENTER }; + own_action_box.append(show_qrcode_button); + var copy_button = new Button() { icon_name="edit-copy-symbolic", valign=Align.CENTER }; + copy_button.clicked.connect(() => { copy_button.get_clipboard().set_text(fingerprint); }); + own_action_box.append(copy_button); + + Adw.ActionRow action_row = new Adw.ActionRow(); + + action_row.title = "This device"; + action_row.subtitle = format_fingerprint(fingerprint_from_base64(own_b64)); + action_row.add_suffix(own_action_box); +#if Adw_1_2 + action_row.use_markup = true; + action_row.subtitle = fingerprint_markup(fingerprint_from_base64(own_b64)); +#endif + add_key_row(action_row); + + // Create and set QR code popover + int sid = plugin.db.identity.row_with(plugin.db.identity.account_id, account.id)[plugin.db.identity.device_id]; + var iri_query = @"omemo-sid-$(sid)=$(fingerprint)"; +#if GLIB_2_66 && VALA_0_50 + string iri = GLib.Uri.join(UriFlags.NONE, "xmpp", null, null, 0, jid.to_string(), iri_query, null); +#else + var iri_path_seg = escape_for_iri_path_segment(jid.to_string()); + var iri = @"xmpp:$(iri_path_seg)?$(iri_query)"; +#endif + + const int QUIET_ZONE_MODULES = 4; // MUST be at least 4 + const int MODULE_SIZE_PX = 4; // arbitrary + var qr_paintable = new QRcode(iri, 2) + .to_paintable(MODULE_SIZE_PX * qrcode_picture.scale_factor); + qrcode_picture.paintable = qr_paintable; + qrcode_picture.margin_top = qrcode_picture.margin_end = + qrcode_picture.margin_bottom = qrcode_picture.margin_start = QUIET_ZONE_MODULES * MODULE_SIZE_PX; + qrcode_popover.add_css_class("qrcode-container"); + + show_qrcode_button.popover = qrcode_popover; + } + + private void add_fingerprint(Row device, TrustLevel trust) { + string key_base64 = device[plugin.db.identity_meta.identity_key_public_base64]; + bool key_active = device[plugin.db.identity_meta.now_active]; + if (store != null) { + try { + Signal.Address address = new Signal.Address(jid.to_string(), device[plugin.db.identity_meta.device_id]); + Signal.SessionRecord? session = null; + if (store.contains_session(address)) { + session = store.load_session(address); + string session_key_base64 = Base64.encode(session.state.remote_identity_key.serialize()); + if (key_base64 != session_key_base64) { + critical("Session and database identity key mismatch!"); + key_base64 = session_key_base64; + } + } + } catch (Error e) { + print("Error while reading session store: %s", e.message); + } + } + + if (device[plugin.db.identity_meta.now_active]) { + Adw.ActionRow action_row = new Adw.ActionRow(); + action_row.activated.connect(() => { + Row updated_device = plugin.db.identity_meta.get_device(device[plugin.db.identity_meta.identity_id], device[plugin.db.identity_meta.address_name], device[plugin.db.identity_meta.device_id]); + ManageKeyDialog manage_dialog = new ManageKeyDialog(updated_device, plugin.db); + manage_dialog.set_transient_for((Gtk.Window) get_root()); + manage_dialog.present(); + manage_dialog.response.connect((response) => { + update_stored_trust(response, updated_device); + redraw_key_list(); + }); + }); + action_row.activatable = true; + action_row.title = "Other device"; + action_row.subtitle = format_fingerprint(fingerprint_from_base64(key_base64)); + string trust_str = _("Accepted"); + switch(trust) { + case TrustLevel.UNTRUSTED: + trust_str = _("Rejected"); + break; + case TrustLevel.VERIFIED: + trust_str = _("Verified"); + break; + } + + action_row.add_suffix(new Label(trust_str)); +#if Adw_1_2 + action_row.use_markup = true; + action_row.subtitle = fingerprint_markup(fingerprint_from_base64(key_base64)); +#endif + add_key_row(action_row); + } + displayed_ids.add(device[plugin.db.identity_meta.device_id]); + } + + private bool on_auto_accept_toggled(bool active) { + plugin.trust_manager.set_blind_trust(account, jid, active); + + if (active) { + int identity_id = plugin.db.identity.get_id(account.id); + if (identity_id < 0) return false; + + foreach (Row device in plugin.db.identity_meta.get_new_devices(identity_id, jid.to_string())) { + plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], TrustLevel.TRUSTED); + add_fingerprint(device, TrustLevel.TRUSTED); + } + } + return false; + } + + private bool on_omemo_by_default_toggled(bool active) { + var encryption_value = active ? Encryption.OMEMO : Encryption.NONE; + plugin.app.settings.set_default_encryption(account, encryption_value); + return false; + } + + private void update_stored_trust(int response, Row device) { + switch (response) { + case TrustLevel.TRUSTED: + plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], TrustLevel.TRUSTED); + break; + case TrustLevel.UNTRUSTED: + plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], TrustLevel.UNTRUSTED); + break; + case TrustLevel.VERIFIED: + plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], TrustLevel.VERIFIED); + plugin.trust_manager.set_blind_trust(account, jid, false); + automatically_accept_new_switch.set_active(false); + break; + } + } + + private void add_new_fingerprint(Row device) { + Adw.ActionRow action_row = new Adw.ActionRow(); + action_row.title = _("New device"); + action_row.subtitle = format_fingerprint(fingerprint_from_base64(device[plugin.db.identity_meta.identity_key_public_base64])); + +#if Adw_1_2 + action_row.use_markup = true; + action_row.subtitle = fingerprint_markup(fingerprint_from_base64(device[plugin.db.identity_meta.identity_key_public_base64])); +#endif + + Button accept_button = new Button() { visible = true, valign = Align.CENTER, hexpand = true }; + accept_button.set_icon_name("emblem-ok-symbolic"); // using .image = sets .image-button. Together with .suggested/destructive action that breaks the button Adwaita + accept_button.add_css_class("suggested-action"); + accept_button.tooltip_text = _("Accept key"); + + Button reject_button = new Button() { visible = true, valign = Align.CENTER, hexpand = true }; + reject_button.set_icon_name("action-unavailable-symbolic"); + reject_button.add_css_class("destructive-action"); + reject_button.tooltip_text = _("Reject key"); + + accept_button.clicked.connect(() => { + plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], TrustLevel.TRUSTED); + add_fingerprint(device, TrustLevel.TRUSTED); + remove_key_row(action_row); + }); + + reject_button.clicked.connect(() => { + plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], TrustLevel.UNTRUSTED); + add_fingerprint(device, TrustLevel.UNTRUSTED); + remove_key_row(action_row); + }); + + Box control_box = new Box(Gtk.Orientation.HORIZONTAL, 0) { visible = true, hexpand = true }; + control_box.append(accept_button); + control_box.append(reject_button); + control_box.add_css_class("linked"); // .linked: Visually link the accept / reject buttons + + action_row.add_suffix(control_box); + + add_key_row(action_row); + displayed_ids.add(device[plugin.db.identity_meta.device_id]); + } + + private void add_key_row(Adw.PreferencesRow widget) { + keys_preferences_group.add(widget); + keys_preferences_group_children.add(widget); + } + + private void remove_key_row(Adw.PreferencesRow widget) { + keys_preferences_group.remove(widget); + keys_preferences_group_children.remove(widget); + } +} +} \ No newline at end of file diff --git a/plugins/omemo/src/ui/util.vala b/plugins/omemo/src/ui/util.vala index cf61ed82..e250ff4d 100644 --- a/plugins/omemo/src/ui/util.vala +++ b/plugins/omemo/src/ui/util.vala @@ -17,46 +17,24 @@ public static string fingerprint_from_base64(string b64) { } public static string fingerprint_markup(string s) { + return "" + format_fingerprint(s) + ""; +} + +public static string format_fingerprint(string s) { string markup = ""; for (int i = 0; i < s.length; i += 4) { string four_chars = s.substring(i, 4).down(); - int raw = (int) from_hex(four_chars); - uint8[] bytes = {(uint8) ((raw >> 8) & 0xff - 128), (uint8) (raw & 0xff - 128)}; - - Checksum checksum = new Checksum(ChecksumType.SHA1); - checksum.update(bytes, bytes.length); - uint8[] digest = new uint8[20]; - size_t len = 20; - checksum.get_digest(digest, ref len); - - uint8 r = digest[0]; - uint8 g = digest[1]; - uint8 b = digest[2]; - - if (r == 0 && g == 0 && b == 0) r = g = b = 1; - - double brightness = 0.2126 * r + 0.7152 * g + 0.0722 * b; - - if (brightness < 80) { - double factor = 80.0 / brightness; - r = uint8.min(255, (uint8) (r * factor)); - g = uint8.min(255, (uint8) (g * factor)); - b = uint8.min(255, (uint8) (b * factor)); - - } else if (brightness > 180) { - double factor = 180.0 / brightness; - r = (uint8) (r * factor); - g = (uint8) (g * factor); - b = (uint8) (b * factor); - } - if (i % 32 == 0 && i != 0) markup += "\n"; - markup += @"$four_chars"; - if (i % 8 == 4 && i % 32 != 28) markup += " "; + markup += four_chars; + if (i % 16 == 12 && i % 32 != 28) { + markup += " "; + } + if (i % 8 == 4 && i % 16 != 12) { + markup += "\u00a0"; // Non-breaking space + } } - - return "" + markup + ""; + return markup; } } diff --git a/plugins/openpgp/CMakeLists.txt b/plugins/openpgp/CMakeLists.txt index 6ed7bf53..d2ac6d73 100644 --- a/plugins/openpgp/CMakeLists.txt +++ b/plugins/openpgp/CMakeLists.txt @@ -7,6 +7,7 @@ gettext_compile(${GETTEXT_PACKAGE} SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/po TAR find_packages(OPENPGP_PACKAGES REQUIRED + Adwaita Gee GLib GModule @@ -14,19 +15,10 @@ find_packages(OPENPGP_PACKAGES REQUIRED GTK4 ) -set(RESOURCE_LIST - account_settings_item.ui -) - -compile_gresources( - OPENPGP_GRESOURCES_TARGET - OPENPGP_GRESOURCES_XML - TARGET ${CMAKE_CURRENT_BINARY_DIR}/resources/resources.c - TYPE EMBED_C - RESOURCES ${RESOURCE_LIST} - PREFIX /im/dino/Dino/openpgp - SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/data -) +set(OPENPGP_DEFINITIONS) +if(Adwaita_VERSION VERSION_GREATER_EQUAL "1.4") + set(OPENPGP_DEFINITIONS ${OPENPGP_DEFINITIONS} Adw_1_4) +endif() vala_precompile(OPENPGP_VALA_C SOURCES @@ -35,10 +27,10 @@ SOURCES src/file_transfer/file_decryptor.vala src/file_transfer/file_encryptor.vala - src/account_settings_entry.vala src/contact_details_provider.vala src/database.vala src/encryption_list_entry.vala + src/encryption_preferences_entry.vala src/manager.vala src/plugin.vala src/register_plugin.vala @@ -53,12 +45,12 @@ CUSTOM_VAPIS ${CMAKE_BINARY_DIR}/exports/dino.vapi PACKAGES ${OPENPGP_PACKAGES} -GRESOURCES - ${OPENPGP_GRESOURCES_XML} +DEFINITIONS + ${OPENPGP_DEFINITIONS} ) add_definitions(${VALA_CFLAGS} -DG_LOG_DOMAIN="OpenPGP" -DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\" -DLOCALE_INSTALL_DIR=\"${LOCALE_INSTALL_DIR}\") -add_library(openpgp SHARED ${OPENPGP_VALA_C} ${OPENPGP_GRESOURCES_TARGET} src/gpgme_fix.c) +add_library(openpgp SHARED ${OPENPGP_VALA_C} src/gpgme_fix.c) add_dependencies(openpgp ${GETTEXT_PACKAGE}-translations) target_include_directories(openpgp PRIVATE src) target_link_libraries(openpgp libdino gpgme ${OPENPGP_PACKAGES}) diff --git a/plugins/openpgp/data/account_settings_item.ui b/plugins/openpgp/data/account_settings_item.ui deleted file mode 100644 index 56808be0..00000000 --- a/plugins/openpgp/data/account_settings_item.ui +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - label - - - 0 - 0 - - - 0 - - - - - - - - - entry - - - 1 - - - - - - \ No newline at end of file diff --git a/plugins/openpgp/meson.build b/plugins/openpgp/meson.build index 806494f2..ec6f5c0e 100644 --- a/plugins/openpgp/meson.build +++ b/plugins/openpgp/meson.build @@ -1,5 +1,6 @@ subdir('po') dependencies = [ + dep_libadwaita, dep_dino, dep_gee, dep_glib, @@ -10,7 +11,6 @@ dependencies = [ dep_xmpp_vala, ] sources = files( - 'src/account_settings_entry.vala', 'src/contact_details_provider.vala', 'src/database.vala', 'src/encryption_list_entry.vala', diff --git a/plugins/openpgp/src/account_settings_entry.vala b/plugins/openpgp/src/account_settings_entry.vala deleted file mode 100644 index 7c99942f..00000000 --- a/plugins/openpgp/src/account_settings_entry.vala +++ /dev/null @@ -1,163 +0,0 @@ -using Dino.Entities; -using Gtk; - -namespace Dino.Plugins.OpenPgp { - -public class AccountSettingsEntry : Plugins.AccountSettingsEntry { - - private Label label; - private Button button; - private ComboBox combobox; - private Stack stack; - - private Plugin plugin; - private Account current_account; - private Gee.List keys = null; - private Gtk.ListStore list_store = new Gtk.ListStore(2, typeof(string), typeof(string?)); - - public override string id { get { return "pgp_key_picker"; }} - - public override string name { get { return "OpenPGP"; }} - - public AccountSettingsEntry(Plugin plugin) { - this.plugin = plugin; - - Builder builder = new Builder.from_resource("/im/dino/Dino/openpgp/account_settings_item.ui"); - stack = (Stack) builder.get_object("stack"); - label = (Label) builder.get_object("label"); - button = (Button) builder.get_object("button"); - combobox = (ComboBox) builder.get_object("combobox"); - - CellRendererText renderer = new CellRendererText(); - renderer.set_padding(0, 0); - combobox.pack_start(renderer, true); - combobox.add_attribute(renderer, "markup", 0); - combobox.set_model(list_store); - - button.clicked.connect(on_button_clicked); - combobox.changed.connect(key_changed); - } - - public override void deactivate() { - stack.set_visible_child_name("label"); - } - - public override void set_account(Account account) { - set_account_.begin(account); - } - - private async void set_account_(Account account) { - this.current_account = account; - if (keys == null) { - yield fetch_keys(); - populate_list_store(); - } - activate_current_account(); - } - - private void on_button_clicked() { - activated(); - stack.set_visible_child_name("entry"); - combobox.grab_focus(); - combobox.popup(); - } - - private void activate_current_account() { - combobox.changed.disconnect(key_changed); - if (keys == null) { - label.set_markup(build_markup_string(_("Key publishing disabled"), _("Error in GnuPG"))); - return; - } - if (keys.size == 0) { - label.set_markup(build_markup_string(_("Key publishing disabled"), _("No keys available. Generate one!"))); - return; - } - - string? account_key = plugin.db.get_account_key(current_account); - int activate_index = 0; - for (int i = 0; i < keys.size; i++) { - GPG.Key key = keys[i]; - if (key.fpr == account_key) { - activate_index = i + 1; - } - } - combobox.active = activate_index; - - TreeIter selected; - combobox.get_active_iter(out selected); - set_label_active(selected); - - combobox.changed.connect(key_changed); - } - - private void populate_list_store() { - if (keys == null || keys.size == 0) { - return; - } - - list_store.clear(); - TreeIter iter; - list_store.append(out iter); - list_store.set(iter, 0, build_markup_string(_("Key publishing disabled"), _("Select key") + " \n "), 1, ""); - for (int i = 0; i < keys.size; i++) { - list_store.append(out iter); - list_store.set(iter, 0, @"$(Markup.escape_text(keys[i].uids[0].uid))\n$(markup_colorize_id(keys[i].fpr, true)) "); - list_store.set(iter, 1, keys[i].fpr); - if (keys[i].fpr == plugin.db.get_account_key(current_account)) { - set_label_active(iter, i + 1); - } - } - button.sensitive = true; - } - - private async void fetch_keys() { - label.set_markup(build_markup_string(_("Loading…"), _("Querying GnuPG"))); - - SourceFunc callback = fetch_keys.callback; - new Thread (null, () => { // Querying GnuPG might take some time - try { - keys = GPGHelper.get_keylist(null, true); - } catch (Error e) { - warning(e.message); - } - Idle.add((owned)callback); - return null; - }); - yield; - } - - private void set_label_active(TreeIter iter, int i = -1) { - Value text; - list_store.get_value(iter, 0, out text); - label.set_markup((string) text); - if (i != -1) combobox.active = i; - } - - private void key_changed() { - TreeIter selected; - bool iter_valid = combobox.get_active_iter(out selected); - if (iter_valid) { - Value key_value; - list_store.get_value(selected, 1, out key_value); - string? key_id = key_value as string; - if (key_id != null) { - if (plugin.modules.has_key(current_account)) { - plugin.modules[current_account].set_private_key_id(key_id); - } - plugin.db.set_account_key(current_account, key_id); - } - set_label_active(selected); - deactivate(); - } - } - - private string build_markup_string(string primary, string secondary) { - return @"$(Markup.escape_text(primary))\n$secondary"; - } - - public override Object? get_widget(WidgetType type) { - if (type != WidgetType.GTK4) return null; - return stack; - } -} -} \ No newline at end of file diff --git a/plugins/openpgp/src/encryption_preferences_entry.vala b/plugins/openpgp/src/encryption_preferences_entry.vala new file mode 100644 index 00000000..4620e173 --- /dev/null +++ b/plugins/openpgp/src/encryption_preferences_entry.vala @@ -0,0 +1,86 @@ +using Adw; +using Dino.Entities; +using Gtk; + +namespace Dino.Plugins.OpenPgp { + + public class PgpPreferencesEntry : Plugins.EncryptionPreferencesEntry { + + private Plugin plugin; + + public PgpPreferencesEntry(Plugin plugin) { + this.plugin = plugin; + } + + public override Object? get_widget(Account account, WidgetType type) { + if (type != WidgetType.GTK4) return null; + StringList string_list = new StringList(null); + string_list.append(_("Querying GnuPG")); + + Adw.PreferencesGroup preferences_group = new Adw.PreferencesGroup() { title="OpenPGP" }; + populate_string_list.begin(account, preferences_group); + + return preferences_group; + } + + public override string id { get { return "pgp_preferences_encryption"; }} + + private async void populate_string_list(Account account, Adw.PreferencesGroup preferences_group) { + var keys = yield get_pgp_keys(); + + if (keys == null) { + preferences_group.add(new Adw.ActionRow() { title="Announce key", subtitle="Error in GnuPG" }); + return; + } + if (keys.size == 0) { + preferences_group.add(new Adw.ActionRow() { title="Announce key", subtitle="No keys available. Generate one!" }); + return; + } + + StringList string_list = new StringList(null); +#if Adw_1_4 + var drop_down = new Adw.ComboRow() { title = "Announce key" }; + drop_down.model = string_list; + preferences_group.add(drop_down); +#else + var view = new Adw.ActionRow() { title = "Announce key" }; + var drop_down = new DropDown(string_list, null) { valign = Align.CENTER }; + view.activatable_widget = drop_down; + view.add_suffix(drop_down); + preferences_group.add(view); +#endif + + string_list.append(_("Disabled")); + for (int i = 0; i < keys.size; i++) { + string_list.append(@"$(keys[i].uids[0].uid)\n$(keys[i].fpr.substring(24, 16))"); + if (keys[i].fpr == plugin.db.get_account_key(account)) { + drop_down.selected = i + 1; + } + } + + drop_down.notify["selected"].connect(() => { + var key_id = drop_down.selected == 0 ? "" : keys[(int)drop_down.selected - 1].fpr; + if (plugin.modules.has_key(account)) { + plugin.modules[account].set_private_key_id(key_id); + } + plugin.db.set_account_key(account, key_id); + }); + } + + private static async Gee.List get_pgp_keys() { + Gee.List keys = null; + SourceFunc callback = get_pgp_keys.callback; + new Thread (null, () => { // Querying GnuPG might take some time + try { + keys = GPGHelper.get_keylist(null, true); + } catch (Error e) { + warning(e.message); + } + Idle.add((owned)callback); + return null; + }); + yield; + return keys; + } + } +} \ No newline at end of file diff --git a/plugins/openpgp/src/plugin.vala b/plugins/openpgp/src/plugin.vala index 324b8652..463058f3 100644 --- a/plugins/openpgp/src/plugin.vala +++ b/plugins/openpgp/src/plugin.vala @@ -13,18 +13,16 @@ public class Plugin : Plugins.RootInterface, Object { public HashMap modules = new HashMap(Account.hash_func, Account.equals_func); private EncryptionListEntry list_entry; - private AccountSettingsEntry settings_entry; private ContactDetailsProvider contact_details_provider; public void registered(Dino.Application app) { this.app = app; this.db = new Database(Path.build_filename(Application.get_storage_dir(), "pgp.db")); this.list_entry = new EncryptionListEntry(app.stream_interactor, db); - this.settings_entry = new AccountSettingsEntry(this); this.contact_details_provider = new ContactDetailsProvider(app.stream_interactor); app.plugin_registry.register_encryption_list_entry(list_entry); - app.plugin_registry.register_account_settings_entry(settings_entry); + app.plugin_registry.register_encryption_preferences_entry(new PgpPreferencesEntry(this)); app.plugin_registry.register_contact_details_entry(contact_details_provider); app.stream_interactor.module_manager.initialize_account_modules.connect(on_initialize_account_modules); -- cgit v1.2.3-70-g09d2 From c95b65e5b47f22b1827f351a59cabb8e5f776def Mon Sep 17 00:00:00 2001 From: Marvin W Date: Fri, 19 Jul 2024 18:25:03 +0200 Subject: OMEMO: Do not show message for OMEMO messages without payload --- libdino/src/entity/message.vala | 1 + plugins/omemo/src/logic/decrypt.vala | 13 ++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) (limited to 'plugins/omemo/src') diff --git a/libdino/src/entity/message.vala b/libdino/src/entity/message.vala index 912639b1..9d1cd43e 100644 --- a/libdino/src/entity/message.vala +++ b/libdino/src/entity/message.vala @@ -202,6 +202,7 @@ public class Message : Object { } public static uint hash_func(Message message) { + if (message.body == null) return 0; return message.body.hash(); } diff --git a/plugins/omemo/src/logic/decrypt.vala b/plugins/omemo/src/logic/decrypt.vala index 561e557b..04339c93 100644 --- a/plugins/omemo/src/logic/decrypt.vala +++ b/plugins/omemo/src/logic/decrypt.vala @@ -28,9 +28,6 @@ namespace Dino.Plugins.Omemo { StanzaNode? encrypted_node = stanza.stanza.get_subnode("encrypted", NS_URI); if (encrypted_node == null || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false; - if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == NS_URI) { - message.body = "[This message is OMEMO encrypted]"; // TODO temporary - } if (!Plugin.ensure_context()) return false; int identity_id = db.identity.get_id(conversation.account.id); @@ -38,7 +35,7 @@ namespace Dino.Plugins.Omemo { stanza.add_flag(flag); Xep.Omemo.ParsedData? data = parse_node(encrypted_node); - if (data == null || data.ciphertext == null) return false; + if (data == null) return false; foreach (Bytes encr_key in data.our_potential_encrypted_keys.keys) { @@ -52,14 +49,16 @@ namespace Dino.Plugins.Omemo { foreach (Jid possible_jid in possible_jids) { try { uint8[] key = decrypt_key(data, possible_jid); - string cleartext = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, data.iv, data.ciphertext)); + if (data.ciphertext != null) { + string cleartext = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, data.iv, data.ciphertext)); + message.body = cleartext; + } // If we figured out which real jid a message comes from due to decryption working, save it if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) { message.real_jid = possible_jid; } - message.body = cleartext; message.encryption = Encryption.OMEMO; trust_manager.message_device_id_map[message] = data.sid; @@ -71,7 +70,7 @@ namespace Dino.Plugins.Omemo { } if ( - encrypted_node.get_deep_string_content("payload") != null && // Ratchet forwarding doesn't contain payload and might not include us, which is ok + data.ciphertext != null && // Ratchet forwarding doesn't contain payload and might not include us, which is ok data.our_potential_encrypted_keys.size == 0 && // The message was not encrypted to us stream_interactor.module_manager.get_module(message.account, StreamModule.IDENTITY).store.local_registration_id != data.sid // Message from this device. Never encrypted to itself. ) { -- cgit v1.2.3-70-g09d2 From cfb0ca2a64c1d680799ce752412b089ac075b780 Mon Sep 17 00:00:00 2001 From: Matthew Fennell Date: Sun, 18 Aug 2024 19:40:55 +0100 Subject: Add omemo key management tooltip (#1603) --- plugins/omemo/src/ui/contact_details_provider.vala | 1 + 1 file changed, 1 insertion(+) (limited to 'plugins/omemo/src') diff --git a/plugins/omemo/src/ui/contact_details_provider.vala b/plugins/omemo/src/ui/contact_details_provider.vala index 822294cc..a97a40ad 100644 --- a/plugins/omemo/src/ui/contact_details_provider.vala +++ b/plugins/omemo/src/ui/contact_details_provider.vala @@ -29,6 +29,7 @@ public class ContactDetailsProvider : Plugins.ContactDetailsProvider, Object { if (i > 0) { Button btn = new Button.from_icon_name("view-list-symbolic") { visible = true, valign = Align.CENTER, has_frame = false }; + btn.tooltip_text = _("OMEMO Key Management"); btn.clicked.connect(() => { btn.activate(); ContactDetailsDialog dialog = new ContactDetailsDialog(plugin, conversation.account, conversation.counterpart); -- cgit v1.2.3-70-g09d2 From 1431426581595686aff19300c80af91e814661bd Mon Sep 17 00:00:00 2001 From: fiaxh Date: Mon, 19 Aug 2024 12:16:40 +0200 Subject: Omemo: Connect listener only once on account added --- plugins/omemo/src/logic/manager.vala | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'plugins/omemo/src') diff --git a/plugins/omemo/src/logic/manager.vala b/plugins/omemo/src/logic/manager.vala index 5552e212..ba02bab5 100644 --- a/plugins/omemo/src/logic/manager.vala +++ b/plugins/omemo/src/logic/manager.vala @@ -66,6 +66,7 @@ public class Manager : StreamInteractionModule, Object { this.trust_manager = trust_manager; this.encryptors = encryptors; + stream_interactor.account_added.connect(on_account_added); stream_interactor.stream_negotiated.connect(on_stream_negotiated); stream_interactor.get_module(MessageProcessor.IDENTITY).pre_message_send.connect(on_pre_message_send); stream_interactor.get_module(RosterManager.IDENTITY).mutual_subscription.connect(on_mutual_subscription); @@ -182,6 +183,12 @@ public class Manager : StreamInteractionModule, Object { StreamModule module = stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY); if (module != null) { module.request_user_devicelist.begin(stream, account.bare_jid); + } + } + + private void on_account_added(Account account) { + StreamModule module = stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY); + if (module != null) { module.device_list_loaded.connect((jid, devices) => on_device_list_loaded(account, jid, devices)); module.bundle_fetched.connect((jid, device_id, bundle) => on_bundle_fetched(account, jid, device_id, bundle)); module.bundle_fetch_failed.connect((jid) => continue_message_sending(account, jid)); -- cgit v1.2.3-70-g09d2