[geary/mjog/user-plugins: 18/26] Convert plugins to use isolated context model
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/mjog/user-plugins: 18/26] Convert plugins to use isolated context model
- Date: Tue, 17 Mar 2020 08:25:45 +0000 (UTC)
commit ee4bd117ee1152f594861471d8157a64a4baae14
Author: Michael Gratton <mike vee net>
Date: Tue Mar 10 13:08:40 2020 +1100
Convert plugins to use isolated context model
Convert the plugin implementation to use a model where each plugin
has its own context object instances and has limited/no access to the
client's and engine's objects.
po/POTFILES.in | 1 +
src/client/application/application-controller.vala | 137 +---
.../application-folder-store-factory.vala | 282 +++++++++
.../application/application-main-window.vala | 13 +-
.../application-notification-context.vala | 688 +++++++++++++++------
.../application/application-plugin-manager.vala | 54 +-
src/client/meson.build | 1 +
.../desktop-notifications.vala | 261 +++++---
.../plugin/messaging-menu/messaging-menu.vala | 105 ++--
.../notification-badge/notification-badge.vala | 64 +-
src/client/plugin/plugin-notification.vala | 9 +-
11 files changed, 1130 insertions(+), 485 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 1a1db9c1..7f3178e5 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -23,6 +23,7 @@ src/client/application/application-configuration.vala
src/client/application/application-contact-store.vala
src/client/application/application-contact.vala
src/client/application/application-controller.vala
+src/client/application/application-folder-store-factory.vala
src/client/application/application-main-window.vala
src/client/application/application-notification-context.vala
src/client/application/application-plugin-manager.vala
diff --git a/src/client/application/application-controller.vala
b/src/client/application/application-controller.vala
index 47143389..75ccb0cf 100644
--- a/src/client/application/application-controller.vala
+++ b/src/client/application/application-controller.vala
@@ -79,8 +79,6 @@ internal class Application.Controller : Geary.BaseObject {
private Gee.Map<Geary.AccountInformation,AccountContext> accounts =
new Gee.HashMap<Geary.AccountInformation,AccountContext>();
- private NotificationContext notifications;
-
// Cancelled if the controller is closed
private GLib.Cancellable controller_open;
@@ -165,13 +163,7 @@ internal class Application.Controller : Geary.BaseObject {
}
- this.notifications = new NotificationContext(
- this.avatars,
- this.get_contact_store_for_account,
- this.should_notify_new_messages
- );
-
- this.plugins = new PluginManager(this.application, this.notifications);
+ this.plugins = new PluginManager(this.application);
// Migrate configuration if necessary.
Migrate.xdg_config_dir(this.application.get_user_data_directory(),
@@ -270,7 +262,7 @@ internal class Application.Controller : Geary.BaseObject {
try {
yield composer_barrier.wait_async();
} catch (GLib.Error err) {
- debug("Error waiting at composer barrier: %s", err.message);
+ warning("Error waiting at composer barrier: %s", err.message);
}
// Now that all composers are closed, we can shut down the
@@ -298,11 +290,10 @@ internal class Application.Controller : Geary.BaseObject {
try {
yield window_barrier.wait_async();
} catch (GLib.Error err) {
- debug("Error waiting at window barrier: %s", err.message);
+ warning("Error waiting at window barrier: %s", err.message);
}
// Release general resources now there's no more UI
- this.notifications.clear_folders();
try {
this.plugins.close();
} catch (GLib.Error err) {
@@ -331,10 +322,10 @@ internal class Application.Controller : Geary.BaseObject {
try {
yield account_barrier.wait_async();
} catch (GLib.Error err) {
- debug("Error waiting at account barrier: %s", err.message);
+ warning("Error waiting at account barrier: %s", err.message);
}
- debug("Closed Application.Controller");
+ info("Closed Application.Controller");
}
/**
@@ -1254,33 +1245,6 @@ internal class Application.Controller : Geary.BaseObject {
return retry;
}
- private bool is_inbox_descendant(Geary.Folder target) {
- bool is_descendent = false;
-
- Geary.Account account = target.account;
- Geary.Folder? inbox = account.get_special_folder(Geary.SpecialFolderType.INBOX);
-
- if (inbox != null) {
- is_descendent = inbox.path.is_descendant(target.path);
- }
- return is_descendent;
- }
-
- private void on_special_folder_type_changed(Geary.Folder folder,
- Geary.SpecialFolderType old_type,
- Geary.SpecialFolderType new_type) {
- // Update notifications
- this.notifications.remove_folder(folder);
- if (folder.special_folder_type == Geary.SpecialFolderType.INBOX ||
- (folder.special_folder_type == Geary.SpecialFolderType.NONE &&
- is_inbox_descendant(folder))) {
- Geary.AccountInformation info = folder.account.information;
- this.notifications.add_folder(
- folder, this.accounts.get(info).cancellable
- );
- }
- }
-
private void on_folders_available_unavailable(
Geary.Account account,
Gee.BidirSortedSet<Geary.Folder>? available,
@@ -1292,33 +1256,13 @@ internal class Application.Controller : Geary.BaseObject {
if (!Controller.should_add_folder(available, folder)) {
continue;
}
- folder.special_folder_type_changed.connect(
- on_special_folder_type_changed
- );
GLib.Cancellable cancellable = context.cancellable;
- switch (folder.special_folder_type) {
- case Geary.SpecialFolderType.INBOX:
+ if (folder.special_folder_type == INBOX) {
if (context.inbox == null) {
context.inbox = folder;
}
folder.open_async.begin(NO_DELAY, cancellable);
-
- // Always notify for new messages in the Inbox
- this.notifications.add_folder(
- folder, cancellable
- );
- break;
-
- case Geary.SpecialFolderType.NONE:
- // Only notify for new messages in non-special
- // descendants of the Inbox
- if (is_inbox_descendant(folder)) {
- this.notifications.add_folder(
- folder, cancellable
- );
- }
- break;
}
}
}
@@ -1329,23 +1273,9 @@ internal class Application.Controller : Geary.BaseObject {
bool has_prev = unavailable_iterator.last();
while (has_prev) {
Geary.Folder folder = unavailable_iterator.get();
- folder.special_folder_type_changed.disconnect(
- on_special_folder_type_changed
- );
- switch (folder.special_folder_type) {
- case Geary.SpecialFolderType.INBOX:
+ if (folder.special_folder_type == INBOX) {
context.inbox = null;
- this.notifications.remove_folder(folder);
- break;
-
- case Geary.SpecialFolderType.NONE:
- // Only notify for new messages in non-special
- // descendants of the Inbox
- if (is_inbox_descendant(folder)) {
- this.notifications.remove_folder(folder);
- }
- break;
}
has_prev = unavailable_iterator.previous();
@@ -1356,48 +1286,15 @@ internal class Application.Controller : Geary.BaseObject {
}
}
- private bool should_notify_new_messages(Geary.Folder folder) {
- // Don't show notifications if the top of the folder's
- // conversations is visible. That is, if there is a main
- // window, it's focused, the folder is selected, and the
- // conversation list is at the top.
- MainWindow? window = this.application.last_active_main_window;
- return (
- window == null ||
- !window.has_toplevel_focus ||
- window.selected_folder != folder ||
- window.conversation_list_view.vadjustment.value > 0.0
- );
- }
-
- // Clears messages if conditions are true: anything in should_notify_new_messages() is
- // false and the supplied visible messages are visible in the conversation list view
- public void clear_new_messages(string caller,
- Gee.Set<Geary.App.Conversation>? supplied) {
- MainWindow? window = this.application.last_active_main_window;
- Geary.Folder? selected = (
- (window != null) ? window.selected_folder : null
- );
- NotificationContext notifications = this.notifications;
- if (selected != null && (
- !notifications.get_folders().contains(selected) ||
- should_notify_new_messages(selected))) {
-
- Gee.Set<Geary.App.Conversation> visible =
- supplied ?? window.conversation_list_view.get_visible_conversations();
-
- foreach (Geary.App.Conversation conversation in visible) {
- try {
- if (notifications.are_any_new_messages(selected,
- conversation.get_email_ids())) {
- debug("Clearing new messages: %s", caller);
- notifications.clear_new_messages(selected);
- break;
- }
- } catch (Geary.EngineError.NOT_FOUND err) {
- // all good
- }
- }
+ /** Clears new message counts in notification plugin contexts. */
+ public void clear_new_messages(Geary.Folder source,
+ Gee.Set<Geary.App.Conversation> visible) {
+ foreach (MainWindow window in this.application.get_main_windows()) {
+ window.folder_list.set_has_new(source, false);
+ }
+ foreach (NotificationContext context in
+ this.plugins.get_notification_contexts()) {
+ context.clear_new_messages(source, visible);
}
}
@@ -1575,7 +1472,7 @@ internal class Application.Controller : Geary.BaseObject {
AccountContext? context = this.accounts.get(service.account);
if (context != null) {
- this.notifications.email_sent(context.account, sent);
+ //this.notifications.email_sent(context.account, sent);
}
}
diff --git a/src/client/application/application-folder-store-factory.vala
b/src/client/application/application-folder-store-factory.vala
new file mode 100644
index 00000000..84384e3a
--- /dev/null
+++ b/src/client/application/application-folder-store-factory.vala
@@ -0,0 +1,282 @@
+/*
+ * Copyright © 2020 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+/**
+ * A factory for constructing plugin folder stores and folder objects.
+ *
+ * This class provides a common implementation that shares folder
+ * objects between different plugin context instances.
+ */
+internal class Application.FolderStoreFactory : Geary.BaseObject {
+
+
+ private class FolderStoreImpl : Geary.BaseObject, Plugin.FolderStore {
+
+
+ private Gee.Map<Geary.Folder,FolderImpl> folders;
+
+
+ public FolderStoreImpl(Gee.Map<Geary.Folder,FolderImpl> folders) {
+ this.folders = folders;
+ }
+
+ /** Returns a read-only set of all known folders. */
+ public Gee.Collection<Plugin.Folder> get_folders() {
+ return this.folders.values.read_only_view;
+ }
+
+ internal void destroy() {
+ this.folders = Gee.Map.empty();
+ }
+
+ }
+
+
+ private class AccountImpl : Geary.BaseObject, Plugin.Account {
+
+
+ public string display_name {
+ get { return this.backing.display_name; }
+ }
+
+
+ private Geary.AccountInformation backing;
+
+
+ public AccountImpl(Geary.AccountInformation backing) {
+ this.backing = backing;
+ }
+
+ }
+
+
+ private class FolderImpl : Geary.BaseObject, Plugin.Folder {
+
+
+ // These constants are used to determine the persistent id of
+ // the folder. Changing these may break plugins.
+ private const string ID_FORMAT = "%s:%s";
+ private const string ID_PATH_SEP = ">";
+
+
+ public string persistent_id {
+ get { return this._persistent_id; }
+ }
+ private string _persistent_id;
+
+ public string display_name {
+ get { return this._display_name; }
+ }
+ private string _display_name;
+
+ public Geary.SpecialFolderType folder_type {
+ get { return this.backing.special_folder_type; }
+ }
+
+ public Plugin.Account? account {
+ get { return this._account; }
+ }
+ private AccountImpl? _account;
+
+ // The underlying engine folder being represented.
+ internal Geary.Folder backing { get; private set; }
+
+
+ public FolderImpl(Geary.Folder backing, AccountImpl? account) {
+ this.backing = backing;
+ this._account = account;
+ this._persistent_id = ID_FORMAT.printf(
+ backing.account.information.id,
+ string.join(ID_PATH_SEP, backing.path.as_array())
+ );
+ folder_type_changed();
+ }
+
+ public GLib.Variant to_variant() {
+ return new GLib.Variant.tuple({
+ this.backing.account.information.id,
+ new GLib.Variant.variant(this.backing.path.to_variant())
+ });
+ }
+
+ internal void folder_type_changed() {
+ notify_property("folder-type");
+ this._display_name = this.backing.get_display_name();
+ notify_property("display-name");
+ }
+
+ }
+
+
+ private Geary.Engine engine;
+
+ private Gee.Map<Geary.AccountInformation,AccountImpl> accounts =
+ new Gee.HashMap<Geary.AccountInformation,AccountImpl>();
+ private Gee.Map<Geary.Folder,FolderImpl> folders =
+ new Gee.HashMap<Geary.Folder,FolderImpl>();
+ private Gee.Set<FolderStoreImpl> stores =
+ new Gee.HashSet<FolderStoreImpl>();
+
+
+ /**
+ * Constructs a new factory instance.
+ */
+ public FolderStoreFactory(Geary.Engine engine) throws GLib.Error {
+ this.engine = engine;
+ this.engine.account_available.connect(on_account_available);
+ this.engine.account_unavailable.connect(on_account_unavailable);
+ foreach (Geary.Account account in this.engine.get_accounts()) {
+ add_account(account.information);
+ }
+ }
+
+ /** Clearing all state of the store. */
+ public void destroy() throws GLib.Error {
+ foreach (FolderStoreImpl store in this.stores) {
+ store.destroy();
+ }
+ this.stores.clear();
+
+ this.engine.account_available.disconnect(on_account_available);
+ this.engine.account_unavailable.disconnect(on_account_unavailable);
+ foreach (Geary.Account account in this.engine.get_accounts()) {
+ remove_account(account.information);
+ }
+ this.folders.clear();
+ }
+
+ /** Constructs a new folder store for use by plugin contexts. */
+ public Plugin.FolderStore new_folder_store() {
+ var store = new FolderStoreImpl(this.folders);
+ this.stores.add(store);
+ return store;
+ }
+
+ /** Destroys a folder store once is no longer required. */
+ public void destroy_folder_store(Plugin.FolderStore plugin) {
+ FolderStoreImpl? impl = plugin as FolderStoreImpl;
+ if (impl != null) {
+ impl.destroy();
+ this.stores.remove(impl);
+ }
+ }
+
+ /** Returns the plugin folder for the given engine folder. */
+ public Plugin.Folder? get_plugin_folder(Geary.Folder engine) {
+ return this.folders.get(engine);
+ }
+
+ /** Returns the engine folder for the given plugin folder. */
+ public Geary.Folder? get_engine_folder(Plugin.Folder plugin) {
+ FolderImpl? impl = plugin as FolderImpl;
+ return (impl != null) ? impl.backing : null;
+ }
+
+ private void add_account(Geary.AccountInformation added) {
+ try {
+ this.accounts.set(added, new AccountImpl(added));
+ Geary.Account account = this.engine.get_account(added);
+ account.folders_available_unavailable.connect(
+ on_folders_available_unavailable
+ );
+ account.folders_special_type.connect(
+ on_folders_type_changed
+ );
+ add_folders(account.list_folders());
+ } catch (GLib.Error err) {
+ warning(
+ "Failed to add account %s to folder store: %s",
+ added.id, err.message
+ );
+ }
+ }
+
+ private void remove_account(Geary.AccountInformation removed) {
+ try {
+ Geary.Account account = this.engine.get_account(removed);
+ account.folders_available_unavailable.disconnect(
+ on_folders_available_unavailable
+ );
+ account.folders_special_type.disconnect(
+ on_folders_type_changed
+ );
+ remove_folders(account.list_folders());
+ this.accounts.unset(removed);
+ } catch (GLib.Error err) {
+ warning(
+ "Error removing account %s from folder store: %s",
+ removed.id, err.message
+ );
+ }
+ }
+
+ private void add_folders(Gee.Collection<Geary.Folder> to_add) {
+ foreach (Geary.Folder folder in to_add) {
+ this.folders.set(
+ folder,
+ new FolderImpl(
+ folder, this.accounts.get(folder.account.information)
+ )
+ );
+ }
+ foreach (FolderStoreImpl store in this.stores) {
+ store.folders_available(to_plugin_folders(to_add));
+ }
+ }
+
+ private void remove_folders(Gee.Collection<Geary.Folder> to_remove) {
+ foreach (Geary.Folder folder in to_remove) {
+ this.folders.unset(folder);
+ }
+ foreach (FolderStoreImpl store in this.stores) {
+ store.folders_unavailable(to_plugin_folders(to_remove));
+ }
+ }
+
+ private Gee.Collection<FolderImpl> to_plugin_folders(
+ Gee.Collection<Geary.Folder> folders
+ ) {
+ return Geary.traverse(
+ folders
+ ).map<FolderImpl>(
+ (f) => this.folders.get(f)
+ ).to_linked_list().read_only_view;
+ }
+
+ private void on_account_available(Geary.AccountInformation to_add) {
+ add_account(to_add);
+ }
+
+ private void on_account_unavailable(Geary.AccountInformation to_remove) {
+ remove_account(to_remove);
+ }
+
+ private void on_folders_available_unavailable(
+ Geary.Account account,
+ Gee.BidirSortedSet<Geary.Folder>? available,
+ Gee.BidirSortedSet<Geary.Folder>? unavailable
+ ) {
+ if (available != null && !available.is_empty) {
+ add_folders(available);
+ }
+ if (unavailable != null && !unavailable.is_empty) {
+ remove_folders(available);
+ }
+ }
+
+ private void on_folders_type_changed(Geary.Account account,
+ Gee.Collection<Geary.Folder> changed) {
+ var folders = to_plugin_folders(changed);
+ foreach (FolderImpl folder in folders) {
+ folder.folder_type_changed();
+ }
+ foreach (FolderStoreImpl store in this.stores) {
+ store.folders_type_changed(folders);
+ }
+ }
+
+}
diff --git a/src/client/application/application-main-window.vala
b/src/client/application/application-main-window.vala
index e88d23fc..31d8ba8b 100644
--- a/src/client/application/application-main-window.vala
+++ b/src/client/application/application-main-window.vala
@@ -741,8 +741,6 @@ public class Application.MainWindow :
);
yield open_conversation_monitor(this.conversations, cancellable);
- this.controller.clear_new_messages(GLib.Log.METHOD, null);
-
this.controller.process_pending_composers();
}
}
@@ -2082,7 +2080,12 @@ public class Application.MainWindow :
// this signal does not necessarily indicate that the application
// previously didn't have focus and now it does
private void on_has_toplevel_focus() {
- this.controller.clear_new_messages(GLib.Log.METHOD, null);
+ if (this.selected_folder != null) {
+ this.controller.clear_new_messages(
+ this.selected_folder,
+ this.conversation_list_view.get_visible_conversations()
+ );
+ }
}
private void on_folder_selected(Geary.Folder? folder) {
@@ -2098,7 +2101,9 @@ public class Application.MainWindow :
}
private void on_visible_conversations_changed(Gee.Set<Geary.App.Conversation> visible) {
- this.controller.clear_new_messages(GLib.Log.METHOD, visible);
+ if (this.selected_folder != null) {
+ this.controller.clear_new_messages(this.selected_folder, visible);
+ }
}
private void on_conversation_activated(Geary.App.Conversation activated) {
diff --git a/src/client/application/application-notification-context.vala
b/src/client/application/application-notification-context.vala
index faa38d68..2152243e 100644
--- a/src/client/application/application-notification-context.vala
+++ b/src/client/application/application-notification-context.vala
@@ -1,9 +1,9 @@
/*
- * Copyright 2016 Software Freedom Conservancy Inc.
- * Copyright 2019 Michael Gratton <mike vee net>
+ * Copyright © 2016 Software Freedom Conservancy Inc.
+ * Copyright © 2019-2020 Michael Gratton <mike vee net>
*
* This software is licensed under the GNU Lesser General Public License
- * (version 2.1 or later). See the COPYING file in this distribution.
+ * (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
@@ -14,285 +14,597 @@
* the plugins will be passed an instance of this class as the
* `context` property.
*
- * Plugins can connect to the "notify::count", the {@link
- * new_messages_arrived} or the {@link new_messages_retired} signals
- * and update their state as these change.
+ * Plugins should register folders they wish to monitor by calling
+ * {@link start_monitoring_folder}. The context will then start
+ * keeping track of email being delivered to the folder and being seen
+ * in a main window updating {@link total_new_messages} and emitting
+ * the {@link new_messages_arrived} and {@link new_messages_retired}
+ * signals as appropriate.
*
* @see Plugin.NotificationPlugin
*/
public class Application.NotificationContext : Geary.BaseObject {
- /** Monitor hook for obtaining a contact store for an account. */
- internal delegate Application.ContactStore? GetContactStore(
- Geary.Account account
- );
+ private const Geary.Email.Field REQUIRED_FIELDS = FLAGS;
+
+
+ private class ApplicationImpl : Geary.BaseObject, Plugin.Application {
+
+
+ private Client backing;
+ private FolderStoreFactory folders;
+
+
+ public ApplicationImpl(Client backing,
+ FolderStoreFactory folders) {
+ this.backing = backing;
+ this.folders = folders;
+ }
+
+ public override void show_folder(Plugin.Folder folder) {
+ Geary.Folder? target = this.folders.get_engine_folder(folder);
+ if (target != null) {
+ this.backing.show_folder.begin(target);
+ }
+ }
+
+ }
+
+
+ private class EmailStoreImpl : Geary.BaseObject, Plugin.EmailStore {
+
+
+ private class EmailImpl : Geary.BaseObject, Plugin.Email {
+
+
+ public Plugin.EmailIdentifier identifier {
+ get {
+ if (this._id == null) {
+ this._id = new IdImpl(this.backing.id, this.account);
+ }
+ return this._id;
+ }
+ }
+ private IdImpl? _id = null;
+
+ public string subject {
+ get { return this._subject; }
+ }
+ string _subject;
+
+ internal Geary.Email backing;
+ // Remove this when EmailIdentifier is updated to include
+ // the account
+ internal Geary.AccountInformation account { get; private set; }
+
+
+ public EmailImpl(Geary.Email backing,
+ Geary.AccountInformation account) {
+ this.backing = backing;
+ this.account = account;
+ Geary.RFC822.Subject? subject = this.backing.subject;
+ this._subject = subject != null ? subject.to_string() : "";
+ }
+
+ public Geary.RFC822.MailboxAddress? get_primary_originator() {
+ return Util.Email.get_primary_originator(this.backing);
+ }
+
+ }
+
+
+ private class IdImpl : Geary.BaseObject,
+ Gee.Hashable<Plugin.EmailIdentifier>, Plugin.EmailIdentifier {
- /** Monitor hook to determine if a folder should be notified about. */
- internal delegate bool ShouldNotifyNewMessages(Geary.Folder folder);
+
+ internal Geary.EmailIdentifier backing { get; private set; }
+ // Remove this when EmailIdentifier is updated to include
+ // the account
+ internal Geary.AccountInformation account { get; private set; }
+
+
+ public IdImpl(Geary.EmailIdentifier backing,
+ Geary.AccountInformation account) {
+ this.backing = backing;
+ this.account = account;
+ }
+
+ public GLib.Variant to_variant() {
+ return this.backing.to_variant();
+ }
+
+ public bool equal_to(Plugin.EmailIdentifier other) {
+ if (this == other) {
+ return true;
+ }
+ IdImpl? impl = other as IdImpl;
+ return (
+ impl != null &&
+ this.backing.equal_to(impl.backing) &&
+ this.account.equal_to(impl.account)
+ );
+ }
+
+ public uint hash() {
+ return this.backing.hash();
+ }
+
+ }
+
+
+ private Client backing;
+
+
+ public EmailStoreImpl(Client backing) {
+ this.backing = backing;
+ }
+
+ public async Gee.Collection<Plugin.Email> get_email(
+ Gee.Collection<Plugin.EmailIdentifier> plugin_ids,
+ GLib.Cancellable? cancellable
+ ) throws GLib.Error {
+ var emails = new Gee.HashSet<Plugin.Email>();
+
+ // The email could theoretically come from any account, so
+ // group them by account up front. The common case will be
+ // only a single account, so optimise for that a bit.
+
+ var accounts = new Gee.HashMap<
+ Geary.AccountInformation,
+ Gee.Set<Geary.EmailIdentifier>
+ >();
+ Geary.AccountInformation? current_account = null;
+ Gee.Set<Geary.EmailIdentifier>? engine_ids = null;
+ foreach (Plugin.EmailIdentifier plugin_id in plugin_ids) {
+ IdImpl? id_impl = plugin_id as IdImpl;
+ if (id_impl != null) {
+ if (id_impl.account != current_account) {
+ current_account = id_impl.account;
+ engine_ids = accounts.get(current_account);
+ if (engine_ids == null) {
+ engine_ids = new Gee.HashSet<Geary.EmailIdentifier>();
+ accounts.set(current_account, engine_ids);
+ }
+ }
+ engine_ids.add(id_impl.backing);
+ }
+ }
+
+ foreach (var account in accounts.keys) {
+ AccountContext context =
+ this.backing.controller.get_context_for_account(account);
+ Gee.Collection<Geary.Email> batch =
+ yield context.emails.list_email_by_sparse_id_async(
+ accounts.get(account),
+ ENVELOPE,
+ NONE,
+ context.cancellable
+ );
+ if (batch != null) {
+ foreach (var email in batch) {
+ emails.add(new EmailImpl(email, account));
+ }
+ }
+ }
+
+ return emails;
+ }
+
+ internal Gee.Collection<Plugin.EmailIdentifier> get_plugin_ids(
+ Gee.Collection<Geary.EmailIdentifier> engine_ids,
+ Geary.AccountInformation account
+ ) {
+ var plugin_ids = new Gee.HashSet<Plugin.EmailIdentifier>();
+ foreach (var id in engine_ids) {
+ plugin_ids.add(new IdImpl(id, account));
+ }
+ return plugin_ids;
+ }
+
+ }
+
+
+ private class ContactStoreImpl : Geary.BaseObject, Plugin.ContactStore {
+
+
+ private Application.ContactStore backing;
+
+
+ public ContactStoreImpl(Application.ContactStore backing) {
+ this.backing = backing;
+ }
+
+ public async Gee.Collection<Contact> search(string query,
+ uint min_importance,
+ uint limit,
+ GLib.Cancellable? cancellable
+ ) throws GLib.Error {
+ return yield this.backing.search(
+ query, min_importance, limit, cancellable
+ );
+ }
+
+ public async Contact load(Geary.RFC822.MailboxAddress mailbox,
+ GLib.Cancellable? cancellable
+ ) throws GLib.Error {
+ return yield this.backing.load(mailbox, cancellable);
+ }
+
+ }
private class MonitorInformation : Geary.BaseObject {
+
public Geary.Folder folder;
public GLib.Cancellable? cancellable = null;
- public int count = 0;
- public Gee.HashSet<Geary.EmailIdentifier> new_ids
- = new Gee.HashSet<Geary.EmailIdentifier>();
+ public Gee.Set<Geary.EmailIdentifier> recent_ids =
+ new Gee.HashSet<Geary.EmailIdentifier>();
- public MonitorInformation(Geary.Folder folder, GLib.Cancellable? cancellable) {
+ public MonitorInformation(Geary.Folder folder,
+ GLib.Cancellable? cancellable) {
this.folder = folder;
this.cancellable = cancellable;
}
}
- /** Current total new message count across all accounts and folders. */
- public int total_new_messages { get; private set; default = 0; }
-
/**
- * Folder containing the recent new message received, if any.
+ * Returns the plugin application object.
*
- * @see last_new_message
+ * No special permissions are required to use access this.
*/
- public Geary.Folder? last_new_message_folder {
- get; private set; default = null;
+ public Plugin.Application plugin_application {
+ get; private set;
}
/**
- * Most recent new message received, if any.
+ * Current total new message count for all monitored folders.
*
- * @see last_new_message_folder
+ * This is the sum of the the counts returned by {@link
+ * get_new_message_count} for all folders that are being monitored
+ * after a call to {@link start_monitoring_folder}.
*/
- public Geary.Email? last_new_message {
- get; private set; default = null;
- }
-
- /** Returns a store to lookup avatars for notifications. */
- public Application.AvatarStore avatars { get; private set; }
-
-
- private Geary.Email.Field required_fields { get; private set; default = FLAGS; }
-
- private Gee.Map<Geary.Folder, MonitorInformation> folder_information =
- new Gee.HashMap<Geary.Folder, MonitorInformation>();
+ public int total_new_messages { get; private set; default = 0; }
- private unowned GetContactStore contact_store_delegate;
- private unowned ShouldNotifyNewMessages notify_delegate;
+ private Gee.Map<Geary.Folder,MonitorInformation> folder_information =
+ new Gee.HashMap<Geary.Folder,MonitorInformation>();
+ private unowned Client application;
+ private FolderStoreFactory folders_factory;
+ private Plugin.FolderStore folders;
+ private EmailStoreImpl email;
+ private PluginManager.PluginFlags flags;
- /** Emitted when a new folder will be monitored. */
- public signal void folder_added(Geary.Folder folder);
- /** Emitted when a folder should no longer be monitored. */
- public signal void folder_removed(Geary.Folder folder);
+ /**
+ * Emitted when new messages have been downloaded.
+ *
+ * This will only be emitted for folders that are being monitored
+ * by calling {@link start_monitoring_folder}.
+ */
+ public signal void new_messages_arrived(
+ Plugin.Folder parent,
+ int total,
+ Gee.Collection<Plugin.EmailIdentifier> added
+ );
- /** Emitted when new messages have been downloaded. */
- public signal void new_messages_arrived(Geary.Folder parent, int total, int added);
+ /**
+ * Emitted when a folder has been cleared of new messages.
+ *
+ * This will only be emitted for folders that are being monitored
+ * after a call to {@link start_monitoring_folder}.
+ */
+ public signal void new_messages_retired(Plugin.Folder parent, int total);
- /** Emitted when a folder has been cleared of new messages. */
- public signal void new_messages_retired(Geary.Folder parent, int total);
- /** Emitted when an email has been sent. */
- public signal void email_sent(Geary.Account account,
- Geary.RFC822.Message sent);
+ /** Constructs a new context instance. */
+ internal NotificationContext(Client application,
+ FolderStoreFactory folders_factory,
+ PluginManager.PluginFlags flags) {
+ this.application = application;
+ this.folders_factory = folders_factory;
+ this.folders = folders_factory.new_folder_store();
+ this.email = new EmailStoreImpl(application);
+ this.flags = flags;
+
+ this.plugin_application = new ApplicationImpl(
+ application, folders_factory
+ );
+ }
+ /**
+ * Returns a store to lookup folders for notifications.
+ *
+ * This method may prompt for permission before returning.
+ *
+ * @throws Geary.EngineError.PERMISSIONS if permission to access
+ * this resource was not given
+ */
+ public async Plugin.FolderStore get_folders()
+ throws Geary.EngineError.PERMISSIONS {
+ return this.folders;
+ }
- /** Constructs a new context instance. */
- internal NotificationContext(AvatarStore avatars,
- GetContactStore contact_store_delegate,
- ShouldNotifyNewMessages notify_delegate) {
- this.avatars = avatars;
- this.contact_store_delegate = contact_store_delegate;
- this.notify_delegate = notify_delegate;
+ /**
+ * Returns a store to lookup email for notifications.
+ *
+ * This method may prompt for permission before returning.
+ *
+ * @throws Geary.EngineError.PERMISSIONS if permission to access
+ * this resource was not given
+ */
+ public async Plugin.EmailStore get_email()
+ throws Geary.EngineError.PERMISSIONS {
+ return this.email;
}
- /** Determines if notifications should be made for a specific folder. */
- public bool should_notify_new_messages(Geary.Folder folder) {
- return this.notify_delegate(folder);
+ /**
+ * Returns a store to lookup contacts for notifications.
+ *
+ * This method may prompt for permission before returning.
+ *
+ * @throws Geary.EngineError.NOT_FOUND if the given account does
+ * not exist
+ * @throws Geary.EngineError.PERMISSIONS if permission to access
+ * this resource was not given
+ */
+ public async Plugin.ContactStore get_contacts_for_folder(Plugin.Folder source)
+ throws Geary.EngineError.NOT_FOUND,
+ Geary.EngineError.PERMISSIONS {
+ Geary.Folder? folder = this.folders_factory.get_engine_folder(source);
+ AccountContext? context = null;
+ if (folder != null) {
+ context = this.application.controller.get_context_for_account(
+ folder.account.information
+ );
+ }
+ if (context == null) {
+ throw new Geary.EngineError.NOT_FOUND(
+ "No account for folder: %s", source.display_name
+ );
+ }
+ return new ContactStoreImpl(context.contacts);
}
- /** Returns a contact store to lookup contacts for notifications. */
- public Application.ContactStore? get_contact_store(Geary.Account account) {
- return this.contact_store_delegate(account);
+ /**
+ * Returns the client's application object.
+ *
+ * Only plugins that are trusted by the client will be provided
+ * access to the application instance.
+ *
+ * @throws Geary.EngineError.PERMISSIONS if permission to access
+ * this resource was not given
+ */
+ public Client get_client_application()
+ throws Geary.EngineError.PERMISSIONS {
+ if (!(PluginManager.PluginFlags.TRUSTED in this.flags)) {
+ throw new Geary.EngineError.PERMISSIONS("Plugin is not trusted");
+ }
+ return this.application;
}
- /** Returns a read-only set the context's monitored folders. */
- public Gee.Collection<Geary.Folder> get_folders() {
- return this.folder_information.keys.read_only_view;
+ /**
+ * Determines if notifications should be made for a specific folder.
+ *
+ * Notification plugins should call this to first before
+ * displaying a "new mail" notification for mail in a specific
+ * folder. It will return true for any monitored folder that is
+ * not currently visible in the currently focused main window, if
+ * any.
+ */
+ public bool should_notify_new_messages(Plugin.Folder target) {
+ // Don't show notifications if the top of a monitored folder's
+ // conversations are visible. That is, if there is a main
+ // window, it's focused, the folder is selected, and the
+ // conversation list is at the top.
+ Geary.Folder? folder = this.folders_factory.get_engine_folder(target);
+ MainWindow? window = this.application.last_active_main_window;
+ return (
+ folder != null &&
+ this.folder_information.has_key(folder) && (
+ window == null ||
+ !window.has_toplevel_focus ||
+ window.selected_folder != folder ||
+ window.conversation_list_view.vadjustment.value > 0.0
+ )
+ );
}
- /** Returns the new message count for a specific folder. */
- public int get_new_message_count(Geary.Folder folder)
+ /**
+ * Returns the new message count for a specific folder.
+ *
+ * The context must have already been requested to monitor the
+ * folder by a call to {@link start_monitoring_folder}.
+ */
+ public int get_new_message_count(Plugin.Folder target)
throws Geary.EngineError.NOT_FOUND {
- MonitorInformation? info = folder_information.get(folder);
+ Geary.Folder? folder = this.folders_factory.get_engine_folder(target);
+ MonitorInformation? info = null;
+ if (folder != null) {
+ info = folder_information.get(folder);
+ }
if (info == null) {
throw new Geary.EngineError.NOT_FOUND(
"No such folder: %s", folder.path.to_string()
);
}
- return info.count;
+ return info.recent_ids.size;
}
- /** Adds fields for loaded email required by a plugin. */
- public void add_required_fields(Geary.Email.Field fields) {
- this.required_fields |= fields;
- }
-
- /** Removes fields for loaded email no longer required by a plugin. */
- public void remove_required_fields(Geary.Email.Field fields) {
- this.required_fields ^= fields;
- }
-
- internal void add_folder(Geary.Folder folder, GLib.Cancellable? cancellable) {
- if (!this.folder_information.has_key(folder)) {
+ /**
+ * Starts monitoring a folder for new messages.
+ *
+ * Notification plugins should call this to start the context
+ * recording new messages for a specific folder.
+ */
+ public void start_monitoring_folder(Plugin.Folder target) {
+ Geary.Folder? folder = this.folders_factory.get_engine_folder(target);
+ AccountContext? context =
+ this.application.controller.get_context_for_account(
+ folder.account.information
+ );
+ if (folder != null &&
+ context != null &&
+ !this.folder_information.has_key(folder)) {
folder.email_locally_appended.connect(on_email_locally_appended);
folder.email_flags_changed.connect(on_email_flags_changed);
folder.email_removed.connect(on_email_removed);
this.folder_information.set(
- folder, new MonitorInformation(folder, cancellable)
+ folder, new MonitorInformation(folder, context.cancellable)
);
-
- folder_added(folder);
}
}
- internal void remove_folder(Geary.Folder folder) {
- if (folder_information.has_key(folder)) {
- folder.email_locally_appended.disconnect(on_email_locally_appended);
- folder.email_flags_changed.disconnect(on_email_flags_changed);
- folder.email_removed.disconnect(on_email_removed);
-
- this.total_new_messages -= this.folder_information.get(folder).count;
-
- this.folder_information.unset(folder);
-
- folder_removed(folder);
+ /** Stops monitoring a folder for new messages. */
+ public void stop_monitoring_folder(Plugin.Folder target) {
+ Geary.Folder? folder = this.folders_factory.get_engine_folder(target);
+ if (folder != null) {
+ remove_folder(folder);
}
}
- internal void clear_folders() {
+ /** Determines if a folder is curently being monitored. */
+ public bool is_monitoring_folder(Plugin.Folder target) {
+ return this.folder_information.has_key(
+ this.folders_factory.get_engine_folder(target)
+ );
+ }
+
+ internal void destroy() {
+ this.folders_factory.destroy_folder_store(this.folders);
// Get an array so the loop does not blow up when removing values.
foreach (Geary.Folder monitored in this.folder_information.keys.to_array()) {
remove_folder(monitored);
}
}
- internal bool are_any_new_messages(Geary.Folder folder,
- Gee.Collection<Geary.EmailIdentifier> ids)
- throws Geary.EngineError.NOT_FOUND {
- MonitorInformation? info = folder_information.get(folder);
- if (info == null) {
- throw new Geary.EngineError.NOT_FOUND(
- "No such folder: %s", folder.path.to_string()
- );
+ internal void clear_new_messages(Geary.Folder location,
+ Gee.Set<Geary.App.Conversation>? visible) {
+ MonitorInformation? info = this.folder_information.get(location);
+ if (info != null) {
+ foreach (Geary.App.Conversation conversation in visible) {
+ if (Geary.traverse(
+ conversation.get_email_ids()
+ ).any((id) => info.recent_ids.contains(id))) {
+ Gee.Set<Geary.EmailIdentifier> old_ids = info.recent_ids;
+ info.recent_ids = new Gee.HashSet<Geary.EmailIdentifier>();
+ update_count(info, false, old_ids);
+ break;
+ }
+ }
}
- return Geary.traverse(ids).any((id) => info.new_ids.contains(id));
}
- internal void clear_new_messages(Geary.Folder folder)
- throws Geary.EngineError.NOT_FOUND {
- MonitorInformation? info = folder_information.get(folder);
- if (info == null) {
- throw new Geary.EngineError.NOT_FOUND(
- "No such folder: %s", folder.path.to_string()
- );
+ private void new_messages(MonitorInformation info,
+ Gee.Collection<Geary.Email> emails) {
+ Gee.Collection<Geary.EmailIdentifier> added =
+ new Gee.HashSet<Geary.EmailIdentifier>();
+ foreach (Geary.Email email in emails) {
+ if (email.email_flags.is_unread() &&
+ info.recent_ids.add(email.id)) {
+ added.add(email.id);
+ }
+ }
+ if (added.size > 0) {
+ update_count(info, true, added);
}
-
- info.new_ids.clear();
- last_new_message_folder = null;
- last_new_message = null;
-
- update_count(info, false, 0);
- }
-
- private void on_email_locally_appended(Geary.Folder folder,
- Gee.Collection<Geary.EmailIdentifier> email_ids) {
- do_process_new_email.begin(folder, email_ids);
- }
-
- private void on_email_flags_changed(Geary.Folder folder,
- Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> ids) {
- retire_new_messages(folder, ids.keys);
- }
-
- private void on_email_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
- retire_new_messages(folder, ids);
}
- private async void do_process_new_email(Geary.Folder folder,
- Gee.Collection<Geary.EmailIdentifier> email_ids) {
+ private void retire_new_messages(Geary.Folder folder,
+ Gee.Collection<Geary.EmailIdentifier> email_ids
+ ) {
MonitorInformation info = folder_information.get(folder);
-
- try {
- Gee.List<Geary.Email>? list = yield folder.list_email_by_sparse_id_async(email_ids,
- required_fields, Geary.Folder.ListFlags.NONE, info.cancellable);
- if (list == null || list.size == 0) {
- debug("Warning: %d new emails, but none could be listed", email_ids.size);
-
- return;
+ Gee.Collection<Geary.EmailIdentifier> removed =
+ new Gee.HashSet<Geary.EmailIdentifier>();
+ foreach (Geary.EmailIdentifier email_id in email_ids) {
+ if (info.recent_ids.remove(email_id)) {
+ removed.add(email_id);
}
+ }
- new_messages(info, list);
-
- debug("do_process_new_email: %d messages listed, %d unread in folder %s",
- list.size, info.count, folder.to_string());
- } catch (Error err) {
- debug("Unable to notify of new email: %s", err.message);
+ if (removed.size > 0) {
+ update_count(info, false, removed);
}
}
- private void new_messages(MonitorInformation info, Gee.Collection<Geary.Email> emails) {
- int appended_count = 0;
- foreach (Geary.Email email in emails) {
- if (!email.fields.fulfills(required_fields)) {
- debug("Warning: new message %s (%Xh) does not fulfill NewMessagesMonitor required fields of
%Xh",
- email.id.to_string(), email.fields, required_fields);
- }
-
- if (info.new_ids.contains(email.id))
- continue;
+ private void update_count(MonitorInformation info,
+ bool arrived,
+ Gee.Collection<Geary.EmailIdentifier> delta) {
+ Plugin.Folder folder =
+ this.folders_factory.get_plugin_folder(info.folder);
+ if (arrived) {
+ this.total_new_messages += delta.size;
+ new_messages_arrived(
+ folder,
+ info.recent_ids.size,
+ this.email.get_plugin_ids(delta, info.folder.account.information)
+ );
+ } else {
+ this.total_new_messages -= delta.size;
+ new_messages_retired(
+ folder, info.recent_ids.size
+ );
+ }
+ }
- if (!email.email_flags.is_unread())
- continue;
+ private void remove_folder(Geary.Folder target) {
+ MonitorInformation? info = this.folder_information.get(target);
+ if (info != null) {
+ target.email_locally_appended.disconnect(on_email_locally_appended);
+ target.email_flags_changed.disconnect(on_email_flags_changed);
+ target.email_removed.disconnect(on_email_removed);
- last_new_message_folder = info.folder;
- last_new_message = email;
+ this.total_new_messages -= info.recent_ids.size;
- info.new_ids.add(email.id);
- appended_count++;
+ this.folder_information.unset(target);
}
- update_count(info, true, appended_count);
}
- private void retire_new_messages(Geary.Folder folder,
- Gee.Collection<Geary.EmailIdentifier> email_ids) {
- MonitorInformation info = folder_information.get(folder);
-
- int removed_count = 0;
- foreach (Geary.EmailIdentifier email_id in email_ids) {
- if (last_new_message != null && last_new_message.id.equal_to(email_id)) {
- last_new_message_folder = null;
- last_new_message = null;
+ private async void do_process_new_email(
+ Geary.Folder folder,
+ Gee.Collection<Geary.EmailIdentifier> email_ids
+ ) {
+ MonitorInformation info = this.folder_information.get(folder);
+ if (info != null) {
+ Gee.List<Geary.Email>? list = null;
+ try {
+ list = yield folder.list_email_by_sparse_id_async(
+ email_ids,
+ REQUIRED_FIELDS,
+ NONE,
+ info.cancellable
+ );
+ } catch (GLib.Error err) {
+ warning(
+ "Unable to list new email for notification: %s", err.message
+ );
+ }
+ if (list != null && !list.is_empty) {
+ new_messages(info, list);
+ } else {
+ warning(
+ "%d new emails, but none could be listed for notification",
+ email_ids.size
+ );
}
-
- if (info.new_ids.remove(email_id))
- removed_count++;
}
-
- update_count(info, false, removed_count);
}
- private void update_count(MonitorInformation info, bool arrived, int delta) {
- int new_size = info.new_ids.size;
+ private void on_email_locally_appended(Geary.Folder folder,
+ Gee.Collection<Geary.EmailIdentifier> email_ids) {
+ do_process_new_email.begin(folder, email_ids);
+ }
- total_new_messages += new_size - info.count;
- info.count = new_size;
+ private void on_email_flags_changed(Geary.Folder folder,
+ Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> ids) {
+ retire_new_messages(folder, ids.keys);
+ }
- if (arrived)
- new_messages_arrived(info.folder, info.count, delta);
- else
- new_messages_retired(info.folder, info.count);
+ private void on_email_removed(Geary.Folder folder,
+ Gee.Collection<Geary.EmailIdentifier> ids) {
+ retire_new_messages(folder, ids);
}
}
diff --git a/src/client/application/application-plugin-manager.vala
b/src/client/application/application-plugin-manager.vala
index e052d0c0..bdae6aba 100644
--- a/src/client/application/application-plugin-manager.vala
+++ b/src/client/application/application-plugin-manager.vala
@@ -19,33 +19,48 @@ public class Application.PluginManager : GLib.Object {
"notification-badge"
};
+ /** Flags assigned to a plugin by the manager. */
+ [Flags]
+ public enum PluginFlags {
+ /** If set, the plugin is in the set of trusted plugins. */
+ TRUSTED;
+ }
+
+
private Client application;
- private Peas.Engine engine;
+ private Peas.Engine plugins;
private bool is_shutdown = false;
private string trusted_path;
+ private FolderStoreFactory folders_factory;
+
private Peas.ExtensionSet notification_extensions;
- private NotificationContext notifications;
+ private Gee.Set<NotificationContext> notification_contexts =
+ new Gee.HashSet<NotificationContext>();
- public PluginManager(Client application,
- NotificationContext notifications) {
+ public PluginManager(Client application) throws GLib.Error {
this.application = application;
- this.engine = Peas.Engine.get_default();
+ this.plugins = Peas.Engine.get_default();
+ this.folders_factory = new FolderStoreFactory(application.engine);
this.trusted_path = application.get_app_plugins_dir().get_path();
this.plugins.add_search_path(trusted_path, null);
- this.notifications = notifications;
this.notification_extensions = new Peas.ExtensionSet(
- this.engine,
- typeof(Plugin.Notification),
- "application", this.application,
- "context", this.notifications
+ this.plugins,
+ typeof(Plugin.Notification)
);
this.notification_extensions.extension_added.connect((info, extension) => {
Plugin.Notification? plugin = extension as Plugin.Notification;
if (plugin != null) {
+ var context = new NotificationContext(
+ this.application,
+ this.folders_factory,
+ to_plugin_flags(info)
+ );
+ this.notification_contexts.add(context);
+ plugin.notifications = context;
plugin.activate();
}
});
@@ -54,19 +69,22 @@ public class Application.PluginManager : GLib.Object {
if (plugin != null) {
plugin.deactivate(this.is_shutdown);
}
+ var context = plugin.notifications;
+ context.destroy();
+ this.notification_contexts.remove(context);
});
string[] optional_names = application.config.get_optional_plugins();
- foreach (Peas.PluginInfo info in this.engine.get_plugin_list()) {
+ foreach (Peas.PluginInfo info in this.plugins.get_plugin_list()) {
string name = info.get_module_name();
try {
if (info.is_available()) {
if (is_trusted(info)) {
debug("Loading trusted plugin: %s", name);
- this.engine.load_plugin(info);
+ this.plugins.load_plugin(info);
} else if (name in optional_names) {
debug("Loading optional plugin: %s", name);
- this.engine.load_plugin(info);
+ this.plugins.load_plugin(info);
}
}
} catch (GLib.Error err) {
@@ -82,9 +100,13 @@ public class Application.PluginManager : GLib.Object {
);
}
+ public inline PluginFlags to_plugin_flags(Peas.PluginInfo plugin) {
+ return is_trusted(plugin) ? PluginFlags.TRUSTED : 0;
+ }
+
public Gee.Collection<Peas.PluginInfo> get_optional_plugins() {
var plugins = new Gee.LinkedList<Peas.PluginInfo>();
- foreach (Peas.PluginInfo plugin in this.engine.get_plugin_list()) {
+ foreach (Peas.PluginInfo plugin in this.plugins.get_plugin_list()) {
try {
plugin.is_available();
if (!is_trusted(plugin)) {
@@ -145,4 +167,8 @@ public class Application.PluginManager : GLib.Object {
this.folders_factory.destroy();
}
+ internal Gee.Collection<NotificationContext> get_notification_contexts() {
+ return this.notification_contexts.read_only_view;
+ }
+
}
diff --git a/src/client/meson.build b/src/client/meson.build
index 847c8583..da3e81e5 100644
--- a/src/client/meson.build
+++ b/src/client/meson.build
@@ -9,6 +9,7 @@ geary_client_vala_sources = files(
'application/application-contact-store.vala',
'application/application-contact.vala',
'application/application-controller.vala',
+ 'application/application-folder-store-factory.vala',
'application/application-main-window.vala',
'application/application-notification-context.vala',
'application/application-plugin-manager.vala',
diff --git a/src/client/plugin/desktop-notifications/desktop-notifications.vala
b/src/client/plugin/desktop-notifications/desktop-notifications.vala
index 7b6c9822..95c0c9bc 100644
--- a/src/client/plugin/desktop-notifications/desktop-notifications.vala
+++ b/src/client/plugin/desktop-notifications/desktop-notifications.vala
@@ -1,6 +1,6 @@
/*
- * Copyright 2016 Software Freedom Conservancy Inc.
- * Copyright 2019 Michael Gratton <mike vee net>.
+ * Copyright © 2016 Software Freedom Conservancy Inc.
+ * Copyright © 2019-2020 Michael Gratton <mike vee net>.
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
@@ -21,33 +21,40 @@ public void peas_register_types(TypeModule module) {
public class Plugin.DesktopNotifications : Geary.BaseObject, Notification {
- public const Geary.Email.Field REQUIRED_FIELDS =
- Geary.Email.Field.ORIGINATORS | Geary.Email.Field.SUBJECT;
+ private const Geary.SpecialFolderType[] MONITORED_TYPES = {
+ INBOX, NONE
+ };
- public Application.Client application {
- get; construct set;
- }
-
- public Application.NotificationContext context {
- get; construct set;
+ public global::Application.NotificationContext notifications {
+ get; set;
}
private const string ARRIVED_ID = "email-arrived";
+ private global::Application.Client? application = null;
+ private EmailStore? email = null;
private GLib.Notification? arrived_notification = null;
private GLib.Cancellable? cancellable = null;
public override void activate() {
- this.context.add_required_fields(REQUIRED_FIELDS);
- this.context.new_messages_arrived.connect(on_new_messages_arrived);
+ try {
+ this.application = this.notifications.get_client_application();
+ } catch (GLib.Error error) {
+ warning(
+ "Failed obtain application instance: %s",
+ error.message
+ );
+ }
+
+ this.notifications.new_messages_arrived.connect(on_new_messages_arrived);
this.cancellable = new GLib.Cancellable();
+
+ this.connect_signals.begin();
}
public override void deactivate(bool is_shutdown) {
this.cancellable.cancel();
- this.context.new_messages_arrived.disconnect(on_new_messages_arrived);
- this.context.remove_required_fields(REQUIRED_FIELDS);
// Keep existing notifications if shutting down since they are
// persistent, but revoke if the plugin is being disabled.
@@ -56,96 +63,116 @@ public class Plugin.DesktopNotifications : Geary.BaseObject, Notification {
}
}
+ private async void connect_signals() {
+ try {
+ this.email = yield this.notifications.get_email();
+ } catch (GLib.Error error) {
+ warning(
+ "Unable to get folders for plugin: %s",
+ error.message
+ );
+ }
+
+ try {
+ FolderStore folders = yield this.notifications.get_folders();
+ folders.folders_available.connect(
+ (folders) => check_folders(folders)
+ );
+ folders.folders_unavailable.connect(
+ (folders) => check_folders(folders)
+ );
+ folders.folders_type_changed.connect(
+ (folders) => check_folders(folders)
+ );
+ check_folders(folders.get_folders());
+ } catch (GLib.Error error) {
+ warning(
+ "Unable to get folders for plugin: %s",
+ error.message
+ );
+ }
+ }
+
private void clear_arrived_notification() {
this.application.withdraw_notification(ARRIVED_ID);
this.arrived_notification = null;
}
- private void notify_new_mail(Geary.Folder folder, int added) {
- string body = ngettext(
- /// Notification body text for new email when no other
- /// new messages are already awaiting.
- "%d new message", "%d new messages", added
- ).printf(added);
+ private async void notify_specific_message(Folder folder,
+ int total,
+ Email email
+ ) throws GLib.Error {
+ string title = to_notitication_title(folder.account, total);
+ Geary.RFC822.MailboxAddress? originator = email.get_primary_originator();
+ if (originator != null) {
+ ContactStore contacts =
+ yield this.notifications.get_contacts_for_folder(folder);
+ global::Application.Contact? contact = yield contacts.load(
+ originator, this.cancellable
+ );
+ title = (
+ contact.is_trusted
+ ? contact.display_name
+ : originator.to_short_display()
+ );
+ }
- int total = 0;
- try {
- total = this.context.get_new_message_count(folder);
- } catch (Geary.EngineError err) {
- // All good
+ string body = email.subject;
+ if (total > 1) {
+ body = ngettext(
+ /// Notification body when a message as been received
+ /// and other unread messages have not been
+ /// seen. First string substitution is the message
+ /// subject, second is the number of unseen messages,
+ /// third is the name of the email account.
+ "%s\n(%d other new message for %s)",
+ "%s\n(%d other new messages for %s)",
+ total - 1
+ ).printf(
+ body,
+ total - 1,
+ folder.account.display_name
+ );
}
+ issue_arrived_notification(title, body, folder, email.identifier);
+ }
+
+ private void notify_general(Folder folder, int total, int added) {
+ string title = to_notitication_title(folder.account, total);
+ string body = ngettext(
+ /// Notification body when multiple messages have been
+ /// received at the same time and other unseen messages
+ /// exist. String substitution is the number of new
+ /// messages that have arrived.
+ "%d new message", "%d new messages", added
+ ).printf(added);
if (total > added) {
body = ngettext(
- /// Notification body text for new email when
- /// other new messages have already been notified
- /// about
+ /// Notification body when multiple messages have been
+ /// received at the same time and some unseen messages
+ /// already exist. String substitution is the message
+ /// above with the number of new messages that have
+ /// arrived, number substitution is the total number
+ /// of unseen messages.
"%s, %d new message total", "%s, %d new messages total",
total
).printf(body, total);
}
- issue_arrived_notification(
- folder.account.information.display_name, body, folder, null
- );
- }
-
- private async void notify_one_message(Geary.Folder folder,
- Geary.Email email,
- GLib.Cancellable? cancellable)
- throws GLib.Error {
- Geary.RFC822.MailboxAddress? originator =
- Util.Email.get_primary_originator(email);
- if (originator != null) {
- Application.ContactStore contacts =
- this.context.get_contact_store(folder.account);
- Application.Contact contact = yield contacts.load(
- originator, cancellable
- );
-
- int count = 1;
- try {
- count = this.context.get_new_message_count(folder);
- } catch (Geary.EngineError.NOT_FOUND err) {
- // All good
- }
-
- string body = "";
- if (count <= 1) {
- body = Util.Email.strip_subject_prefixes(email);
- } else {
- body = ngettext(
- "%s\n(%d other new message for %s)",
- "%s\n(%d other new messages for %s)", count - 1).printf(
- Util.Email.strip_subject_prefixes(email),
- count - 1,
- folder.account.information.display_name
- );
- }
-
- issue_arrived_notification(
- contact.is_trusted
- ? contact.display_name : originator.to_short_display(),
- body,
- folder,
- email.id
- );
- } else {
- notify_new_mail(folder, 1);
- }
+ issue_arrived_notification(title, body, folder, null);
}
private void issue_arrived_notification(string summary,
string body,
- Geary.Folder folder,
- Geary.EmailIdentifier? id) {
+ Folder folder,
+ EmailIdentifier? id) {
// only one outstanding notification at a time
clear_arrived_notification();
string? action = null;
GLib.Variant[] target_param = new GLib.Variant[] {
- folder.account.information.id,
- new GLib.Variant.variant(folder.path.to_variant())
+ new GLib.Variant.variant(folder.to_variant())
};
if (id == null) {
@@ -172,11 +199,13 @@ public class Plugin.DesktopNotifications : Geary.BaseObject, Notification {
GLib.Notification notification = new GLib.Notification(summary);
notification.set_body(body);
notification.set_icon(
- new GLib.ThemedIcon("%s-symbolic".printf(Application.Client.APP_ID))
+ new GLib.ThemedIcon(
+ "%s-symbolic".printf(global::Application.Client.APP_ID)
+ )
);
- /* We do not show notification action under Unity */
-
+ // Do not show notification actions under Unity, it's
+ // notifications daemon doesn't support them.
if (this.application.config.desktop_environment == UNITY) {
this.application.send_notification(id, notification);
return notification;
@@ -192,22 +221,60 @@ public class Plugin.DesktopNotifications : Geary.BaseObject, Notification {
}
}
- private void on_new_messages_arrived(Geary.Folder folder,
- int total,
- int added) {
- if (this.context.should_notify_new_messages(folder)) {
- if (added == 1 &&
- this.context.last_new_message_folder != null &&
- this.context.last_new_message != null) {
- this.notify_one_message.begin(
- this.context.last_new_message_folder,
- this.context.last_new_message,
- this.cancellable
- );
- } else if (added > 0) {
- notify_new_mail(folder, added);
+ private async void handle_new_messages(Folder folder,
+ int total,
+ Gee.Collection<EmailIdentifier> added) {
+ if (this.notifications.should_notify_new_messages(folder)) {
+ // notify about a specific message if it's the only one
+ // present and it can be loaded, otherwise notify
+ // generally
+ bool notified = false;
+ if (this.email != null &&
+ added.size == 1) {
+ try {
+ Email? message = Geary.Collection.first(
+ yield this.email.get_email(added, this.cancellable)
+ );
+ if (message != null) {
+ yield notify_specific_message(folder, total, message);
+ notified = true;
+ } else {
+ warning("Could not load email for notification");
+ }
+ } catch (GLib.Error error) {
+ warning("Error loading email for notification: %s", error.message);
+ }
+ }
+
+ if (!notified) {
+ notify_general(folder, total, added.size);
}
}
}
+ private void check_folders(Gee.Collection<Folder> folders) {
+ foreach (Folder folder in folders) {
+ if (folder.folder_type in MONITORED_TYPES) {
+ this.notifications.start_monitoring_folder(folder);
+ } else {
+ this.notifications.stop_monitoring_folder(folder);
+ }
+ }
+ }
+
+ private inline string to_notitication_title(Account account, int count) {
+ return ngettext(
+ /// Notification title when new messages have been
+ /// received. String substitution is the name of the email
+ /// account.
+ "New message for %s", "New messages for %s", count
+ ).printf(account.display_name);
+ }
+
+ private void on_new_messages_arrived(Folder folder,
+ int total,
+ Gee.Collection<EmailIdentifier> added) {
+ this.handle_new_messages.begin(folder, total, added);
+ }
+
}
diff --git a/src/client/plugin/messaging-menu/messaging-menu.vala
b/src/client/plugin/messaging-menu/messaging-menu.vala
index 6fd3b69f..a97840cd 100644
--- a/src/client/plugin/messaging-menu/messaging-menu.vala
+++ b/src/client/plugin/messaging-menu/messaging-menu.vala
@@ -1,6 +1,6 @@
/*
- * Copyright 2016 Software Freedom Conservancy Inc.
- * Copyright 2019 Michael Gratton <mike vee net>.
+ * Copyright © 2016 Software Freedom Conservancy Inc.
+ * Copyright © 2019-2020 Michael Gratton <mike vee net>.
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
@@ -19,66 +19,56 @@ public void peas_register_types(TypeModule module) {
public class Plugin.MessagingMenu : Geary.BaseObject, Notification {
- public Application.Client application {
- get; construct set;
+ public global::Application.NotificationContext notifications {
+ get; set;
}
- public Application.NotificationContext context {
- get; construct set;
- }
private global::MessagingMenu.App? app = null;
+ private FolderStore? folders = null;
public override void activate() {
this.app = new global::MessagingMenu.App(
- "%s.desktop".printf(Application.Client.APP_ID)
+ "%s.desktop".printf(global::Application.Client.APP_ID)
);
this.app.register();
this.app.activate_source.connect(on_activate_source);
- this.context.folder_removed.connect(on_folder_removed);
- this.context.new_messages_arrived.connect(on_new_messages_changed);
- this.context.new_messages_retired.connect(on_new_messages_changed);
+ this.notifications.new_messages_arrived.connect(on_new_messages_changed);
+ this.notifications.new_messages_retired.connect(on_new_messages_changed);
+ this.connect_folders.begin();
}
public override void deactivate(bool is_shutdown) {
- this.context.folder_removed.disconnect(on_folder_removed);
- this.context.new_messages_arrived.disconnect(on_new_messages_changed);
- this.context.new_messages_retired.disconnect(on_new_messages_changed);
-
this.app.activate_source.disconnect(on_activate_source);
this.app.unregister();
this.app = null;
}
- private string get_source_id(Geary.Folder folder) {
- return "new-messages-id-%s-%s".printf(folder.account.information.id, folder.path.to_string());
- }
-
- private void on_activate_source(string source_id) {
- foreach (Geary.Folder folder in this.context.get_folders()) {
- if (source_id == get_source_id(folder)) {
- this.application.show_folder.begin(folder);
- break;
- }
+ private async void connect_folders() {
+ try {
+ this.folders = yield this.notifications.get_folders();
+ folders.folders_available.connect(
+ (folders) => check_folders(folders)
+ );
+ folders.folders_unavailable.connect(
+ (folders) => check_folders(folders)
+ );
+ folders.folders_type_changed.connect(
+ (folders) => check_folders(folders)
+ );
+ check_folders(folders.get_folders());
+ } catch (GLib.Error error) {
+ warning(
+ "Unable to get folders for plugin: %s",
+ error.message
+ );
}
}
- private void on_new_messages_changed(Geary.Folder folder, int count) {
- if (count > 0) {
- show_new_messages_count(folder, count);
- } else {
- remove_new_messages_count(folder);
- }
- }
-
- private void on_folder_removed(Geary.Folder folder) {
- remove_new_messages_count(folder);
- }
-
- private void show_new_messages_count(Geary.Folder folder, int count) {
- if (this.context.should_notify_new_messages(folder)) {
+ private void show_new_messages_count(Folder folder, int count) {
+ if (this.notifications.should_notify_new_messages(folder)) {
string source_id = get_source_id(folder);
if (this.app.has_source(source_id)) {
@@ -87,7 +77,7 @@ public class Plugin.MessagingMenu : Geary.BaseObject, Notification {
this.app.append_source_with_count(
source_id,
null,
- _("%s — New Messages").printf(folder.account.information.display_name),
+ _("%s — New Messages").printf(folder.display_name),
count);
}
@@ -95,7 +85,7 @@ public class Plugin.MessagingMenu : Geary.BaseObject, Notification {
}
}
- private void remove_new_messages_count(Geary.Folder folder) {
+ private void remove_new_messages_count(Folder folder) {
string source_id = get_source_id(folder);
if (this.app.has_source(source_id)) {
this.app.remove_attention(source_id);
@@ -103,4 +93,37 @@ public class Plugin.MessagingMenu : Geary.BaseObject, Notification {
}
}
+ private string get_source_id(Folder folder) {
+ return "geary%s".printf(folder.to_variant().print(false));
+ }
+
+ private void on_activate_source(string source_id) {
+ if (this.folders != null) {
+ foreach (Folder folder in this.folders.get_folders()) {
+ if (source_id == get_source_id(folder)) {
+ this.notifications.plugin_application.show_folder(folder);
+ break;
+ }
+ }
+ }
+ }
+
+ private void on_new_messages_changed(Folder folder, int count) {
+ if (count > 0) {
+ show_new_messages_count(folder, count);
+ } else {
+ remove_new_messages_count(folder);
+ }
+ }
+
+ private void check_folders(Gee.Collection<Folder> folders) {
+ foreach (Folder folder in folders) {
+ if (folder.folder_type == INBOX) {
+ this.notifications.start_monitoring_folder(folder);
+ } else if (this.notifications.is_monitoring_folder(folder)) {
+ this.notifications.stop_monitoring_folder(folder);
+ }
+ }
+ }
+
}
diff --git a/src/client/plugin/notification-badge/notification-badge.vala
b/src/client/plugin/notification-badge/notification-badge.vala
index b3b343a7..661ad662 100644
--- a/src/client/plugin/notification-badge/notification-badge.vala
+++ b/src/client/plugin/notification-badge/notification-badge.vala
@@ -1,6 +1,6 @@
/*
- * Copyright 2016 Software Freedom Conservancy Inc.
- * Copyright 2019 Michael Gratton <mike vee net>.
+ * Copyright © 2016 Software Freedom Conservancy Inc.
+ * Copyright © 2019-2020 Michael Gratton <mike vee net>.
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
@@ -19,21 +19,22 @@ public void peas_register_types(TypeModule module) {
public class Plugin.NotificationBadge : Geary.BaseObject, Notification {
- public Application.Client application {
- get; construct set;
- }
+ private const Geary.SpecialFolderType[] MONITORED_TYPES = {
+ INBOX, NONE
+ };
- public Application.NotificationContext context {
- get; construct set;
+ public global::Application.NotificationContext notifications {
+ get; set;
}
private UnityLauncherEntry? entry = null;
public override void activate() {
- var connection = this.application.get_dbus_connection();
- var path = this.application.get_dbus_object_path();
try {
+ var application = this.notifications.get_client_application();
+ var connection = application.get_dbus_connection();
+ var path = application.get_dbus_object_path();
if (connection == null || path == null) {
throw new GLib.IOError.NOT_CONNECTED(
"Application does not have a DBus connection or path"
@@ -42,7 +43,7 @@ public class Plugin.NotificationBadge : Geary.BaseObject, Notification {
this.entry = new UnityLauncherEntry(
connection,
path + "/plugin/notificationbadge",
- Application.Client.APP_ID + ".desktop"
+ global::Application.Client.APP_ID + ".desktop"
);
} catch (GLib.Error error) {
warning(
@@ -51,18 +52,53 @@ public class Plugin.NotificationBadge : Geary.BaseObject, Notification {
);
}
- this.context.notify["total-new-messages"].connect(on_total_changed);
- update_count();
+ connect_folders.begin();
}
public override void deactivate(bool is_shutdown) {
- this.context.notify["total-new-messages"].disconnect(on_total_changed);
+ this.notifications.notify["total-new-messages"].disconnect(
+ on_total_changed
+ );
this.entry = null;
}
+ public async void connect_folders() {
+ try {
+ FolderStore folders = yield this.notifications.get_folders();
+ folders.folders_available.connect(
+ (folders) => check_folders(folders)
+ );
+ folders.folders_unavailable.connect(
+ (folders) => check_folders(folders)
+ );
+ folders.folders_type_changed.connect(
+ (folders) => check_folders(folders)
+ );
+ check_folders(folders.get_folders());
+ } catch (GLib.Error error) {
+ warning(
+ "Unable to get folders for plugin: %s",
+ error.message
+ );
+ }
+
+ this.notifications.notify["total-new-messages"].connect(on_total_changed);
+ update_count();
+ }
+
+ private void check_folders(Gee.Collection<Folder> folders) {
+ foreach (Folder folder in folders) {
+ if (folder.folder_type in MONITORED_TYPES) {
+ this.notifications.start_monitoring_folder(folder);
+ } else {
+ this.notifications.stop_monitoring_folder(folder);
+ }
+ }
+ }
+
private void update_count() {
if (this.entry != null) {
- int count = this.context.total_new_messages;
+ int count = this.notifications.total_new_messages;
if (count > 0) {
this.entry.set_count(count);
} else {
diff --git a/src/client/plugin/plugin-notification.vala b/src/client/plugin/plugin-notification.vala
index fd409644..b00549f9 100644
--- a/src/client/plugin/plugin-notification.vala
+++ b/src/client/plugin/plugin-notification.vala
@@ -10,14 +10,9 @@
*/
public interface Plugin.Notification : Geary.BaseObject {
- /** The application instance containing the plugin. */
- public abstract Application.Client application {
- get; construct set;
- }
-
/** Context object for notifications. */
- public abstract Application.NotificationContext context {
- get; construct set;
+ public abstract global::Application.NotificationContext notifications {
+ get; set;
}
/* Invoked to activate the plugin, after loading. */
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]