From d8881c4b16a1cd376dd69304c44444aee581da32 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Sat, 8 Apr 2017 11:53:10 +0200 Subject: [PATCH] Show account status (incl errors) in ManageAccounts.Dialog --- libdino/src/service/connection_manager.vala | 129 ++++++++++------ libdino/src/service/module_manager.vala | 17 +-- main/data/manage_accounts/account_row.ui | 5 + main/data/manage_accounts/dialog.ui | 15 ++ main/src/ui/manage_accounts/account_row.vala | 21 +++ main/src/ui/manage_accounts/dialog.vala | 139 +++++++++++++----- main/src/ui/util.vala | 1 - .../openpgp/src/account_settings_widget.vala | 4 +- 8 files changed, 234 insertions(+), 97 deletions(-) diff --git a/libdino/src/service/connection_manager.vala b/libdino/src/service/connection_manager.vala index 9d069efa..03c97bdb 100644 --- a/libdino/src/service/connection_manager.vala +++ b/libdino/src/service/connection_manager.vala @@ -9,6 +9,7 @@ public class ConnectionManager { public signal void stream_opened(Account account, Core.XmppStream stream); public signal void connection_state_changed(Account account, ConnectionState state); + public signal void connection_error(Account account, ConnectionError error); public enum ConnectionState { CONNECTED, @@ -17,12 +18,32 @@ public class ConnectionManager { } private ArrayList connection_todo = new ArrayList(Account.equals_func); - private HashMap stream_states = new HashMap(Account.hash_func, Account.equals_func); + private HashMap connections = new HashMap(Account.hash_func, Account.equals_func); + private HashMap connection_errors = new HashMap(Account.hash_func, Account.equals_func); + private NetworkManager? network_manager; private Login1Manager? login1; private ModuleManager module_manager; public string? log_options; + public class ConnectionError { + + public enum Source { + CONNECTION, + SASL, + STREAM_ERROR + } + + public Source source; + public string? identifier; + public StreamError.Flag? flag; + + public ConnectionError(Source source, string? identifier) { + this.source = source; + this.identifier = identifier; + } + } + private class Connection { public Core.XmppStream stream { get; set; } public ConnectionState connection_state { get; set; default = ConnectionState.DISCONNECTED; } @@ -46,26 +67,33 @@ public class ConnectionManager { } public Core.XmppStream? get_stream(Account account) { - if (get_connection_state(account) == ConnectionState.CONNECTED) { - return stream_states[account].stream; + if (get_state(account) == ConnectionState.CONNECTED) { + return connections[account].stream; } return null; } - public ConnectionState get_connection_state(Account account) { - if (stream_states.has_key(account)){ - return stream_states[account].connection_state; + public ConnectionState get_state(Account account) { + if (connections.has_key(account)){ + return connections[account].connection_state; } return ConnectionState.DISCONNECTED; } + public ConnectionError? get_error(Account account) { + if (connection_errors.has_key(account)) { + return connection_errors[account]; + } + return null; + } + public ArrayList get_managed_accounts() { return connection_todo; } public Core.XmppStream? connect(Account account) { if (!connection_todo.contains(account)) connection_todo.add(account); - if (!stream_states.has_key(account)) { + if (!connections.has_key(account)) { return connect_(account); } else { check_reconnect(account); @@ -76,18 +104,18 @@ public class ConnectionManager { public void disconnect(Account account) { change_connection_state(account, ConnectionState.DISCONNECTED); connection_todo.remove(account); - if (stream_states.has_key(account)) { + if (connections.has_key(account)) { try { - stream_states[account].stream.disconnect(); + connections[account].stream.disconnect(); + connections.unset(account); } catch (Error e) { } } } private Core.XmppStream? connect_(Account account, string? resource = null) { + if (connections.has_key(account)) connections[account].stream.remove_modules(); + connection_errors.unset(account); if (resource == null) resource = account.resourcepart; - if (stream_states.has_key(account)) { - stream_states[account].stream.remove_modules(); - } Core.XmppStream stream = new Core.XmppStream(); foreach (Core.XmppStreamModule module in module_manager.get_modules(account, resource)) { @@ -96,11 +124,15 @@ public class ConnectionManager { stream.log = new Core.XmppLog(account.bare_jid.to_string(), log_options); Connection connection = new Connection(stream, new DateTime.now_local()); - stream_states[account] = connection; + connections[account] = connection; change_connection_state(account, ConnectionState.CONNECTING); stream.stream_negotiated.connect((stream) => { change_connection_state(account, ConnectionState.CONNECTED); }); + stream.get_module(PlainSasl.Module.IDENTITY).received_auth_failure.connect((stream, node) => { + set_connection_error(account, ConnectionError.Source.SASL, null); + change_connection_state(account, ConnectionState.DISCONNECTED); + }); new Thread (null, () => { try { stream.connect(account.domainpart); @@ -108,10 +140,13 @@ public class ConnectionManager { stderr.printf("Stream Error: %s\n", e.message); change_connection_state(account, ConnectionState.DISCONNECTED); if (!connection_todo.contains(account)) { - stream_states.unset(account); + connections.unset(account); } else { - interpret_reconnect_flags(account, stream.get_flag(StreamError.Flag.IDENTITY) ?? - new StreamError.Flag() { reconnection_recomendation = StreamError.Flag.Reconnect.NOW }); + StreamError.Flag? flag = stream.get_flag(StreamError.Flag.IDENTITY); + if (flag != null) { + set_connection_error(account, ConnectionError.Source.STREAM_ERROR, flag.error_type); + } + interpret_connection_error(account); } } return null; @@ -121,29 +156,33 @@ public class ConnectionManager { return stream; } - private void interpret_reconnect_flags(Account account, StreamError.Flag stream_error_flag) { - int wait_sec = 10; + private void interpret_connection_error(Account account) { + ConnectionError? error = connection_errors[account]; + int wait_sec = 5; + if (error == null) { + wait_sec = 3; + } else if (error.source == ConnectionError.Source.STREAM_ERROR && error.flag != null) { + if (error.flag.resource_rejected) { + connect_(account, account.resourcepart + "-" + random_uuid()); + return; + } + switch (error.flag.reconnection_recomendation) { + case StreamError.Flag.Reconnect.NOW: + wait_sec = 5; break; + case StreamError.Flag.Reconnect.LATER: + wait_sec = 60; break; + case StreamError.Flag.Reconnect.NEVER: + return; + } + } else if (error.source == ConnectionError.Source.SASL) { + return; + } if (network_manager != null && network_manager.State != NetworkManager.CONNECTED_GLOBAL) { wait_sec = 60; } - switch (stream_error_flag.reconnection_recomendation) { - case StreamError.Flag.Reconnect.NOW: - wait_sec = 10; - break; - case StreamError.Flag.Reconnect.LATER: - case StreamError.Flag.Reconnect.UNKNOWN: - wait_sec = 60; - break; - case StreamError.Flag.Reconnect.NEVER: - return; - } print(@"recovering in $wait_sec\n"); Timeout.add_seconds(wait_sec, () => { - if (stream_error_flag.resource_rejected) { - connect_(account, account.resourcepart + "-" + random_uuid()); - } else { - connect_(account); - } + connect_(account); return false; }); } @@ -156,16 +195,16 @@ public class ConnectionManager { private void check_reconnect(Account account) { PingResponseListenerImpl ping_response_listener = new PingResponseListenerImpl(this, account); - Core.XmppStream stream = stream_states[account].stream; + Core.XmppStream stream = connections[account].stream; stream.get_module(Xep.Ping.Module.IDENTITY).send_ping(stream, account.domainpart, ping_response_listener); Timeout.add_seconds(5, () => { - if (stream_states[account].stream != stream) return false; + if (connections[account].stream != stream) return false; if (ping_response_listener.acked) return false; change_connection_state(account, ConnectionState.DISCONNECTED); try { - stream_states[account].stream.disconnect(); + connections[account].stream.disconnect(); } catch (Error e) { } return false; }); @@ -207,8 +246,8 @@ public class ConnectionManager { Xmpp.Presence.Stanza presence = new Xmpp.Presence.Stanza(); presence.type_ = Xmpp.Presence.Stanza.TYPE_UNAVAILABLE; try { - stream_states[account].stream.get_module(Presence.Module.IDENTITY).send_presence(stream_states[account].stream, presence); - stream_states[account].stream.disconnect(); + connections[account].stream.get_module(Presence.Module.IDENTITY).send_presence(connections[account].stream, presence); + connections[account].stream.disconnect(); } catch (Error e) { print(@"on_prepare_for_sleep error $(e.message)\n"); } } } else { @@ -218,8 +257,16 @@ public class ConnectionManager { } private void change_connection_state(Account account, ConnectionState state) { - stream_states[account].connection_state = state; - connection_state_changed(account, state); + if (connections.has_key(account)) { + connections[account].connection_state = state; + connection_state_changed(account, state); + } + } + + private void set_connection_error(Account account, ConnectionError.Source source, string? id) { + ConnectionError error = new ConnectionError(source, id); + connection_errors[account] = error; + connection_error(account, error); } } diff --git a/libdino/src/service/module_manager.vala b/libdino/src/service/module_manager.vala index ab268876..9a510662 100644 --- a/libdino/src/service/module_manager.vala +++ b/libdino/src/service/module_manager.vala @@ -33,18 +33,11 @@ public class ModuleManager { public ArrayList get_modules(Account account, string? resource = null) { ArrayList modules = new ArrayList(); lock (module_map) { - if (!module_map.has_key(account)) { - initialize(account); - } - foreach (Core.XmppStreamModule module in module_map[account]) { - if (Bind.Module.IDENTITY.matches(module)) { - // TODO: argh?! - modules.add(new Bind.Module(resource == null ? account.resourcepart : resource)); - } else { - modules.add(module); - } - } + if (!module_map.has_key(account)) initialize(account); + foreach (Core.XmppStreamModule module in module_map[account]) modules.add(module); } + modules.add(new Bind.Module(resource == null ? account.resourcepart : resource)); + modules.add(new PlainSasl.Module(account.bare_jid.to_string(), account.password)); return modules; } @@ -52,8 +45,6 @@ public class ModuleManager { lock(module_map) { module_map[account] = new ArrayList(); module_map[account].add(new Tls.Module()); - module_map[account].add(new PlainSasl.Module(account.bare_jid.to_string(), account.password)); - module_map[account].add(new Bind.Module(account.resourcepart)); module_map[account].add(new Roster.Module()); module_map[account].add(new Xep.ServiceDiscovery.Module.with_identity("client", "pc")); module_map[account].add(new Xep.PrivateXmlStorage.Module()); diff --git a/main/data/manage_accounts/account_row.ui b/main/data/manage_accounts/account_row.ui index 154c7ae4..3887c51b 100644 --- a/main/data/manage_accounts/account_row.ui +++ b/main/data/manage_accounts/account_row.ui @@ -23,6 +23,11 @@ True + + + dialog-warning-symbolic + + diff --git a/main/data/manage_accounts/dialog.ui b/main/data/manage_accounts/dialog.ui index e431bfff..886382e7 100644 --- a/main/data/manage_accounts/dialog.ui +++ b/main/data/manage_accounts/dialog.ui @@ -130,6 +130,21 @@ 1 + + + 0 + True + + + + 1 + 1 + 1 + 1 + + True diff --git a/main/src/ui/manage_accounts/account_row.vala b/main/src/ui/manage_accounts/account_row.vala index 5e4570f0..f08faa78 100644 --- a/main/src/ui/manage_accounts/account_row.vala +++ b/main/src/ui/manage_accounts/account_row.vala @@ -9,13 +9,34 @@ public class AccountRow : Gtk.ListBoxRow { [GtkChild] public Image image; [GtkChild] public Label jid_label; + [GtkChild] public Image icon; public Account account; + private StreamInteractor stream_interactor; public AccountRow(StreamInteractor stream_interactor, Account account) { + this.stream_interactor = stream_interactor; this.account = account; Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(40, 40, image.scale_factor)).draw_account(stream_interactor, account)); jid_label.set_label(account.bare_jid.to_string()); + + stream_interactor.connection_manager.connection_error.connect((account, error) => { + Idle.add(() => { + if (account.equals(this.account)) update_warning_icon(); + return false; + }); + }); + stream_interactor.connection_manager.connection_state_changed.connect((account, state) => { + Idle.add(() => { + if (account.equals(this.account)) update_warning_icon(); + return false; + }); + }); + } + + private void update_warning_icon() { + ConnectionManager.ConnectionError? error = stream_interactor.connection_manager.get_error(account); + icon.visible = (error != null); } } diff --git a/main/src/ui/manage_accounts/dialog.vala b/main/src/ui/manage_accounts/dialog.vala index c0689474..e8697bad 100644 --- a/main/src/ui/manage_accounts/dialog.vala +++ b/main/src/ui/manage_accounts/dialog.vala @@ -21,6 +21,7 @@ public class Dialog : Gtk.Window { [GtkChild] public Image image; [GtkChild] public Button image_button; [GtkChild] public Label jid_label; + [GtkChild] public Label state_label; [GtkChild] public Switch active_switch; [GtkChild] public Stack password_stack; @@ -39,12 +40,14 @@ public class Dialog : Gtk.Window { private Database db; private StreamInteractor stream_interactor; + private Account? selected_account; construct { - account_list.row_selected.connect(account_list_row_selected); - add_button.clicked.connect(add_button_clicked); - no_accounts_add.clicked.connect(add_button_clicked); - remove_button.clicked.connect(remove_button_clicked); + Util.force_error_color(state_label, ".is_error"); + account_list.row_selected.connect(on_account_list_row_selected); + add_button.clicked.connect(on_add_button_clicked); + no_accounts_add.clicked.connect(on_add_button_clicked); + remove_button.clicked.connect(on_remove_button_clicked); password_entry.key_release_event.connect(on_password_key_release_event); alias_entry.key_release_event.connect(on_alias_key_release_event); image_button.clicked.connect(on_image_button_clicked); @@ -79,10 +82,23 @@ public class Dialog : Gtk.Window { } stream_interactor.get_module(AvatarManager.IDENTITY).received_avatar.connect((pixbuf, jid, account) => { - Idle.add(() => { - on_received_avatar(pixbuf, jid, account); - return false; - });}); + Idle.add(() => { + on_received_avatar(pixbuf, jid, account); + return false; + }); + }); + stream_interactor.connection_manager.connection_error.connect((account, error) => { + Idle.add(() => { + if (account.equals(selected_account)) update_status_label(account); + return false; + }); + }); + stream_interactor.connection_manager.connection_state_changed.connect((account, state) => { + Idle.add(() => { + if (account.equals(selected_account)) update_status_label(account); + return false; + }); + }); if (account_list.get_row_at_index(0) != null) account_list.select_row(account_list.get_row_at_index(0)); } @@ -94,7 +110,7 @@ public class Dialog : Gtk.Window { return account_item; } - private void add_button_clicked() { + private void on_add_button_clicked() { AddAccountDialog add_account_dialog = new AddAccountDialog(stream_interactor); add_account_dialog.set_transient_for(this); add_account_dialog.added.connect((account) => { @@ -106,7 +122,7 @@ public class Dialog : Gtk.Window { add_account_dialog.show(); } - private void remove_button_clicked() { + private void on_remove_button_clicked() { AccountRow account_item = account_list.get_selected_row() as AccountRow; if (account_item != null) { account_list.remove(account_item); @@ -121,40 +137,14 @@ public class Dialog : Gtk.Window { } } - private void account_list_row_selected(ListBoxRow? row) { + private void on_account_list_row_selected(ListBoxRow? row) { AccountRow? account_item = row as AccountRow; - if (account_item != null) populate_grid_data(account_item.account); - } - - private void populate_grid_data(Account account) { - active_switch.state_set.disconnect(on_active_switch_state_changed); - - Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(50, 50, image.scale_factor)).draw_account(stream_interactor, account)); - active_switch.set_active(account.enabled); - jid_label.label = account.bare_jid.to_string(); - - string filler = ""; - for (int i = 0; i < account.password.length; i++) filler += password_entry.get_invisible_char().to_string(); - password_label.label = filler; - password_stack.set_visible_child_name("label"); - password_entry.text = account.password; - - alias_stack.set_visible_child_name("label"); - alias_label.label = account.alias; - alias_entry.text = account.alias; - - password_button.clicked.connect(() => { set_active_stack(password_stack); }); - alias_button.clicked.connect(() => { set_active_stack(alias_stack); }); - active_switch.state_set.connect(on_active_switch_state_changed); - - foreach(Plugins.AccountSettingsWidget widget in plugin_widgets) { - widget.set_account(account); + if (account_item != null) { + selected_account = account_item.account; + populate_grid_data(account_item.account); } - - child_activated(null); } - private void on_image_button_clicked() { FileChooserDialog chooser = new FileChooserDialog ( "Select avatar", this, FileChooserAction.OPEN, @@ -175,6 +165,7 @@ public class Dialog : Gtk.Window { Account account = (account_list.get_selected_row() as AccountRow).account; account.enabled = state; if (state) { + if (account.enabled) account_disabled(account); account_enabled(account); } else { account_disabled(account); @@ -211,6 +202,62 @@ public class Dialog : Gtk.Window { } } + private void populate_grid_data(Account account) { + active_switch.state_set.disconnect(on_active_switch_state_changed); + + Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(50, 50, image.scale_factor)).draw_account(stream_interactor, account)); + active_switch.set_active(account.enabled); + jid_label.label = account.bare_jid.to_string(); + + string filler = ""; + for (int i = 0; i < account.password.length; i++) filler += password_entry.get_invisible_char().to_string(); + password_label.label = filler; + password_stack.set_visible_child_name("label"); + password_entry.text = account.password; + + alias_stack.set_visible_child_name("label"); + alias_label.label = account.alias; + alias_entry.text = account.alias; + + update_status_label(account); + + password_button.clicked.connect(() => { set_active_stack(password_stack); }); + alias_button.clicked.connect(() => { set_active_stack(alias_stack); }); + active_switch.state_set.connect(on_active_switch_state_changed); + + foreach(Plugins.AccountSettingsWidget widget in plugin_widgets) { + widget.set_account(account); + } + + child_activated(null); + } + + private void update_status_label(Account account) { + state_label.label = ""; + ConnectionManager.ConnectionError? error = stream_interactor.connection_manager.get_error(account); + if (error != null) { + state_label.label = get_connection_error_description(error); + state_label.get_style_context().add_class("is_error"); + + if (error.source == ConnectionManager.ConnectionError.Source.SASL || + (error.flag != null && error.flag.reconnection_recomendation == Xmpp.StreamError.Flag.Reconnect.NEVER)) { + active_switch.active = false; + } + + } else { + ConnectionManager.ConnectionState state = stream_interactor.connection_manager.get_state(account); + switch (state) { + case ConnectionManager.ConnectionState.CONNECTING: + state_label.label = "Connecting..."; break; + case ConnectionManager.ConnectionState.CONNECTED: + state_label.label = "Connected"; break; + case ConnectionManager.ConnectionState.DISCONNECTED: + state_label.label = "Disconnected"; break; + } + state_label.get_style_context().remove_class("is_error"); + } + } + private void child_activated(Gtk.Widget? widget) { if (widget != password_stack) password_stack.set_visible_child_name("label"); if (widget != alias_stack) alias_stack.set_visible_child_name("label"); @@ -224,6 +271,18 @@ public class Dialog : Gtk.Window { stack.set_visible_child_name("entry"); child_activated(stack); } + + private string get_connection_error_description(ConnectionManager.ConnectionError error) { + switch (error.source) { + case ConnectionManager.ConnectionError.Source.SASL: + return "Wrong password"; + } + if (error.identifier != null) { + return "Error" + ": " + error.identifier; + } else { + return "Error"; + } + } } } diff --git a/main/src/ui/util.vala b/main/src/ui/util.vala index 993a996f..4df785c0 100644 --- a/main/src/ui/util.vala +++ b/main/src/ui/util.vala @@ -82,7 +82,6 @@ public class Util : Object { private const string force_background_css = "%s { background-color: %s; }"; private const string force_color_css = "%s { color: %s; }"; - private static void force_css(Gtk.Widget widget, string css) { var p = new Gtk.CssProvider(); try { diff --git a/plugins/openpgp/src/account_settings_widget.vala b/plugins/openpgp/src/account_settings_widget.vala index 5eb0ba16..026d7d3c 100644 --- a/plugins/openpgp/src/account_settings_widget.vala +++ b/plugins/openpgp/src/account_settings_widget.vala @@ -80,7 +80,7 @@ private class AccountSettingsWidget : Stack, Plugins.AccountSettingsWidget { list_store.set(iter, 0, build_markup_string("Key publishing disabled", "Select key"), 1, ""); for (int i = 0; i < keys.size; i++) { list_store.append(out iter); - list_store.set(iter, 0, build_markup_string(keys[i].uids[0].uid, "0x" + keys[i].fpr[0:16])); + list_store.set(iter, 0, @"$(Markup.escape_text(keys[i].uids[0].uid))\n0x$(Markup.escape_text(keys[i].fpr[0:16]))"); 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); @@ -95,7 +95,7 @@ private class AccountSettingsWidget : Stack, Plugins.AccountSettingsWidget { list_store.clear(); list_store.append(out iter); label.set_markup(build_markup_string("Loading...", "Querying GnuPG")); - new Thread (null, () => { // Querying GnuPG might take some while + new Thread (null, () => { // Querying GnuPG might take some time try { keys = GPGHelper.get_keylist(null, true); Idle.add(() => {