[polari/wip/bastianilso/error-handling: 2/4] Handle errors from ensuring channels.
- From: Bastian Ilsø Hougaard <bastianilso src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [polari/wip/bastianilso/error-handling: 2/4] Handle errors from ensuring channels.
- Date: Sun, 2 Aug 2015 19:26:10 +0000 (UTC)
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]