[polari/wip/bastianilso/error-handling: 6/10] errorNotification



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

    errorNotification

 src/appNotifications.js |  282 ++++++++++++++++++++++++++++++++++++++++++++---
 src/application.js      |   94 +++++++++++++---
 src/mainWindow.js       |    7 +
 3 files changed, 348 insertions(+), 35 deletions(-)
---
diff --git a/src/appNotifications.js b/src/appNotifications.js
index 24d6009..b047031 100644
--- a/src/appNotifications.js
+++ b/src/appNotifications.js
@@ -1,31 +1,38 @@
 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',
-                            Lang.bind(this, this._onChildRevealed));
     },
 
     close: function() {
         this.widget.reveal_child = false;
+        this.widget.destroy();
+        this.widget = null;
     },
 
-    _onChildRevealed: function() {
-        if (!this.widget.child_revealed)
-            this.widget.destroy();
-    }
+    open: function() {
+        this.widget.reveal_child = true;
+    },
+
+
+    get statusReason() {
+        return this._statusReason;
+    },
 });
 
 const CommandOutputNotification = new Lang.Class({
@@ -89,42 +96,283 @@ const GridOutput = new Lang.Class({
     }
 });
 
+const ErrorNotification = new Lang.Class({
+    Name: 'ErrorNotification',
+    Extends: AppNotification,
+
+    _init: function(requestData) {
+        this.parent();
+        //log('creating error notification..');
+        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 });
+        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
+            // - when you disconnect while connecting and reconnect (then you disconnect temporarily again 
with this error and reconnect shortly after)
+            // - 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 "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();
+                this.close();
+            }));
+    },
+
+    _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);
+        this.close();
+    },
+
+    _reconnectNoEncryption: function() {
+        let oldDetails = this._account.dup_parameters_vardict().deep_unpack();
+        log(oldDetails);
+        let details = { account: GLib.Variant.new('s', oldDetails.nickname),
+                        server:  GLib.Variant.new('s', oldDetails.server) };
+
+        if (oldDetails.fullname)
+            details.fullname = GLib.Variant.new('s', oldDetails.fullname);
+
+        details.port = GLib.Variant.new('u', 6667);
+        details['use-ssl'] = GLib.Variant.new('b', false);
+
+        let removed = Object.keys(oldDetails).filter(
+                function(p) {
+                    return !details.hasOwnProperty(p);
+                });
+
+        let vardict = GLib.Variant.new('a{sv}', details);
+        this._account.update_parameters_vardict_async(vardict, removed,
+            Lang.bind(this, function(a, res) {
+                a.update_parameters_vardict_finish(res); // TODO: Check for errors
+                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;
+        //log('checking if ' + params.identifier + ' is active..');
+        //log('this._activeNotifications[' + params.type + '].identifier: ' + 
this._activeNotifications[params.type].identifier)
+        if (this._activeNotifications[params.type].identifier != params.identifier)
+            return;
+        if (this._activeNotifications[params.type].notification == params.notification)
+            return;
+        //log('..true. calling displayNotification.');
+        this._displayNotification(params);
+    },
 
-        notification.widget.connect('destroy',
-                                    Lang.bind(this, this._onChildDestroy));
+    loadNotifications: function(identifier, type) {
+        //log('current notice is ' + this._activeNotifications[type].notification
+        //    + ' and is loaded for: ' + this._activeNotifications[type].identifier);
+        // We only perform this check if activeNotification has already been initialized.
+        // Otherwise we'll never be able to load notifications from this._activeNotifications.
+        if (this._activeNotifications[type]) {
+            //log('loadNotifications: checking archived notification == displayed notification..');
+            //log('activeNotification for ' + type + ': ' + this._activeNotifications[type]);
+            /*log('this._notifications[identifier] for ' + identifier + ': ' + 
this._notifications[identifier]);
+            if (this._notifications[identifier]) {
+                log('.notification for ' + identifier + ': ' + this._notifications[identifier].notification);
+                if (this._notifications[identifier].notification)
+                    log('.notification.widget for ' + identifier + ': ' + 
this._notifications[identifier].notification.widget);
+            }*/
+            if (this._activeNotifications[type] == this._notifications[identifier]) {
+                //log('match between active notification and current notification!');
+                return;
+            }
+            //log('loadNotifications: notifications did not match');
+        }
+
+        let notification = this._notifications[identifier] ? this._notifications[identifier].notification : 
null;
+        let params = { notification: notification,
+                       type: type, identifier: identifier };
+
+        this._displayNotification(params);
+    },
+
+    _displayNotification: function(params) {
+        //log('now displaying ' + params.notification + ' for: ' + params.identifier);
+
+        //log('_displayNotification: checking if any notifications of that type is visible..');
+        if (this._activeNotifications[params.type].notification) {
+            //log('_displayNotification: true. removing visible notification');
+            //this._activeNotifications[params.type].notification.close();
+            //log('grid children: ' + this._grid.get_children().length);
+            if (this._activeNotifications[params.type].notification.widget) {
+            //log('removing ' + this._activeNotifications[params.type].notification);
+                // If the next notification is null and we're still displaying for the same room.
+                // Then we should just make the active notification null.
+                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);
+                }
+            }
+        }
+        //log('storing parameters inside this._activeNotifications[' + params.type + ']..');
+        this._activeNotifications[params.type] = params;
+        //log('_displayNotification: checking if our notification is empty...');
+        if (!params.notification) {
+            //log('_displayNotification: notification is empty, returning..');
+            this._updateVisibility();
+            return;
+        }
+
+        this._grid.add(params.notification.widget);
+        params.notification.open();
+        params.notification.widget.connect('destroy',
+                                    Lang.bind(this, this._updateVisibility));
         this.widget.show();
     },
 
-    _onChildDestroy: function() {
-        if (this._grid.get_children().length == 0)
+    _updateVisibility: function() {
+        //log('amount of children in tree: ' + this._grid.get_children().length);
+        if (this._grid.get_children().length == 0) {
+            //log('hiding widget..');
            this.widget.hide();
+        }
     }
 });
 
 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..d603c5a 100644
--- a/src/application.js
+++ b/src/application.js
@@ -46,7 +46,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 +78,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 +195,21 @@ const Application = new Lang.Class({
         this._window.showMessageUserDialog();
     },
 
+    _onAccountChanged: function(am, account) {
+        // We reset the notification when account changes because
+        // External parties can make the account reconnect while
+        // we are showing an error.
+        // Fx. the case where we are try to connect, then disconnect, then reconnect.
+        // We get a network error and then somehow we begin reconnecting again.
+        log('account changed to ' + account.connection_error + ', resetting notification.');
+        let accountError = { notification: null,
+                             type: 'account',
+                             identifier: account.get_path_suffix() };
+        this.notificationQueue.setNotification(accountError);
+
+
+    },
+
     _addSavedChannel: function(account, channel) {
         let savedChannels = this._settings.get_value('saved-channel-list').deep_unpack();
         let savedChannel = {
@@ -273,8 +292,14 @@ const Application = new Lang.Class({
     },
 
     _ensureChannel: function(requestData) {
+        this.mark_busy();
         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,41 +309,64 @@ 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_path_suffix() };
         try {
             req.ensure_channel_finish(res);
 
             if (requestData.targetHandleType == Tp.HandleType.ROOM)
                 this._addSavedChannel(account, requestData.targetId);
         } catch (e if e.matches(Tp.Error, Tp.Error.DISCONNECTED)) {
-            let error = account.connection_error;
+            //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.unmark_busy();
+            this.notificationQueue.setNotification(roomError);
+            this.notificationQueue.setNotification(accountError);
         }
 
         if (requestData.retry > 0)
@@ -332,6 +380,16 @@ const Application = new Lang.Class({
                              channelName, time);
     },
 
+    _onReconnectAccount: function(action, parameter) {
+        let factory = Tp.AccountManager.dup().get_factory();
+        let accountPath = parameter.unpack();
+        log(accountPath);
+        let account = factory.ensure_account(accountPath, []);
+        account.reconnect_async(Lang.bind(this, function(a, res) {
+                account.reconnect_finish(res);
+            }));
+    },
+
     _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..ff49d6b 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_path_suffix(), '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]