[polari/wip/bastianilso/error-handling: 1/2] errorNotification



commit 3fe4e970ebd7a93aadc44cf84cf685ca17f7e5e0
Author: Bastian Ilsø <bastianilso src gnome org>
Date:   Tue Jul 28 14:07:41 2015 +0200

    errorNotification

 src/appNotifications.js |  239 ++++++++++++++++++++++++++++++++++++++++++++---
 src/application.js      |  113 ++++++++++++++++++----
 src/mainWindow.js       |    7 ++
 3 files changed, 328 insertions(+), 31 deletions(-)
---
diff --git a/src/appNotifications.js b/src/appNotifications.js
index 24d6009..0d42e07 100644
--- a/src/appNotifications.js
+++ b/src/appNotifications.js
@@ -1,17 +1,21 @@
 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',
     Abstract: true,
 
     _init: function() {
-        this.widget = new Gtk.Revealer({ reveal_child: true });
+        this.widget = new Gtk.Revealer({ reveal_child: false });
         this.widget.transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN;
 
         this.widget.connect('notify::child-revealed',
@@ -23,9 +27,20 @@ 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,42 +104,242 @@ const GridOutput = new Lang.Class({
     }
 });
 
+const ErrorNotification = new Lang.Class({
+    Name: 'ErrorNotification',
+    Extends: AppNotification,
+
+    _init: function(requestData) {
+        this.parent();
+        this._grid = new Gtk.Grid({ orientation: Gtk.Orientation.HORIZONTAL,
+                                    column_spacing: 12 });
+        this._app = Gio.Application.get_default();
+        this._roomId = requestData.roomId;
+        this._grid.add(new Gtk.Image({icon_name: 'dialog-error-symbolic' }));
+        this._account = requestData.account;
+        this._window = requestData.window;
+        this._button = new Gtk.Button({ valign: Gtk.Align.CENTER, hexpand: true,
+                                        halign: Gtk.Align.END });
+        this._label = new Gtk.Label({ max_width_chars: 30, wrap: true });
+        this._populateNotification(requestData.error);
+        this._grid.add(this._label);
+        this._grid.add(this._button);
+        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)) {
+            // Network Error is given
+            // - when the server adress is misspelled
+            // - inconsistenly when turning your internet on and off
+            // - if you are on an internet connection and the DNS is down
+            // - when you try to connect to a port that only supports SSL, but didn't check off "use-ssl" 
(fx by specifying a wrong port number).
+            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._editAccountAction));
+        } 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._reconnectAccountAction));
+        } 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._reconnectAccountAction));
+        } 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._joinRoomAction));
+        } 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._joinRoomAction));
+        } 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._joinRoomAction));
+        } 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)) {
+            // Not a fan of this label, but it's the best i've got. Don't want to expose words like 
"certificate", "encryption" and "wobblegobble"
+            // If we do automatic SSL it might come as a surprise to the user who might not even worry about 
it..
+            // "The connection to %s is public." <- indicating that everyone can potentially read what you 
write? :S
+            // Show "Unlocked" symbol instead of error icon? or show world icon.
+            // "Everyone can see what you read and write on this connection." Subtitle?
+            this._label.label = _("The connection to %s is not safe.").format(this._account.display_name);
+            // More like a "For this connection it's okay." or "For this connection I don't care" button.
+            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._joinRoomAction));
+        }
+    },
+
+    _editAccountAction: 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._reconnectAccountAction();
+            }));
+    },
+
+    _reconnectAccountAction: function() {
+        // Not sure we actually need this..
+        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._reconnectAccountAction();
+            }));
+    },
+
+    _joinRoomAction: 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 ]));
+        this.close();
+    },
+});
+
 const NotificationQueue = new Lang.Class({
     Name: 'NotificationQueue',
 
     _init: function() {
         this.widget = new Gtk.Frame({ valign: Gtk.Align.START,
                                       halign: Gtk.Align.CENTER,
-                                      no_show_all: true });
+                                      no_show_all: false });
         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._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 7262bfd..c084dba 100644
--- a/src/application.js
+++ b/src/application.js
@@ -39,6 +39,8 @@ const Application = new Lang.Class({
         let w = new Polari.FixedSizeFrame(); // register gtype
         w.destroy();
 
+        this._rooms = {};
+
         this._chatroomManager = ChatroomManager.getDefault();
         this._accountsMonitor = AccountsMonitor.getDefault();
 
@@ -46,7 +48,8 @@ const Application = new Lang.Class({
             function(am, account) {
                 this._removeSavedChannelsForAccount(account);
             }));
-
+        this._accountsMonitor.connect('account-status-changed',
+                                      Lang.bind(this, this._onAccountChanged));
         this._settings = new Gio.Settings({ schema_id: 'org.gnome.Polari' });
 
         this.pasteManager = new PasteManager.PasteManager();
@@ -77,6 +80,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 +197,21 @@ const Application = new Lang.Class({
         this._window.showMessageUserDialog();
     },
 
+    _onAccountChanged: function(am, account) {
+        if (account.connection_status != Tp.ConnectionStatus.CONNECTED)
+            return;
+
+        // Some thirdparty might be trying to reconnect the account
+        // We can safely assume that there are no account errors when
+        // we successfully connected the account to the server.
+
+        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 +262,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 +294,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 +306,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 +316,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);
@@ -284,22 +330,14 @@ const Application = new Lang.Class({
                                            this._onEnsureChannel, requestData));
     },
 
-    _retryRequest: function(requestData) {
-        let account = requestData.account;
-
-        // Try again with a different nick
-        let params = account.dup_parameters_vardict().deep_unpack();
-        let oldNick = params['account'].deep_unpack();
-        let nick = oldNick + '_';
-        this._updateAccountName(account, nick, Lang.bind(this,
-            function() {
-                this._ensureChannel(requestData);
-            }));
-    },
-
     _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);
 
@@ -307,18 +345,48 @@ 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 an 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;
+            log('dbus error: ' + account.connection_error);
+            //let error = Tp.error_get_dbus_name(Tp.Error.CERT_NOT_ACTIVATED);
             if (error == ConnectionError.ALREADY_CONNECTED &&
                 requestData.retry++ < MAX_RETRIES) {
-                    this._retryRequest(requestData);
+                    let params = account.dup_parameters_vardict().deep_unpack();
+                    let oldNick = params['account'].deep_unpack();
+                    let nick = oldNick + '_';
+                    this._updateAccountName(account, nick, Lang.bind(this,
+                        function() {
+                            this._ensureChannel(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);
+                /*notification.widget.connect('destroy',
+                    Lang.bind(this, function() {
+                        this._ensureChannel(requestData);
+                    }));*/
+                accountError.notification = notification;
+            }
         } catch (e if e.matches(Tp.Error, Tp.Error.CANCELLED)) {
             // interrupted by user request, don't log
         } catch (e) {
             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);
         }
 
         if (requestData.retry > 0)
@@ -332,6 +400,13 @@ const Application = new Lang.Class({
                              channelName, time);
     },
 
+    _onReconnectAccount: function(action, parameter) {
+        let factory = Tp.AccountManager.dup().get_factory();
+        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 2443d10..adc9bc6 100644
--- a/src/mainWindow.js
+++ b/src/mainWindow.js
@@ -158,6 +158,13 @@ const MainWindow = new Lang.Class({
         this._membersChangedId =
             this._room.connect('members-changed',
                                Lang.bind(this, this._updateUserListLabel));
+        let app = Gio.Application.get_default();
+        log('**********')
+        log('loading archived notifications for: ' + room.account.get_path_suffix());
+        app.notificationQueue.loadNotifications(room.account.get_object_path(), 'account');
+        log('**')
+        log('loading archived notifications for: ' + room.id);
+        app.notificationQueue.loadNotifications(room.id, 'room');
     },
 
     _createWidget: function(app) {


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