diff options
38 files changed, 1483 insertions, 445 deletions
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<Entities.Encryption, EncryptionListEntry> encryption_list_entries = new HashMap<Entities.Encryption, EncryptionListEntry>(); public HashMap<string, CallEncryptionEntry> call_encryption_entries = new HashMap<string, CallEncryptionEntry>(); public ArrayList<AccountSettingsEntry> account_settings_entries = new ArrayList<AccountSettingsEntry>(); + public ArrayList<EncryptionPreferencesEntry> encryption_preferences_entries = new ArrayList<EncryptionPreferencesEntry>(); public ArrayList<ContactDetailsProvider> contact_details_entries = new ArrayList<ContactDetailsProvider>(); public Map<string, TextCommand> text_commands = new HashMap<string, TextCommand>(); public Gee.List<ConversationAdditionPopulator> conversation_addition_populators = new ArrayList<ConversationAdditionPopulator>(); @@ -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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="GtkListItem"> + <property name="child"> + <object class="GtkBox"> + <property name="orientation">horizontal</property> + <property name="spacing">6</property> + <child> + <object class="DinoUiAvatarPicture"> + <property name="height-request">25</property> + <property name="width-request">25</property> + <binding name="model"> + <lookup name="avatar-model" type="DinoUiViewModelAccountDetails"> + <lookup name="item">GtkListItem</lookup> + </lookup> + </binding> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="xalign">0</property> + <binding name="label"> + <lookup name="bare_jid" type="DinoUiViewModelAccountDetails"> + <lookup name="item">GtkListItem</lookup> + </lookup> + </binding> + </object> + </child> + </object> + </property> + </template> +</interface>
\ 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 @@ <?xml version="1.0" encoding="UTF-8"?> <gresources> <gresource prefix="/im/dino/Dino"> + <file>account_picker_row.ui</file> <file>add_conversation/add_contact_dialog.ui</file> <file>add_conversation/add_groupchat_dialog.ui</file> <file>add_conversation/conference_details_fragment.ui</file> @@ -66,6 +67,9 @@ <file>occupant_list.ui</file> <file>occupant_list_item.ui</file> <file>quote.ui</file> + <file>preferences_window.ui</file> + <file>preferences_window_account.ui</file> + <file>preferences_window_general.ui</file> <file>search_autocomplete.ui</file> <file>settings_dialog.ui</file> <file>style-dark.css</file> 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 @@ <menu id="menu_app"> <section> <item> - <attribute name="action">app.accounts</attribute> - <attribute name="label" translatable="yes">Accounts</attribute> - </item> - </section> - <section> - <item> - <attribute name="action">app.settings</attribute> + <attribute name="action">app.preferences</attribute> <attribute name="label" translatable="yes">Preferences</attribute> </item> <item> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk" version="4.0"/> + <object class="DinoUiViewModelPreferencesWindow" id="model" /> + <template class="DinoUiPreferencesWindow"> + <property name="default-width">500</property> + <property name="default-height">600</property> + <property name="modal">True</property> + <child> + <object class="DinoUiPreferencesWindowAccounts" id="accounts_page"> + <property name="title">Accounts</property> + <property name="name">accounts</property> + <property name="icon-name">system-users-symbolic</property> + </object> + </child> + <child> + <object class="DinoUiPreferencesWindowEncryption" id="encryption_page"> + <property name="title">Encryption</property> + <property name="name">encryption</property> + <property name="icon-name">changes-prevent-symbolic</property> + </object> + </child> + <child> + <object class="DinoUiGeneralPreferencesPage" id="general_page"> + <property name="title">General</property> + <property name="name">general</property> + <property name="icon-name">preferences-system-symbolic</property> + <property name="model" bind-source="model" bind-property="general-page" bind-flags="sync-create" /> + </object> + </child> + </template> +</interface>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk" version="4.0"/> + <template class="DinoUiAccountPreferencesSubpage"> + <property name="orientation">vertical</property> + <child> + <object class="AdwHeaderBar"> + <style> + <class name="flat"/> + </style> + <property name="show-title">False</property> + <child> + <object class="GtkButton" id="back_button"> + <property name="icon-name">go-previous-symbolic</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkScrolledWindow"> + <property name="vexpand">True</property> + <property name="hexpand">True</property> + <child> + <object class="AdwClamp"> + <child> + <object class="GtkBox"> + <property name="margin-top">24</property> + <property name="margin-bottom">24</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="spacing">24</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkOverlay"> + <property name="halign">center</property> + <property name="child"> + <object class="DinoUiAvatarPicture" id="avatar"> + <property name="height-request">144</property> + <property name="width-request">144</property> + </object> + </property> + <child type="overlay"> + <object class="GtkBox" id="avatar_menu_box"> + <property name="opacity">0.9</property> + <property name="margin-end">6</property> + <property name="margin-bottom">6</property> + <property name="halign">end</property> + <property name="valign">end</property> + <style> + <class name="card"/> + <class name="toolbar"/> + <class name="overlay-toolbar"/> + </style> + <child> + <object class="GtkButton" id="edit_avatar_button"> + <property name="icon-name">document-edit-symbolic</property> + </object> + </child> + <child> + <object class="GtkButton" id="remove_avatar_button"> + <property name="icon-name">user-trash-symbolic</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="AdwActionRow" id="xmpp_address"> + <property name="title" translatable="yes">XMPP Address</property> + <style> + <class name="property"/> + </style> + </object> + </child> + <child> + <object class="AdwActionRow" id="local_alias"> + <property name="title" translatable="yes">Local alias</property> + <child type="suffix"> + <object class="GtkEntry" id="local_alias_entry"> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + <child> + <object class="AdwActionRow" id="connection_status"> + <property name="title" translatable="yes">Connection status</property> + <style> + <class name="property"/> + </style> + <child type="suffix"> + <object class="GtkButton" id="enter_password_button"> + <property name="label">Enter password</property> + <property name="valign">center</property> + <property name="visible">False</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="button_container"> + <property name="halign">center</property> + <property name="spacing">24</property> + <child> + <object class="GtkButton" id="disable_account_button"> + <property name="label" translatable="1">Disable account</property> + <property name="halign">center</property> + <style> + <class name="pill"/> + </style> + </object> + </child> + <child> + <object class="GtkButton" id="remove_account_button"> + <property name="label" translatable="1">Remove account</property> + <property name="halign">center</property> + <style> + <class name="pill"/> + <class name="destructive-action"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk" version="4.0"/> + <template class="DinoUiGeneralPreferencesPage" parent="AdwPreferencesPage"> + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="AdwActionRow"> + <property name="title" translatable="yes">Send _Typing Notifications</property> + <property name="use-underline">True</property> + <property name="activatable-widget">typing_switch</property> + <child type="suffix"> + <object class="GtkSwitch" id="typing_switch"> +<!-- <property name="active" bind-source="DinoUiGeneralPreferencesPage" bind-property="send-typing" bind-flags="sync-create|bidirectional" />--> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + <child> + <object class="AdwActionRow"> + <property name="title" translatable="yes">Send _Read Receipts</property> + <property name="use-underline">True</property> + <property name="activatable-widget">marker_switch</property> + <child type="suffix"> + <object class="GtkSwitch" id="marker_switch"> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="AdwActionRow"> + <property name="title" translatable="yes">_Notifications</property> + <property name="subtitle" translatable="yes">Notify when a new message arrives</property> + <property name="use-underline">True</property> + <property name="activatable-widget">notification_switch</property> + <child type="suffix"> + <object class="GtkSwitch" id="notification_switch"> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="AdwActionRow"> + <property name="title" translatable="yes">_Convert Smileys to Emoji</property> + <property name="use-underline">True</property> + <property name="activatable-widget">emoji_switch</property> + <child type="suffix"> + <object class="GtkSwitch" id="emoji_switch"> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> 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 @@ -<?xml version="1.0" encoding="UTF-8"?> -<interface> - <template class="DinoUiSettingsDialog" parent="AdwPreferencesWindow"> - <property name="default-width">500</property> - <property name="default-height">360</property> - <property name="modal">True</property> - <property name="search-enabled">False</property> - <child> - <object class="AdwPreferencesPage"> - <child> - <object class="AdwPreferencesGroup"> - <child> - <object class="AdwActionRow"> - <property name="title" translatable="yes">Send _Typing Notifications</property> - <property name="use-underline">True</property> - <property name="activatable-widget">typing_switch</property> - <child type="suffix"> - <object class="GtkSwitch" id="typing_switch"> - <property name="valign">center</property> - </object> - </child> - </object> - </child> - <child> - <object class="AdwActionRow"> - <property name="title" translatable="yes">Send _Read Receipts</property> - <property name="use-underline">True</property> - <property name="activatable-widget">marker_switch</property> - <child type="suffix"> - <object class="GtkSwitch" id="marker_switch"> - <property name="valign">center</property> - </object> - </child> - </object> - </child> - </object> - </child> - <child> - <object class="AdwPreferencesGroup"> - <child> - <object class="AdwActionRow"> - <property name="title" translatable="yes">_Notifications</property> - <property name="subtitle" translatable="yes">Notify when a new message arrives</property> - <property name="use-underline">True</property> - <property name="activatable-widget">notification_switch</property> - <child type="suffix"> - <object class="GtkSwitch" id="notification_switch"> - <property name="valign">center</property> - </object> - </child> - </object> - </child> - </object> - </child> - <child> - <object class="AdwPreferencesGroup"> - <child> - <object class="AdwActionRow"> - <property name="title" translatable="yes">_Convert Smileys to Emoji</property> - <property name="use-underline">True</property> - <property name="activatable-widget">emoji_switch</property> - <child type="suffix"> - <object class="GtkSwitch" id="emoji_switch"> - <property name="valign">center</property> - </object> - </child> - </object> - </child> - </object> - </child> - </object> - </child> - </template> -</interface> 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<string, Variant> hash_table = new HashTable<string, Variant>(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, AccountDetails> account_details = new HashMap<Account, AccountDetails>(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<Account>(); + 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<Adw.PreferencesGroup> added_widgets = new ArrayList<Adw.PreferencesGroup>(); + + 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk" version="4.0"/> + <template class="DinoPluginsOmemoOmemoPreferencesWidget"> + <child> + <object class="AdwPreferencesGroup" id="keys_preferences_group"> + <property name="title">OMEMO</property> + <property name="description">Each device has its own OMEMO key. Messages can only be decrypted by a device if they are encrypted to its key. Messages are only encrypted to accepted devices.</property> + <property name="margin-bottom">12</property> + </object> + </child> + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="AdwActionRow" id="encrypt_by_default_row"> + <child type="suffix"> + <object class="GtkSwitch" id="encrypt_by_default_switch"> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + <child> + <object class="AdwActionRow" id="automatically_accept_new_row"> + <child type="suffix"> + <object class="GtkSwitch" id="automatically_accept_new_switch"> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="new_keys_container"> + <property name="visible">0</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child> + <object class="GtkLabel" id="new_keys_label"> + <property name="halign">start</property> + <attributes> + <attribute name="weight" value="PANGO_WEIGHT_BOLD"></attribute> + </attributes> + </object> + </child> + <child> + <object class="GtkFrame"> + <property name="child"> + <object class="GtkScrolledWindow"> + <property name="hscrollbar_policy">never</property> + <property name="propagate_natural_height">1</property> + <property name="child"> + <object class="GtkListBox" id="new_keys_listbox"> + <property name="selection-mode">none</property> + </object> + </property> + </object> + </property> + </object> + </child> + </object> + </child> + </template> + <object class="GtkPopover" id="qrcode_popover"> + <property name="position">left</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-start">10</property> + <property name="margin-end">10</property> + <property name="margin-top">10</property> + <property name="margin-bottom">10</property> + <child> + <object class="GtkPicture" id="qrcode_picture"> + <property name="can-shrink">False</property> + </object> + </child> + </object> + </property> + </object> +</interface> 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 @@ <gresources> <gresource prefix="/im/dino/Dino/omemo"> <file>contact_details_dialog.ui</file> + <file>encryption_preferences_entry.ui</file> <file>manage_key_dialog.ui</file> </gresource> </gresources> 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<span font='8'>%s</span>".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<span font_family='monospace' font='8'>%s</span>".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<uint32> displayed_ids = new HashSet<uint32>(); + + [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<Widget> keys_preferences_group_children = new ArrayList<Widget>(); + + 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 "<span font_family='monospace' font='9'>" + format_fingerprint(s) + "</span>"; +} + +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 += @"<span foreground=\"$("#%02x%02x%02x".printf(r, g, b))\">$four_chars</span>"; - 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 "<span font_family='monospace' font='8'>" + markup + "</span>"; + 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 @@ -<interface> - <requires lib="gtk" version="4.0"/> - <object class="GtkStack" id="stack"> - <child> - <object class="GtkStackPage"> - <property name="name">label</property> - <property name="child"> - <object class="GtkButton" id="button"> - <property name="has-frame">0</property> - <property name="sensitive">0</property> - <child> - <object class="GtkLabel" id="label"> - <property name="xalign">0</property> - </object> - </child> - </object> - </property> - </object> - </child> - <child> - <object class="GtkStackPage"> - <property name="name">entry</property> - <property name="child"> - <object class="GtkComboBox" id="combobox"> - <property name="hexpand">1</property> - </object> - </property> - </object> - </child> - </object> -</interface>
\ 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<GPG.Key> 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") + "<span font_family='monospace' font='8'> \n </span>"), 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<span font_family='monospace' font='8'>$(markup_colorize_id(keys[i].fpr, true))</span><span font='8'> </span>"); - 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<void*> (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<span font='8'>$secondary</span>"; - } - - 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<GPG.Key> get_pgp_keys() { + Gee.List<GPG.Key> keys = null; + SourceFunc callback = get_pgp_keys.callback; + new Thread<void*> (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<Account, Module> modules = new HashMap<Account, Module>(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); |