Compare commits

...

2 Commits

Author SHA1 Message Date
David Zeuthen
a69e45b5eb goa: implement marking a message as Starred or Junk
This works with the latest GOA tree

Signed-off-by: David Zeuthen <davidz@redhat.com>
2011-05-19 14:52:52 -04:00
David Zeuthen
a355398b0f Initial goa client work
Signed-off-by: David Zeuthen <davidz@redhat.com>
2011-05-16 17:10:43 -04:00
6 changed files with 560 additions and 7 deletions

View File

@ -1655,3 +1655,49 @@ StTooltip StLabel {
.magnifier-zoom-region.full-screen {
border-width: 0px;
}
/* goa message popup */
.goa-message-table {
}
.goa-message-base {
font-size: 9pt;
}
.goa-message-from-header {
color: #666666;
font-weight: bold;
}
.goa-message-subject-header {
color: #666666;
font-weight: bold;
}
.goa-message-date-header {
color: #666666;
font-weight: bold;
}
.goa-message-from {
font-weight: bold;
min-width: 125px;
}
.goa-message-hbox {
spacing: 0.25em;
min-width: 300px;
}
.goa-message-subject {
}
.goa-message-excerpt {
color: rgba(153, 153, 153, 1.0);
}
.goa-message-date {
min-width: 80px;
}

View File

@ -26,6 +26,7 @@ nobase_dist_js_DATA = \
ui/endSessionDialog.js \
ui/environment.js \
ui/extensionSystem.js \
ui/goaClient.js \
ui/iconGrid.js \
ui/lightbox.js \
ui/link.js \

500
js/ui/goaClient.js Normal file
View File

@ -0,0 +1,500 @@
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Goa = imports.gi.Goa;
const Lang = imports.lang;
const Mainloop = imports.mainloop;
const Shell = imports.gi.Shell;
const Signals = imports.signals;
const St = imports.gi.St;
const Gettext = imports.gettext.domain('gnome-shell');
const _ = Gettext.gettext;
const C_ = Gettext.pgettext;
const Gtk = imports.gi.Gtk;
const Pango = imports.gi.Pango;
const History = imports.misc.history;
const Main = imports.ui.main;
const MessageTray = imports.ui.messageTray;
// ----------------------------------------------------------------------------------------------------
function Client() {
this._init();
}
Client.prototype = {
_init : function() {
this._client = null;
this._accountIdToMailMonitor = {}
this._mailSource = null;
// TODO: need to call refreshAllMonitors() when network-connectivity changes
Goa.Client.new(null, /* cancellable */
Lang.bind(this, this._onClientConstructed));
},
_onClientConstructed : function(object, asyncRes) {
this._client = object.new_finish(asyncRes);
this._updateAccounts();
this._client.connect('account-added', Lang.bind(this, this._updateAccounts));
this._client.connect('account-removed', Lang.bind(this, this._updateAccounts));
this._client.connect('account-changed', Lang.bind(this, this._updateAccounts));
},
_updateAccounts : function () {
let objects = this._client.get_accounts();
let mailIds = {};
// Add monitors for accounts that now exist
for (let n = 0; n < objects.length; n++) {
let object = objects[n];
let id = object.account.id;
if (object.mail) {
mailIds[id] = true;
if (!(id in this._accountIdToMailMonitor)) {
let monitor = new MailMonitor(this, object);
this._accountIdToMailMonitor[id] = monitor;
}
}
}
// Nuke monitors for accounts that are now non-existant
let monitorsToRemove = []
for (let existingMonitorId in this._accountIdToMailMonitor) {
if (!(existingMonitorId in mailIds)) {
monitorsToRemove.push(existingMonitorId);
}
}
for (let n = 0; n < monitorsToRemove.length; n++) {
let id = monitorsToRemove[n];
let monitor = this._accountIdToMailMonitor[id];
delete this._accountIdToMailMonitor[id]
monitor.destroy();
}
},
_ensureMailSource: function() {
if (!this._mailSource) {
this._mailSource = new MailSource(this);
this._mailSource.connect('destroy', Lang.bind(this,
function () {
this._mailSource = null;
}));
Main.messageTray.add(this._mailSource);
}
},
addPendingMessage: function(message) {
this._ensureMailSource();
this._mailSource.addMessage(message);
},
refreshAllMonitors: function() {
log('Refreshing all mail monitors');
for (let id in this._accountIdToMailMonitor) {
let monitor = this._accountIdToMailMonitor[id];
monitor.refresh();
}
}
}
// ----------------------------------------------------------------------------------------------------
function Message(uid, from, subject, excerpt, uri, can_be_marked_as_spam, can_be_starred) {
this._init(uid, from, subject, excerpt, uri, can_be_marked_as_spam, can_be_starred);
}
Message.prototype = {
_init: function(uid, from, subject, excerpt, uri, can_be_marked_as_spam, can_be_starred) {
this.uid = uid;
this.from = from;
this.subject = subject;
this.excerpt = excerpt;
this.uri = uri;
this.can_be_marked_as_spam = can_be_marked_as_spam;
this.can_be_starred = can_be_starred;
this.receivedAt = new Date();
}
}
// ----------------------------------------------------------------------------------------------------
function MailMonitor(client, accountObject) {
this._init(client, accountObject);
}
MailMonitor.prototype = {
_init : function(client, accountObject) {
this._client = client;
this._accountObject = accountObject;
this._account = this._accountObject.get_account();
this._mail = this._accountObject.get_mail();
// Create the remote monitor object
this._proxy = null;
this._cancellable = new Gio.Cancellable();
this._mail.call_create_monitor(this._cancellable, Lang.bind(this, this._onMonitorCreated));
},
destroy : function() {
this._cancellable.cancel();
if (this._proxy) {
// We don't really care if this fails or not
this._proxy.call_close(null, Lang.bind(this, function() { }));
this._proxy.disconnect(this._messageReceivedId);
this._proxy = null;
}
},
refresh : function() {
if (this._proxy) {
// We don't really care if this fails or not
log('Refreshing mail monitor for account ' + this._account.name);
this._proxy.call_refresh(null, Lang.bind(this, function() { }));
}
},
_onMonitorCreated : function(mail, asyncRes) {
// TODO: a (gboolean, object_path) tuple is returned here
// See https://bugzilla.gnome.org/show_bug.cgi?id=649657
let ret = mail.call_create_monitor_finish(asyncRes);
let object_path = ret[1];
Goa.MailMonitorProxy.new_for_bus(Gio.BusType.SESSION,
Gio.DBusProxyFlags.NONE,
'org.gnome.OnlineAccounts',
object_path,
null, /* cancellable */
Lang.bind(this, this._onMonitorProxyConstructed));
},
_onMonitorProxyConstructed : function(monitor, asyncRes) {
this._proxy = monitor.new_for_bus_finish(asyncRes);
// Now listen for changes on the mail monitor proxy
this._messageReceivedId = this._proxy.connect('message-received',
Lang.bind(this, this._onMessageReceived));
},
_onMessageReceived : function(monitor, uid, from, subject, excerpt, uri, can_be_marked_as_spam, can_be_starred) {
let message = new Message(uid, from, subject, excerpt, uri, can_be_marked_as_spam, can_be_starred);
if (!Main.messageTray.getBusy()) {
let source = new Source(this._client, message);
let notification = new Notification(source, this._client, this, message);
// If the user is not marked as busy, present the notification to the user
Main.messageTray.add(source);
source.notify(notification);
} else {
// ... otherwise, if the user is busy, just add it to the MailSource's list
// of pending messages
this._client.addPendingMessage(message);
}
},
MessageAddStar: function (message) {
this._proxy.call_add_star(message.uid,
null, /* cancellable */
Lang.bind(this,
function(object, asyncRes) {
this._proxy.call_add_star_finish(asyncRes);
}));
},
MessageMarkAsSpam: function (message) {
this._proxy.call_mark_as_spam(message.uid,
null, /* cancellable */
Lang.bind(this,
function(object, asyncRes) {
this._proxy.call_add_star_finish(asyncRes);
}));
}
}
// ----------------------------------------------------------------------------------------------------
function Source(client, message) {
this._init(client, message);
}
Source.prototype = {
__proto__: MessageTray.Source.prototype,
_init : function(client, message) {
this._client = client;
this._message = message;
// Init super class and add ourselves to the message tray
MessageTray.Source.prototype._init.call(this, 'Message from ' + _stripEmailAddress(this._message.from));
this.setTransient(true);
this.isChat = true;
this._setSummaryIcon(this.createNotificationIcon());
},
createNotificationIcon : function() {
// TODO: use account icon
let icon = new St.Icon({ icon_type: St.IconType.FULLCOLOR,
icon_size: this.ICON_SIZE,
icon_name: 'mail-send'});
return icon;
}
}
// ----------------------------------------------------------------------------------------------------
function _stripEmailAddress(name_and_addr) {
let bracketStartPos = name_and_addr.indexOf(' <');
if (bracketStartPos == -1) {
return name_and_addr;
} else {
return name_and_addr.slice(0, bracketStartPos);
}
}
function Notification(source, client, monitor, message) {
this._init(source, client, monitor, message);
}
Notification.prototype = {
__proto__: MessageTray.Notification.prototype,
_init : function(source, client, monitor, message) {
this._client = client;
this._monitor = monitor
this._message = message;
this._ignore = false;
this._alreadyExpanded = false;
this._strippedFrom = _stripEmailAddress(this._message.from);
let title = this._strippedFrom;
let banner = this._message.subject + ' \u2014 ' + this._message.excerpt; // — U+2014 EM DASH
// Init super class
MessageTray.Notification.prototype._init.call(this, source, title, banner);
// Change the contents once expanded
this.connect('expanded', Lang.bind (this, this._onExpanded));
this.update(title, banner);
this.setUrgency(MessageTray.Urgency.NORMAL);
this.setTransient(true);
this.addButton('ignore', 'Ignore');
if (message.can_be_starred)
this.addButton('star', 'Star');
if (message.can_be_marked_as_spam)
this.addButton('spam', 'Junk');
if (this._message.uri.length > 0) {
this.addButton('open', 'Open');
}
this.connect('action-invoked', Lang.bind(this,
function(notification, id) {
if (id == 'ignore') {
this._actionIgnore();
} else if (id == 'star') {
this._actionStar();
} else if (id == 'spam') {
this._actionSpam();
} else if (id == 'open') {
this._actionOpen();
}
}));
this.connect('clicked', Lang.bind(this,
function() {
if (this._message.uri.length > 0) {
this._actionOpen();
}
}));
// Hmm, should be ::done-displaying instead?
this.connect('destroy', Lang.bind(this, this._onDestroyed));
},
_onExpanded : function() {
if (this._alreadyExpanded)
return;
this._alreadyExpanded = true;
let escapedExcerpt = GLib.markup_escape_text(this._message.excerpt, -1);
let bannerMarkup = '<b>Subject:</b> ' + this._message.subject + '\n';
// TODO: if available, insert other headers such as Cc
bannerMarkup += '\n' + escapedExcerpt;
this.update(this._strippedFrom, bannerMarkup, {bannerMarkup: true});
},
_onDestroyed : function(reason) {
// If not ignoring the message, push it onto the Mail source
if (!this._ignore) {
this._client.addPendingMessage(this._message);
}
},
_actionIgnore : function() {
this._ignore = true;
},
_actionStar : function() {
this._ignore = true;
this._monitor.MessageAddStar(this._message);
},
_actionSpam : function() {
this._ignore = true;
this._monitor.MessageMarkAsSpam(this._message);
},
_actionOpen : function() {
this._ignore = true;
Gio.app_info_launch_default_for_uri(this._message.uri,
global.create_app_launch_context());
}
}
// ----------------------------------------------------------------------------------------------------
function _sameDay(dateA, dateB) {
return (dateA.getDate() == dateB.getDate() &&
dateA.getMonth() == dateB.getMonth() &&
dateA.getYear() == dateB.getYear());
}
function _sameYear(dateA, dateB) {
return (dateA.getYear() == dateB.getYear());
}
function _formatRelativeDate(date) {
let ret = ''
let now = new Date();
if (_sameDay(date, now)) {
ret = date.toLocaleFormat("%l:%M %p");
} else {
if (_sameYear(date, now)) {
ret = date.toLocaleFormat("%B %e");
} else {
ret = date.toLocaleFormat("%B %e, %Y");
}
}
return ret;
}
function _addMessageToTable(table, message) {
let formattedExcerpt = message.excerpt.replace(/\r/g, '').replace(/\n/g, ' ');
let formattedDate = _formatRelativeDate(message.receivedAt);
let fromLabel = new St.Label({ style_class: 'goa-message-base goa-message-from',
text: _stripEmailAddress(message.from)});
let hbox = new St.BoxLayout({ style_class: 'goa-message-hbox', vertical: false });
let subjectLabel = new St.Label({ style_class: 'goa-message-base goa-message-subject',
text: message.subject });
let excerptLabel = new St.Label({ style_class: 'goa-message-base goa-message-excerpt',
text: formattedExcerpt });
let dateLabel = new St.Label({ style_class: 'goa-message-base goa-message-date',
text: formattedDate });
excerptLabel.clutter_text.line_wrap = false;
excerptLabel.clutter_text.ellipsize = Pango.EllipsizeMode.END;
hbox.add(subjectLabel, { x_fill: true,
y_fill: false,
x_align: St.Align.END,
y_align: St.Align.START });
hbox.add(excerptLabel, { x_fill: true,
y_fill: false,
x_align: St.Align.END,
y_align: St.Align.START });
let n = table.get_row_count();
table.add(fromLabel, { x_fill: true, x_expand: true, row: n, col: 0 });
table.add(hbox, { row: n, col: 1 });
table.add(dateLabel, { row: n, col: 2 });
}
function MailSource(client) {
this._init(client);
}
MailSource.prototype = {
__proto__: MessageTray.Source.prototype,
_init : function(client) {
this._client = client;
this._pendingMessages = [];
// Init super class and add ourselves to the message tray
MessageTray.Source.prototype._init.call(this, 'Mail');
// Create the notification
this._notification = new MessageTray.Notification(this)
this._notification.setUrgency(MessageTray.Urgency.NORMAL);
this._notification.setResident(true);
this._updateNotification();
this.pushNotification(this._notification);
// Refresh all monitors everytime the "Mail" notification is displayed
this._notification.connect('expanded', Lang.bind(this,
function() {
this._client.refreshAllMonitors();
}));
},
createNotificationIcon : function() {
let numPending = this._pendingMessages.length;
let baseIcon = new Gio.ThemedIcon({ name: 'mail-mark-unread'});
let numerableIcon = new Gtk.NumerableIcon({ gicon: baseIcon });
numerableIcon.set_count(numPending);
let icon = new St.Icon({ icon_type: St.IconType.FULLCOLOR,
icon_size: this.ICON_SIZE });
icon.set_gicon(numerableIcon);
return icon;
},
_updateNotification: function() {
if (!this._notification)
return
let title = 'Mail';
let banner = ''
let table = new St.Table({ homogeneous: false,
style_class: 'goa-message-table',
reactive: true });
for (let n = 0; n < this._pendingMessages.length; n++)
_addMessageToTable (table, this._pendingMessages[n]);
this._notification.update(title, banner, { clear: true,
icon: this.createNotificationIcon() });
this._notification.addActor(table);
this._notification.addButton('close', 'Close');
this._notification.addButton('ignore-all', 'Ignore All');
this._notification.connect('action-invoked', Lang.bind(this,
function(notification, id) {
if (id == 'close') {
// TODO: Can't find another way hide it
notification._onClicked();
} else if (id == 'ignore-all') {
this.clearMessages();
}
}));
},
addMessage: function(message) {
this._pendingMessages.push(message);
// Update notification
this._updateNotification();
// Update icon with latest pending count
this._setSummaryIcon(this.createNotificationIcon());
},
clearMessages: function() {
let notification = this._notification;
this._notification = null;
if (notification)
notification.destroy();
this.destroy();
},
}

View File

@ -29,6 +29,7 @@ const WindowAttentionHandler = imports.ui.windowAttentionHandler;
const Scripting = imports.ui.scripting;
const ShellDBus = imports.ui.shellDBus;
const TelepathyClient = imports.ui.telepathyClient;
const GoaClient = imports.ui.goaClient;
const WindowManager = imports.ui.windowManager;
const Magnifier = imports.ui.magnifier;
const XdndHandler = imports.ui.xdndHandler;
@ -50,6 +51,7 @@ let messageTray = null;
let notificationDaemon = null;
let windowAttentionHandler = null;
let telepathyClient = null;
let goaClient = null;
let ctrlAltTabManager = null;
let recorder = null;
let shellDBusService = null;
@ -139,6 +141,7 @@ function start() {
notificationDaemon = new NotificationDaemon.NotificationDaemon();
windowAttentionHandler = new WindowAttentionHandler.WindowAttentionHandler();
telepathyClient = new TelepathyClient.Client();
goaClient = new GoaClient.Client();
overview.init();
statusIconDispatcher.start(messageTray.actor);

View File

@ -1301,6 +1301,10 @@ MessageTray.prototype = {
},
getBusy: function(source) {
return this._busy;
},
contains: function(source) {
return this._getIndexOfSummaryItemForSource(source) >= 0;
},

View File

@ -1174,17 +1174,17 @@ load_gicon_with_colors (StTextureCache *cache,
{
AsyncTextureLoadData *request;
ClutterActor *texture;
char *gicon_string;
guint gicon_hash;
char *key;
GtkIconTheme *theme;
GtkIconInfo *info;
gicon_string = g_icon_to_string (icon);
gicon_hash = g_icon_hash (icon);
if (colors)
{
/* This raises some doubts about the practice of using string keys */
key = g_strdup_printf (CACHE_PREFIX_GICON "icon=%s,size=%d,colors=%2x%2x%2x%2x,%2x%2x%2x%2x,%2x%2x%2x%2x,%2x%2x%2x%2x",
gicon_string, size,
key = g_strdup_printf (CACHE_PREFIX_GICON "icon_hash=%u,size=%d,colors=%2x%2x%2x%2x,%2x%2x%2x%2x,%2x%2x%2x%2x,%2x%2x%2x%2x",
gicon_hash, size,
colors->foreground.red, colors->foreground.blue, colors->foreground.green, colors->foreground.alpha,
colors->warning.red, colors->warning.blue, colors->warning.green, colors->warning.alpha,
colors->error.red, colors->error.blue, colors->error.green, colors->error.alpha,
@ -1192,10 +1192,9 @@ load_gicon_with_colors (StTextureCache *cache,
}
else
{
key = g_strdup_printf (CACHE_PREFIX_GICON "icon=%s,size=%d",
gicon_string, size);
key = g_strdup_printf (CACHE_PREFIX_GICON "icon_hash=%u,size=%d",
gicon_hash, size);
}
g_free (gicon_string);
if (create_texture_and_ensure_request (cache, key, size, &request, &texture))
{