[polari/wip/bastianilso/error-handling: 2/4] Handle errors from ensuring channels.



commit c47366a4c4691894445eb0faabcf09ae0eb783d1
Author: Bastian Ilsø <bastianilso src gnome org>
Date:   Sun Aug 2 17:50:29 2015 +0200

    Handle errors from ensuring channels.
    
    Currently errors we encounter while ensuring channels
    are logged in the terminal. This patch adds a more user-
    friendly way of dealing with errors using error notifications
    with actions which the user may use to solve the error.

 src/accountsMonitor.js  |    3 +-
 src/appNotifications.js |  228 ++++++++++++++++++++++++++++++++++++++++++++--
 src/application.js      |   85 +++++++++++++++++-
 src/mainWindow.js       |    4 +
 4 files changed, 304 insertions(+), 16 deletions(-)
---
diff --git a/src/accountsMonitor.js b/src/accountsMonitor.js
index b692d1a..75ff7d1 100644
--- a/src/accountsMonitor.js
+++ b/src/accountsMonitor.js
@@ -66,7 +66,6 @@ const AccountsMonitor = new Lang.Class({
                    Lang.bind(this, this._accountEnabledChanged));
         am.connect('account-disabled',
                    Lang.bind(this, this._accountEnabledChanged));
-
         this.emit('account-manager-prepared', am);
     },
 
@@ -125,6 +124,8 @@ const AccountsMonitor = new Lang.Class({
     _accountEnabledChanged: function(am, account) {
         if (this._accounts.indexOf(account) == -1)
             return;
+        if (!account.enabled)
+            this.emit('account-disabled', account);
         this.emit('accounts-changed');
     }
 });
diff --git a/src/appNotifications.js b/src/appNotifications.js
index 24d6009..f82f1c8 100644
--- a/src/appNotifications.js
+++ b/src/appNotifications.js
@@ -1,10 +1,14 @@
 const Gtk = imports.gi.Gtk;
 const Tp = imports.gi.TelepathyGLib;
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
 
 const Lang = imports.lang;
 const Mainloop = imports.mainloop;
+const Connections = imports.connections;
 
 const COMMAND_OUTPUT_REVEAL_TIME = 3;
+const TP_CURRENT_TIME = GLib.MAXUINT32;
 
 const AppNotification = new Lang.Class({
     Name: 'AppNotification',
@@ -23,9 +27,19 @@ const AppNotification = new Lang.Class({
     },
 
     _onChildRevealed: function() {
-        if (!this.widget.child_revealed)
+        if (!this.widget.child_revealed) {
             this.widget.destroy();
-    }
+            this.widget = null;
+        }
+    },
+
+    open: function() {
+        this.widget.reveal_child = true;
+    },
+
+    get statusReason() {
+        return this._statusReason;
+    },
 });
 
 const CommandOutputNotification = new Lang.Class({
@@ -89,6 +103,130 @@ const GridOutput = new Lang.Class({
     }
 });
 
+const ErrorNotification = new Lang.Class({
+    Name: 'ErrorNotification',
+    Extends: AppNotification,
+
+    _init: function(requestData) {
+        this.parent();
+
+        this._app = Gio.Application.get_default();
+        this._roomId = requestData.roomId;
+        this._account = requestData.account;
+        this._window = requestData.window;
+
+        this._grid = new Gtk.Grid({ orientation: Gtk.Orientation.HORIZONTAL,
+                                    column_spacing: 12 });
+        this._grid.add(new Gtk.Image({icon_name: 'dialog-error-symbolic' }));
+
+        this._label = new Gtk.Label({ max_width_chars: 30, wrap: true });
+        this._grid.add(this._label);
+
+        this._button = new Gtk.Button({ valign: Gtk.Align.CENTER, hexpand: true,
+                                        halign: Gtk.Align.END });
+        this._grid.add(this._button);
+
+        this._populateNotification(requestData.error);
+
+        this.widget.add(this._grid);
+        this.widget.show_all();
+
+        this._statusReason = requestData.error;
+    },
+
+    _populateNotification: function(error) {
+        if (error == Tp.error_get_dbus_name(Tp.Error.NETWORK_ERROR)) {
+            this._label.label = _("Unable to connect to %s").format(this._account.display_name);
+            this._button.label =  _("Edit Account");
+            this._button.connect('clicked', Lang.bind(this, this._editAccount));
+        } else if (error == Tp.error_get_dbus_name(Tp.Error.AUTHENTICATION_FAILED)) {
+            this._label.label = _("Authentication failed for %s.").format(this._account.display_name);
+            this._button.label =  _("Retry");
+            this._button.connect('clicked', Lang.bind(this, this._reconnectAccount));
+        } else if (error == Tp.error_get_dbus_name(Tp.Error.SERVICE_BUSY)) {
+            this._label.label = _("%s is too busy at the moment.").format(this._account.display_name);
+            this._button.label =  _("Retry");
+            this._button.connect('clicked', Lang.bind(this, this._reconnectAccount));
+        } else if (error == Tp.error_get_dbus_name(Tp.Error.CHANNEL_BANNED)) {
+            this._label.label = _("You are banned from this room.");
+            this._button.label = _("Retry");
+            this._button.connect('clicked', Lang.bind(this, this._joinRoom));
+        } else if (error == Tp.error_get_dbus_name(Tp.Error.CHANNEL_FULL)) {
+            this._label.label = _("The room is full.");
+            this._button.label = _("Retry");
+            this._button.connect('clicked', Lang.bind(this, this._joinRoom));
+        } else if (error == Tp.error_get_dbus_name(Tp.Error.CHANNEL_INVITE_ONLY)) {
+            this._label.label = _("The room is invite-only.");
+            this._button.label = _("Retry");
+            this._button.connect('clicked', Lang.bind(this, this._joinRoom));
+        } else if (error == Tp.error_get_dbus_name(Tp.Error.ENCRYPTION_ERROR)
+                || error == Tp.error_get_dbus_name(Tp.Error.CERT_NOT_PROVIDED)
+                || error == Tp.error_get_dbus_name(Tp.Error.CERT_UNTRUSTED)
+                || error == Tp.error_get_dbus_name(Tp.Error.CERT_EXPIRED)
+                || error == Tp.error_get_dbus_name(Tp.Error.CERT_NOT_ACTIVATED)
+                || error == Tp.error_get_dbus_name(Tp.Error.CERT_HOSTNAME_MISMATCH)
+                || error == Tp.error_get_dbus_name(Tp.Error.CERT_FINGERPRINT_MISMATCH)
+                || error == Tp.error_get_dbus_name(Tp.Error.CERT_SELF_SIGNED)
+                || error == Tp.error_get_dbus_name(Tp.Error.ENCRYPTION_NOT_AVAILABLE)
+                || error == Tp.error_get_dbus_name(Tp.Error.CERT_INVALID)
+                || error == Tp.error_get_dbus_name(Tp.Error.CERT_REVOKED)
+                || error == Tp.error_get_dbus_name(Tp.Error.CERT_INSECURE)
+                || error == Tp.error_get_dbus_name(Tp.Error.CERT_LIMIT_EXCEEDED)) {
+            this._label.label = _("The connection to %s is not safe.").format(this._account.display_name);
+            this._button.label =  _("Continue Anyway");
+            this._button.connect('clicked', Lang.bind(this, this._reconnectNoEncryption));
+        } else {
+            log('no match for: ' + error);
+            this._label.label = _("Failed to connect for an unknown reason.");
+            this._button.label = _("Retry");
+            this._button.connect('clicked', Lang.bind(this, this._joinRoom));
+        }
+    },
+
+    _editAccount: function(button) {
+        let dialog = new Connections.ConnectionDetailsDialog(this._account);
+        dialog.widget.transient_for = this._window;
+        dialog.widget.show();
+        dialog.widget.connect('response', Lang.bind(this,
+            function(w, response) {
+                dialog.widget.destroy();
+
+                if (response != Gtk.ResponseType.OK)
+                    return;
+
+                this._reconnectAccount();
+            }));
+    },
+
+    _reconnectAccount: function() {
+        let action = this._app.lookup_action('reconnect-account');
+        let accountPath = GLib.Variant.new('o', this._account.get_object_path());
+        action.activate(accountPath);
+    },
+
+    _reconnectNoEncryption: function() {
+        let sv = { port: GLib.Variant.new('u', 6667) };
+        sv['use-ssl'] = GLib.Variant.new('b', false);
+        let asv = GLib.Variant.new('a{sv}', sv);
+        this._account.update_parameters_vardict_async(asv, [],
+            Lang.bind(this, function(a, res) {
+                a.update_parameters_vardict_finish(res);
+                this._reconnectAccount();
+            }));
+    },
+
+    _joinRoom: function() {
+        let action = this._app.lookup_action('join-room');
+        let regex = /(?:#|&)(.+)/g;
+        let room = this._roomId.match(regex);
+        if (room)
+            action.activate(GLib.Variant.new('(ssu)',
+                                            [ this._account.get_object_path(),
+                                              room[0],
+                                              TP_CURRENT_TIME ]));
+    },
+});
+
 const NotificationQueue = new Lang.Class({
     Name: 'NotificationQueue',
 
@@ -101,30 +239,100 @@ const NotificationQueue = new Lang.Class({
         this._grid = new Gtk.Grid({ orientation: Gtk.Orientation.VERTICAL,
                                     row_spacing: 6, visible: true });
         this.widget.add(this._grid);
+        this._notifications = {};
+        this._activeNotifications = {};
+        let initParams = {};
+        this._activeNotifications['room'] = initParams;
+        this._activeNotifications['account'] = initParams;
+        this._updateVisibility();
     },
 
-    addNotification: function(notification) {
-        this._grid.add(notification.widget);
+    setNotification: function(params) {
+        this._notifications[params.identifier] = params;
 
-        notification.widget.connect('destroy',
-                                    Lang.bind(this, this._onChildDestroy));
-        this.widget.show();
+        if (this._activeNotifications[params.type].identifier != params.identifier ||
+            this._activeNotifications[params.type].notification == params.notification)
+            return;
+
+        this._displayNotification(params);
     },
 
-    _onChildDestroy: function() {
+    loadNotifications: function(identifier, type) {
+        if (this._activeNotifications[type] == this._notifications[identifier])
+            return;
+
+        let notification = this._notifications[identifier] ?
+                           this._notifications[identifier].notification
+                           : null;
+
+        let params = { notification: notification, type: type,
+                       identifier: identifier };
+
+        this._displayNotification(params);
+    },
+
+    _displayNotification: function(params) {
+        let isNotification = this._activeNotifications[params.type].notification &&
+                             this._activeNotifications[params.type].notification.widget;
+
+        if (isNotification) {
+            if (!params.notification && params.identifier == 
this._activeNotifications[params.type].identifier)
+                this._activeNotifications[params.type].notification.close();
+            else
+                this._grid.remove(this._activeNotifications[params.type].notification.widget);
+        }
+
+        if (params.notification) {
+            if (isNotification)
+                params.notification.widget.transition_type = Gtk.RevealerTransitionType.NONE;
+
+            this._grid.add(params.notification.widget);
+            params.notification.open();
+            params.notification.widget.transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN;
+            params.notification.widget.connect('destroy',
+                                        Lang.bind(this, this._updateVisibility));
+        }
+
+        this._activeNotifications[params.type] = params;
+        this._updateVisibility();
+    },
+
+    _updateVisibility: function() {
         if (this._grid.get_children().length == 0)
            this.widget.hide();
+        else
+           this.widget.show();
     }
 });
 
 const CommandOutputQueue = new Lang.Class({
     Name: 'CommandOutputQueue',
-    Extends: NotificationQueue,
 
     _init: function() {
-        this.parent();
+        this.widget = new Gtk.Frame({ valign: Gtk.Align.START,
+                                      halign: Gtk.Align.CENTER,
+                                      no_show_all: true });
+        this.widget.get_style_context().add_class('app-notification');
+
+        this._grid = new Gtk.Grid({ orientation: Gtk.Orientation.VERTICAL,
+                                    row_spacing: 6, visible: true });
+        this.widget.add(this._grid);
 
         this.widget.valign = Gtk.Align.END;
         this.widget.get_style_context().add_class('irc-feedback');
+    },
+
+    addNotification: function(notification) {
+        this._grid.add(notification.widget);
+
+        notification.widget.connect('destroy',
+                                    Lang.bind(this, this._onChildDestroy));
+        this.widget.show();
+        notification.open();
+    },
+
+    _onChildDestroy: function() {
+        if (this._grid.get_children().length == 0)
+           this.widget.hide();
     }
 });
diff --git a/src/application.js b/src/application.js
index 673d664..6947402 100644
--- a/src/application.js
+++ b/src/application.js
@@ -13,7 +13,6 @@ const MainWindow = imports.mainWindow;
 const PasteManager = imports.pasteManager;
 const Utils = imports.utils;
 
-
 const MAX_RETRIES = 3;
 
 const ConnectionError = {
@@ -39,12 +38,25 @@ const Application = new Lang.Class({
         let w = new Polari.FixedSizeFrame(); // register gtype
         w.destroy();
 
+        this._rooms = {};
+
         this._chatroomManager = ChatroomManager.getDefault();
         this._accountsMonitor = AccountsMonitor.getDefault();
 
         this._accountsMonitor.connect('account-removed', Lang.bind(this,
             function(am, account) {
                 this._removeSavedChannelsForAccount(account);
+                this._clearAccountNotification(account);
+            }));
+        this._accountsMonitor.connect('account-status-changed', Lang.bind(this,
+            function(am, account) {
+                if (account.connection_status != Tp.ConnectionStatus.CONNECTED)
+                    return;
+                this._clearAccountNotification(account);
+            }));
+        this._accountsMonitor.connect('account-disabled', Lang.bind(this,
+            function(am, account) {
+                this._clearAccountNotification(account);
             }));
 
         this._settings = new Gio.Settings({ schema_id: 'org.gnome.Polari' });
@@ -77,6 +89,9 @@ const Application = new Lang.Class({
             activate: Lang.bind(this, this._onLeaveCurrentRoom),
             create_hook: Lang.bind(this, this._leaveRoomCreateHook),
             accels: ['<Primary>w'] },
+          { name: 'reconnect-account',
+            activate: Lang.bind(this, this._onReconnectAccount),
+            parameter_type: GLib.VariantType.new('o') },
           { name: 'user-list',
             activate: Lang.bind(this, this._onToggleAction),
             create_hook: Lang.bind(this, this._userListCreateHook),
@@ -191,6 +206,13 @@ const Application = new Lang.Class({
         this._window.showMessageUserDialog();
     },
 
+    _clearAccountNotification: function(account) {
+        let accountError = { notification: null,
+                             type: 'account',
+                             identifier: account.get_object_path() };
+        this.notificationQueue.setNotification(accountError);
+    },
+
     _addSavedChannel: function(account, channel) {
         let savedChannels = this._settings.get_value('saved-channel-list').deep_unpack();
         let savedChannel = {
@@ -241,6 +263,24 @@ const Application = new Lang.Class({
         account.update_parameters_vardict_async(asv, [], callback);
     },
 
+    _reconnectAccount: function(accountPath) {
+        // Request online presence inside accountsMonitor, when it becomes
+        // enabled and when we initialize accounts.
+
+        let factory = Tp.AccountManager.dup().get_factory();
+        let account = factory.ensure_account(accountPath, []);
+
+        let accountError = { notification: null,
+                             type: 'account',
+                             identifier: accountPath };
+        this.notificationQueue.setNotification(accountError);
+
+        let keys = Object.keys(this._rooms[accountPath]);
+        keys.forEach(Lang.bind(this, function(roomId) {
+            this._ensureChannel(this._rooms[accountPath][roomId]);
+            }));
+    },
+
     _requestChannel: function(accountPath, targetType, targetId, time) {
         // have this in AccountMonitor?
         let factory = Tp.AccountManager.dup().get_factory();
@@ -255,7 +295,7 @@ const Application = new Lang.Class({
             return;
         }
 
-        let roomId = Polari.create_room_id(account,  targetId, targetType);
+        let roomId = Polari.create_room_id(account, targetId, targetType);
 
         let requestData = {
           account: account,
@@ -267,7 +307,9 @@ const Application = new Lang.Class({
           retry: 0,
           originalNick: account.nickname };
 
-        this._pendingRequests[roomId] = requestData;
+        if (!this._rooms[accountPath])
+            this._rooms[accountPath] = {};
+        this._rooms[accountPath][roomId] = requestData;
 
         this._ensureChannel(requestData);
     },
@@ -275,6 +317,11 @@ const Application = new Lang.Class({
     _ensureChannel: function(requestData) {
         let account = requestData.account;
 
+        let roomError = { notification: null,
+                          type: 'room',
+                          identifier: requestData.roomId };
+        this.notificationQueue.setNotification(roomError);
+
         let req = Tp.AccountChannelRequest.new_text(account, requestData.time);
         req.set_target_id(requestData.targetHandleType, requestData.targetId);
         req.set_delegate_to_preferred_handler(true);
@@ -301,7 +348,12 @@ const Application = new Lang.Class({
 
     _onEnsureChannel: function(req, res, requestData) {
         let account = req.account;
-
+        let roomError = { notification: null,
+                          type: 'room',
+                          identifier: requestData.roomId };
+        let accountError = { notification: null,
+                             type: 'account',
+                             identifier: account.get_object_path() };
         try {
             req.ensure_channel_finish(res);
 
@@ -309,20 +361,38 @@ const Application = new Lang.Class({
                 this._addSavedChannel(account, requestData.targetId);
         } catch (e if e.matches(Tp.Error, Tp.Error.DISCONNECTED)) {
             let error = account.connection_error;
+            // If we receive a disconnect error and the network is unavailable,
+            // then the error is not specific to polari and polari will
+            // just be in offline state.
+            let networkMonitor = Gio.NetworkMonitor.get_default();
+            if (!networkMonitor.network_available)
+                return;
             if (error == ConnectionError.ALREADY_CONNECTED &&
                 requestData.retry++ < MAX_RETRIES) {
                     this._retryRequest(requestData);
                     return;
             }
 
-            if (error && error != ConnectionError.CANCELLED)
+            if (error && error != ConnectionError.CANCELLED) {
                 logError(e);
+                requestData.error = error;
+                requestData.window = this._window.window;
+                let notification = new AppNotifications.ErrorNotification(requestData);
+                accountError.notification = notification;
+            }
         } catch (e if e.matches(Tp.Error, Tp.Error.CANCELLED)) {
             // interrupted by user request, don't log
         } catch (e) {
             requestData.status = 'disconnected';
             this.emitJS('room-status-changed', requestData);
             logError(e, 'Failed to ensure channel');
+            let error = Tp.error_get_dbus_name(e.code);
+            requestData.error = error;
+            requestData.window = this._window.window;
+            roomError.notification = new AppNotifications.ErrorNotification(requestData);
+        } finally {
+            this.notificationQueue.setNotification(roomError);
+            this.notificationQueue.setNotification(accountError);
         }
         requestData.status = 'connected';
         this.emitJS('room-status-changed', requestData);
@@ -338,6 +408,11 @@ const Application = new Lang.Class({
                              channelName, time);
     },
 
+    _onReconnectAccount: function(action, parameter) {
+        let accountPath = parameter.unpack();
+        this._reconnectAccount(accountPath);
+    },
+
     _onMessageUser: function(action, parameter) {
         let [accountPath, contactName, time] = parameter.deep_unpack();
         this._requestChannel(accountPath, Tp.HandleType.CONTACT,
diff --git a/src/mainWindow.js b/src/mainWindow.js
index a92358f..c138252 100644
--- a/src/mainWindow.js
+++ b/src/mainWindow.js
@@ -156,6 +156,10 @@ const MainWindow = new Lang.Class({
         this._membersChangedId =
             this._room.connect('members-changed',
                                Lang.bind(this, this._updateUserListLabel));
+
+        let app = Gio.Application.get_default();
+        app.notificationQueue.loadNotifications(room.account.get_object_path(), 'account');
+        app.notificationQueue.loadNotifications(room.id, 'room');
     },
 
     _createWidget: function(app) {



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]