Merge branch 'mjog/user-plugins' into 'mainline'
Support optional (non-builtin) plugins See merge request GNOME/geary!441
This commit is contained in:
commit
3f81b7d507
40 changed files with 2152 additions and 713 deletions
|
|
@ -89,6 +89,14 @@
|
|||
<translation type="gettext">geary</translation>
|
||||
|
||||
<releases>
|
||||
<release version="3.38" date="">
|
||||
<description>
|
||||
<p>Enhancements included in this release:</p>
|
||||
<ul>
|
||||
<li>New preferences pane for managing plugins</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.36" date="2020-03-13">
|
||||
<description>
|
||||
<p>Enhancements included in this release:</p>
|
||||
|
|
|
|||
|
|
@ -141,6 +141,12 @@
|
|||
be displayed.</description>
|
||||
</key>
|
||||
|
||||
<key name="optional-plugins" type="as">
|
||||
<default>[]</default>
|
||||
<summary>List of optional plugins</summary>
|
||||
<description>Plugins listed here will be loaded on startup.</description>
|
||||
</key>
|
||||
|
||||
<key name="migrated-config" type="b">
|
||||
<default>false</default>
|
||||
<summary>Whether we migrated the old settings</summary>
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ json_glib = dependency('json-glib-1.0', version: '>= 1.0')
|
|||
libhandy = dependency('libhandy-0.0', version: '>= 0.0.10')
|
||||
libmath = cc.find_library('m')
|
||||
libpeas = dependency('libpeas-1.0', version: '>= 1.24.0')
|
||||
libpeas_gtk = dependency('libpeas-gtk-1.0', version: '>= 1.24.0')
|
||||
libsecret = dependency('libsecret-1', version: '>= 0.11')
|
||||
libsoup = dependency('libsoup-2.4', version: '>= 2.48')
|
||||
libunwind_dep = dependency(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -83,13 +84,25 @@ src/client/folder-list/folder-list-inboxes-branch.vala
|
|||
src/client/folder-list/folder-list-search-branch.vala
|
||||
src/client/folder-list/folder-list-special-grouping.vala
|
||||
src/client/folder-list/folder-list-tree.vala
|
||||
src/client/plugin/plugin-account.vala
|
||||
src/client/plugin/plugin-application.vala
|
||||
src/client/plugin/plugin-contact-store.vala
|
||||
src/client/plugin/plugin-email-store.vala
|
||||
src/client/plugin/plugin-email.vala
|
||||
src/client/plugin/plugin-error.vala
|
||||
src/client/plugin/plugin-folder-store.vala
|
||||
src/client/plugin/plugin-folder.vala
|
||||
src/client/plugin/plugin-notification-etension.vala
|
||||
src/client/plugin/plugin-plugin-base.vala
|
||||
src/client/plugin/plugin-trusted-etension.vala
|
||||
src/client/plugin/desktop-notifications/desktop-notifications.plugin.in
|
||||
src/client/plugin/desktop-notifications/desktop-notifications.vala
|
||||
src/client/plugin/folder-highlight/folder-highlight.plugin.in
|
||||
src/client/plugin/folder-highlight/folder-highlight.vala
|
||||
src/client/plugin/messaging-menu/messaging-menu.plugin.in
|
||||
src/client/plugin/messaging-menu/messaging-menu.vala
|
||||
src/client/plugin/notification-badge/notification-badge.plugin.in
|
||||
src/client/plugin/notification-badge/notification-badge.vala
|
||||
src/client/plugin/plugin-notification.vala
|
||||
src/client/sidebar/sidebar-branch.vala
|
||||
src/client/sidebar/sidebar-common.vala
|
||||
src/client/sidebar/sidebar-count-cell-renderer.vala
|
||||
|
|
|
|||
|
|
@ -82,8 +82,8 @@ public class Application.Client : Gtk.Application {
|
|||
{ Action.Application.NEW_WINDOW, on_activate_new_window },
|
||||
{ Action.Application.PREFERENCES, on_activate_preferences},
|
||||
{ Action.Application.QUIT, on_activate_quit},
|
||||
{ Action.Application.SHOW_EMAIL, on_activate_show_email, "(svv)"},
|
||||
{ Action.Application.SHOW_FOLDER, on_activate_show_folder, "(sv)"}
|
||||
{ Action.Application.SHOW_EMAIL, on_activate_show_email, "(vv)"},
|
||||
{ Action.Application.SHOW_FOLDER, on_activate_show_folder, "(v)"}
|
||||
};
|
||||
|
||||
// This is also the order in which they are presented to the user,
|
||||
|
|
@ -603,13 +603,13 @@ public class Application.Client : Gtk.Application {
|
|||
this.controller.expunge_accounts.begin();
|
||||
}
|
||||
|
||||
public async void show_email(Geary.Folder? folder,
|
||||
public async void show_email(Geary.Folder folder,
|
||||
Geary.EmailIdentifier id) {
|
||||
MainWindow main = yield this.present();
|
||||
main.show_email.begin(folder, Geary.Collection.single(id), true);
|
||||
}
|
||||
|
||||
public async void show_folder(Geary.Folder? folder) {
|
||||
public async void show_folder(Geary.Folder folder) {
|
||||
MainWindow main = yield this.present();
|
||||
yield main.select_folder(folder, true);
|
||||
}
|
||||
|
|
@ -632,7 +632,8 @@ public class Application.Client : Gtk.Application {
|
|||
yield this.present();
|
||||
|
||||
Components.PreferencesWindow prefs = new Components.PreferencesWindow(
|
||||
get_active_main_window()
|
||||
get_active_main_window(),
|
||||
this.controller.plugins
|
||||
);
|
||||
prefs.show();
|
||||
}
|
||||
|
|
@ -1021,12 +1022,13 @@ public class Application.Client : Gtk.Application {
|
|||
|
||||
private Geary.Folder? get_folder_from_action_target(GLib.Variant target) {
|
||||
Geary.Folder? folder = null;
|
||||
string id = (string) target.get_child_value(0);
|
||||
GLib.Variant param = target.get_child_value(0).get_variant();
|
||||
string id = (string) param.get_child_value(0);
|
||||
try {
|
||||
Geary.Account account = this.engine.get_account_for_id(id);
|
||||
Geary.FolderPath? path =
|
||||
account.to_folder_path(
|
||||
target.get_child_value(1).get_variant()
|
||||
param.get_child_value(1).get_variant()
|
||||
);
|
||||
folder = account.get_folder(path);
|
||||
} catch (GLib.Error err) {
|
||||
|
|
@ -1088,13 +1090,12 @@ public class Application.Client : Gtk.Application {
|
|||
private void on_activate_show_email(GLib.SimpleAction action,
|
||||
GLib.Variant? target) {
|
||||
if (target != null) {
|
||||
// Target is a (account_id,folder_path,email_id) tuple
|
||||
Geary.Folder? folder = get_folder_from_action_target(target);
|
||||
Geary.EmailIdentifier? email_id = null;
|
||||
if (folder != null) {
|
||||
try {
|
||||
email_id = folder.account.to_email_identifier(
|
||||
target.get_child_value(2).get_variant()
|
||||
target.get_child_value(1).get_variant()
|
||||
);
|
||||
} catch (GLib.Error err) {
|
||||
debug("Could not find email id: %s", err.message);
|
||||
|
|
@ -1110,7 +1111,6 @@ public class Application.Client : Gtk.Application {
|
|||
private void on_activate_show_folder(GLib.SimpleAction action,
|
||||
GLib.Variant? target) {
|
||||
if (target != null) {
|
||||
// Target is a (account_id,folder_path) tuple
|
||||
Geary.Folder? folder = get_folder_from_action_target(target);
|
||||
if (folder != null) {
|
||||
this.show_folder.begin(folder);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ public class Application.Configuration : Geary.BaseObject {
|
|||
public const string FOLDER_LIST_PANE_POSITION_VERTICAL_KEY = "folder-list-pane-position-vertical";
|
||||
public const string FORMATTING_TOOLBAR_VISIBLE = "formatting-toolbar-visible";
|
||||
public const string MESSAGES_PANE_POSITION_KEY = "messages-pane-position";
|
||||
public const string OPTIONAL_PLUGINS = "optional-plugins";
|
||||
public const string SEARCH_STRATEGY_KEY = "search-strategy";
|
||||
public const string SINGLE_KEY_SHORTCUTS = "single-key-shortcuts";
|
||||
public const string SPELL_CHECK_LANGUAGES = "spell-check-languages";
|
||||
|
|
@ -204,6 +205,20 @@ public class Application.Configuration : Geary.BaseObject {
|
|||
this.settings.set_value(COMPOSER_WINDOW_SIZE_KEY, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of optional plugins to load by default
|
||||
*/
|
||||
public string[] get_optional_plugins() {
|
||||
return this.settings.get_strv(OPTIONAL_PLUGINS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of optional plugins to load by default
|
||||
*/
|
||||
public void set_optional_plugins(string[] value) {
|
||||
this.settings.set_strv(OPTIONAL_PLUGINS, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns enabled spell checker languages.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -75,13 +75,6 @@ public class Application.ContactStore : Geary.BaseObject {
|
|||
);
|
||||
}
|
||||
|
||||
/** Closes the store, flushing all caches. */
|
||||
public void close() {
|
||||
this.folks_address_cache.clear();
|
||||
this.contact_id_cache.clear();
|
||||
this.engine_address_cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a contact for a specific mailbox.
|
||||
*
|
||||
|
|
@ -183,6 +176,13 @@ public class Application.ContactStore : Geary.BaseObject {
|
|||
return results;
|
||||
}
|
||||
|
||||
/** Closes the store, flushing all caches. */
|
||||
internal void close() {
|
||||
this.folks_address_cache.clear();
|
||||
this.contact_id_cache.clear();
|
||||
this.engine_address_cache.clear();
|
||||
}
|
||||
|
||||
internal async Geary.Contact
|
||||
lookup_engine_contact(Geary.RFC822.MailboxAddress mailbox,
|
||||
GLib.Cancellable cancellable)
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@ internal class Application.Controller : Geary.BaseObject {
|
|||
/** Account management for the application. */
|
||||
public Accounts.Manager account_manager { get; private set; }
|
||||
|
||||
/** Plugin manager for the application. */
|
||||
public PluginManager plugins { get; private set; }
|
||||
|
||||
/** Certificate management for the application. */
|
||||
public Application.CertificateManager certificate_manager {
|
||||
get; private set;
|
||||
|
|
@ -82,8 +85,6 @@ internal class Application.Controller : Geary.BaseObject {
|
|||
private UpgradeDialog upgrade_dialog;
|
||||
private Folks.IndividualAggregator folks;
|
||||
|
||||
private PluginManager plugin_manager;
|
||||
|
||||
// List composers that have not yet been closed
|
||||
private Gee.Collection<Composer.Widget> composer_widgets =
|
||||
new Gee.LinkedList<Composer.Widget>();
|
||||
|
|
@ -162,13 +163,7 @@ internal class Application.Controller : Geary.BaseObject {
|
|||
|
||||
}
|
||||
|
||||
this.plugin_manager = new PluginManager(application);
|
||||
this.plugin_manager.notifications = new NotificationContext(
|
||||
this.avatars,
|
||||
this.get_contact_store_for_account,
|
||||
this.should_notify_new_messages
|
||||
);
|
||||
this.plugin_manager.load();
|
||||
this.plugins = new PluginManager(this.application);
|
||||
|
||||
// Migrate configuration if necessary.
|
||||
Migrate.xdg_config_dir(this.application.get_user_data_directory(),
|
||||
|
|
@ -267,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
|
||||
|
|
@ -295,11 +290,15 @@ 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.plugin_manager.notifications.clear_folders();
|
||||
try {
|
||||
this.plugins.close();
|
||||
} catch (GLib.Error err) {
|
||||
warning("Error closing plugin manager: %s", err.message);
|
||||
}
|
||||
this.avatars.close();
|
||||
this.pending_mailtos.clear();
|
||||
this.composer_widgets.clear();
|
||||
|
|
@ -323,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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -859,9 +858,6 @@ internal class Application.Controller : Geary.BaseObject {
|
|||
|
||||
internal void register_window(MainWindow window) {
|
||||
window.retry_service_problem.connect(on_retry_service_problem);
|
||||
window.folder_list.set_new_messages_monitor(
|
||||
this.plugin_manager.notifications
|
||||
);
|
||||
}
|
||||
|
||||
internal void unregister_window(MainWindow window) {
|
||||
|
|
@ -1249,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.plugin_manager.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.plugin_manager.notifications.add_folder(
|
||||
folder, this.accounts.get(info).cancellable
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_folders_available_unavailable(
|
||||
Geary.Account account,
|
||||
Gee.BidirSortedSet<Geary.Folder>? available,
|
||||
|
|
@ -1287,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.plugin_manager.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.plugin_manager.notifications.add_folder(
|
||||
folder, cancellable
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1324,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.plugin_manager.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.plugin_manager.notifications.remove_folder(folder);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
has_prev = unavailable_iterator.previous();
|
||||
|
|
@ -1351,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.plugin_manager.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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1570,7 +1472,7 @@ internal class Application.Controller : Geary.BaseObject {
|
|||
|
||||
AccountContext? context = this.accounts.get(service.account);
|
||||
if (context != null) {
|
||||
this.plugin_manager.notifications.email_sent(context.account, sent);
|
||||
//this.notifications.email_sent(context.account, sent);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
282
src/client/application/application-folder-store-factory.vala
Normal file
282
src/client/application/application-folder-store-factory.vala
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -1,206 +1,475 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides a context for notification plugins.
|
||||
*
|
||||
* The context provides an interface for notification plugins to
|
||||
* interface with the Geary client application. Notification plugins
|
||||
* will be passed an instance of this class as the `context`
|
||||
* parameter.
|
||||
*
|
||||
* 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.
|
||||
* Implementation of the notification extension context.
|
||||
*/
|
||||
public class Application.NotificationContext : Geary.BaseObject {
|
||||
internal class Application.NotificationContext :
|
||||
Geary.BaseObject, Plugin.NotificationContext {
|
||||
|
||||
|
||||
/** 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;
|
||||
|
||||
/** Monitor hook to determine if a folder should be notified about. */
|
||||
internal delegate bool ShouldNotifyNewMessages(Geary.Folder folder);
|
||||
|
||||
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 {
|
||||
|
||||
|
||||
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; }
|
||||
public int total_new_messages { get { return this._total_new_messages; } }
|
||||
public int _total_new_messages = 0;
|
||||
|
||||
/**
|
||||
* Folder containing the recent new message received, if any.
|
||||
*
|
||||
* @see last_new_message
|
||||
*/
|
||||
public Geary.Folder? last_new_message_folder {
|
||||
get; private set; default = null;
|
||||
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;
|
||||
|
||||
|
||||
internal NotificationContext(Client application,
|
||||
FolderStoreFactory folders_factory) {
|
||||
this.application = application;
|
||||
this.folders_factory = folders_factory;
|
||||
this.folders = folders_factory.new_folder_store();
|
||||
this.email = new EmailStoreImpl(application);
|
||||
}
|
||||
|
||||
public async Plugin.EmailStore get_email()
|
||||
throws Plugin.Error.PERMISSION_DENIED {
|
||||
return this.email;
|
||||
}
|
||||
|
||||
public async Plugin.FolderStore get_folders()
|
||||
throws Plugin.Error.PERMISSION_DENIED {
|
||||
return this.folders;
|
||||
}
|
||||
|
||||
public async Plugin.ContactStore get_contacts_for_folder(Plugin.Folder source)
|
||||
throws Plugin.Error.NOT_FOUND, Plugin.Error.PERMISSION_DENIED {
|
||||
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 Plugin.Error.NOT_FOUND(
|
||||
"No account for folder: %s", source.display_name
|
||||
);
|
||||
}
|
||||
return new ContactStoreImpl(context.contacts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Most recent new message received, if any.
|
||||
* Determines if notifications should be made for a specific folder.
|
||||
*
|
||||
* @see last_new_message_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 Geary.Email? last_new_message {
|
||||
get; private set; default = null;
|
||||
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 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>();
|
||||
|
||||
private unowned GetContactStore contact_store_delegate;
|
||||
private unowned ShouldNotifyNewMessages notify_delegate;
|
||||
|
||||
|
||||
/** 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. */
|
||||
public signal void new_messages_arrived(Geary.Folder parent, int total, int added);
|
||||
|
||||
/** 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(AvatarStore avatars,
|
||||
GetContactStore contact_store_delegate,
|
||||
ShouldNotifyNewMessages notify_delegate) {
|
||||
this.avatars = avatars;
|
||||
this.contact_store_delegate = contact_store_delegate;
|
||||
this.notify_delegate = notify_delegate;
|
||||
}
|
||||
|
||||
/** 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 contact store to lookup contacts for notifications. */
|
||||
public Application.ContactStore? get_contact_store(Geary.Account account) {
|
||||
return this.contact_store_delegate(account);
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
/** Returns the new message count for a specific folder. */
|
||||
public int get_new_message_count(Geary.Folder folder)
|
||||
throws Geary.EngineError.NOT_FOUND {
|
||||
MonitorInformation? info = folder_information.get(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 Plugin.Error.NOT_FOUND {
|
||||
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(
|
||||
throw new Plugin.Error.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);
|
||||
}
|
||||
}
|
||||
|
||||
private void retire_new_messages(Geary.Folder folder,
|
||||
Gee.Collection<Geary.EmailIdentifier> email_ids
|
||||
) {
|
||||
MonitorInformation info = folder_information.get(folder);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
info.new_ids.clear();
|
||||
last_new_message_folder = null;
|
||||
last_new_message = null;
|
||||
if (removed.size > 0) {
|
||||
update_count(info, false, removed);
|
||||
}
|
||||
}
|
||||
|
||||
update_count(info, false, 0);
|
||||
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
|
||||
);
|
||||
}
|
||||
notify_property("total-new-messages");
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (!info.recent_ids.is_empty) {
|
||||
this._total_new_messages -= info.recent_ids.size;
|
||||
notify_property("total-new-messages");
|
||||
}
|
||||
|
||||
this.folder_information.unset(target);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void on_email_locally_appended(Geary.Folder folder,
|
||||
|
|
@ -213,84 +482,9 @@ public class Application.NotificationContext : Geary.BaseObject {
|
|||
retire_new_messages(folder, ids.keys);
|
||||
}
|
||||
|
||||
private void on_email_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (!email.email_flags.is_unread())
|
||||
continue;
|
||||
|
||||
last_new_message_folder = info.folder;
|
||||
last_new_message = email;
|
||||
|
||||
info.new_ids.add(email.id);
|
||||
appended_count++;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
total_new_messages += new_size - info.count;
|
||||
info.count = new_size;
|
||||
|
||||
if (arrived)
|
||||
new_messages_arrived(info.folder, info.count, delta);
|
||||
else
|
||||
new_messages_retired(info.folder, info.count);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2019 Michael Gratton <mike@vee.net>
|
||||
* 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.
|
||||
|
|
@ -11,57 +11,293 @@
|
|||
public class Application.PluginManager : GLib.Object {
|
||||
|
||||
|
||||
public NotificationContext notifications { get; set; }
|
||||
|
||||
private Client application;
|
||||
private Peas.Engine engine;
|
||||
private Peas.ExtensionSet? notification_extensions = null;
|
||||
private bool is_shutdown = false;
|
||||
// Plugins that will be loaded automatically when the client
|
||||
// application stats up
|
||||
private const string[] AUTOLOAD_MODULES = {
|
||||
"desktop-notifications",
|
||||
"folder-highlight",
|
||||
"notification-badge",
|
||||
};
|
||||
|
||||
|
||||
public PluginManager(Client application) {
|
||||
this.application = application;
|
||||
this.engine = Peas.Engine.get_default();
|
||||
this.engine.add_search_path(
|
||||
application.get_app_plugins_dir().get_path(), null
|
||||
);
|
||||
private class PluginContext {
|
||||
|
||||
|
||||
public Peas.PluginInfo info { get; private set; }
|
||||
public Plugin.PluginBase plugin { get; private set; }
|
||||
|
||||
|
||||
public PluginContext(Peas.PluginInfo info, Plugin.PluginBase plugin) {
|
||||
this.info = info;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public async void activate() throws GLib.Error {
|
||||
yield this.plugin.activate();
|
||||
}
|
||||
|
||||
public async void deactivate(bool is_shutdown) throws GLib.Error {
|
||||
yield this.plugin.deactivate(is_shutdown);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void load() {
|
||||
this.notification_extensions = new Peas.ExtensionSet(
|
||||
this.engine,
|
||||
typeof(Plugin.Notification),
|
||||
"application", this.application,
|
||||
"context", this.notifications
|
||||
);
|
||||
this.notification_extensions.extension_added.connect((info, extension) => {
|
||||
Plugin.Notification? plugin = extension as Plugin.Notification;
|
||||
if (plugin != null) {
|
||||
plugin.activate();
|
||||
}
|
||||
});
|
||||
this.notification_extensions.extension_removed.connect((info, extension) => {
|
||||
Plugin.Notification? plugin = extension as Plugin.Notification;
|
||||
if (plugin != null) {
|
||||
plugin.deactivate(this.is_shutdown);
|
||||
}
|
||||
});
|
||||
|
||||
// Load built-in plugins by default
|
||||
foreach (Peas.PluginInfo info in this.engine.get_plugin_list()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/** Emitted when a plugin is successfully loaded and activated. */
|
||||
public signal void plugin_activated(Peas.PluginInfo info);
|
||||
|
||||
/** Emitted when a plugin raised an error loading or activating. */
|
||||
public signal void plugin_error(Peas.PluginInfo info, GLib.Error error);
|
||||
|
||||
/**
|
||||
* Emitted when a plugin was unloaded.
|
||||
*
|
||||
* If the given error is not null, it was raised on deactivate.
|
||||
*/
|
||||
public signal void plugin_deactivated(Peas.PluginInfo info,
|
||||
GLib.Error? error);
|
||||
|
||||
|
||||
private Client application;
|
||||
private Peas.Engine plugins;
|
||||
private bool is_shutdown = false;
|
||||
private string trusted_path;
|
||||
|
||||
private FolderStoreFactory folders_factory;
|
||||
|
||||
private Gee.Map<Peas.PluginInfo,PluginContext> plugin_set =
|
||||
new Gee.HashMap<Peas.PluginInfo,PluginContext>();
|
||||
private Gee.Map<Peas.PluginInfo,NotificationContext> notification_contexts =
|
||||
new Gee.HashMap<Peas.PluginInfo,NotificationContext>();
|
||||
|
||||
|
||||
public PluginManager(Client application) throws GLib.Error {
|
||||
this.application = application;
|
||||
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.plugins.load_plugin.connect_after(on_load_plugin);
|
||||
this.plugins.unload_plugin.connect(on_unload_plugin);
|
||||
|
||||
string[] optional_names = application.config.get_optional_plugins();
|
||||
foreach (Peas.PluginInfo info in this.plugins.get_plugin_list()) {
|
||||
string name = info.get_module_name();
|
||||
try {
|
||||
info.is_available();
|
||||
if (info.is_builtin()) {
|
||||
debug("Loading built-in plugin: %s", info.get_name());
|
||||
this.engine.load_plugin(info);
|
||||
} else {
|
||||
debug("Not loading plugin: %s", info.get_name());
|
||||
if (info.is_available()) {
|
||||
if (is_autoload(info)) {
|
||||
debug("Loading autoload plugin: %s", name);
|
||||
this.plugins.load_plugin(info);
|
||||
} else if (name in optional_names) {
|
||||
debug("Loading optional plugin: %s", name);
|
||||
this.plugins.load_plugin(info);
|
||||
}
|
||||
}
|
||||
} catch (GLib.Error err) {
|
||||
warning("Plugin %s not available: %s",
|
||||
info.get_name(), err.message);
|
||||
warning("Plugin %s not available: %s", name, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the engine folder for the given plugin folder, if any. */
|
||||
public Geary.Folder? get_engine_folder(Plugin.Folder plugin) {
|
||||
return this.folders_factory.get_engine_folder(plugin);
|
||||
}
|
||||
|
||||
public Gee.Collection<Peas.PluginInfo> get_optional_plugins() {
|
||||
var plugins = new Gee.LinkedList<Peas.PluginInfo>();
|
||||
foreach (Peas.PluginInfo plugin in this.plugins.get_plugin_list()) {
|
||||
try {
|
||||
plugin.is_available();
|
||||
if (!is_autoload(plugin)) {
|
||||
plugins.add(plugin);
|
||||
}
|
||||
} catch (GLib.Error err) {
|
||||
warning(
|
||||
"Plugin %s not available: %s",
|
||||
plugin.get_module_name(), err.message
|
||||
);
|
||||
}
|
||||
}
|
||||
return plugins;
|
||||
}
|
||||
|
||||
public bool load_optional(Peas.PluginInfo plugin) throws GLib.Error {
|
||||
bool loaded = false;
|
||||
if (plugin.is_available() &&
|
||||
!plugin.is_loaded() &&
|
||||
!is_autoload(plugin)) {
|
||||
this.plugins.load_plugin(plugin);
|
||||
loaded = true;
|
||||
string name = plugin.get_module_name();
|
||||
string[] optional_names =
|
||||
this.application.config.get_optional_plugins();
|
||||
if (!(name in optional_names)) {
|
||||
optional_names += name;
|
||||
this.application.config.set_optional_plugins(optional_names);
|
||||
}
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public bool unload_optional(Peas.PluginInfo plugin) throws GLib.Error {
|
||||
bool unloaded = false;
|
||||
if (plugin.is_available() &&
|
||||
plugin.is_loaded() &&
|
||||
!is_autoload(plugin)) {
|
||||
this.plugins.unload_plugin(plugin);
|
||||
unloaded = true;
|
||||
string name = plugin.get_module_name();
|
||||
string[] old_names =
|
||||
this.application.config.get_optional_plugins();
|
||||
string[] new_names = new string[0];
|
||||
for (int i = 0; i < old_names.length; i++) {
|
||||
if (old_names[i] != name) {
|
||||
new_names += old_names[i];
|
||||
}
|
||||
}
|
||||
this.application.config.set_optional_plugins(new_names);
|
||||
}
|
||||
return unloaded;
|
||||
}
|
||||
|
||||
internal void close() throws GLib.Error {
|
||||
this.is_shutdown = true;
|
||||
this.plugins.set_loaded_plugins(null);
|
||||
this.plugins.garbage_collect();
|
||||
this.folders_factory.destroy();
|
||||
}
|
||||
|
||||
internal inline bool is_autoload(Peas.PluginInfo info) {
|
||||
return info.get_module_name() in AUTOLOAD_MODULES;
|
||||
}
|
||||
|
||||
internal Gee.Collection<NotificationContext> get_notification_contexts() {
|
||||
return this.notification_contexts.values.read_only_view;
|
||||
}
|
||||
|
||||
private void on_load_plugin(Peas.PluginInfo info) {
|
||||
var plugin = this.plugins.create_extension(
|
||||
info,
|
||||
typeof(Plugin.PluginBase),
|
||||
"plugin_application",
|
||||
new ApplicationImpl(this.application, this.folders_factory)
|
||||
) as Plugin.PluginBase;
|
||||
if (plugin != null) {
|
||||
bool do_activate = true;
|
||||
var trusted = plugin as Plugin.TrustedExtension;
|
||||
if (trusted != null) {
|
||||
if (info.get_module_dir().has_prefix(this.trusted_path)) {
|
||||
trusted.client_application = this.application;
|
||||
trusted.client_plugins = this;
|
||||
} else {
|
||||
do_activate = false;
|
||||
this.plugins.unload_plugin(info);
|
||||
}
|
||||
}
|
||||
|
||||
var notification = plugin as Plugin.NotificationExtension;
|
||||
if (notification != null) {
|
||||
var context = new NotificationContext(
|
||||
this.application,
|
||||
this.folders_factory
|
||||
);
|
||||
this.notification_contexts.set(info, context);
|
||||
notification.notifications = context;
|
||||
}
|
||||
|
||||
if (do_activate) {
|
||||
var plugin_context = new PluginContext(info, plugin);
|
||||
plugin_context.activate.begin((obj, res) => {
|
||||
on_plugin_activated(plugin_context, res);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
warning(
|
||||
"Could not construct BasePlugin from %s", info.get_module_name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_unload_plugin(Peas.PluginInfo info) {
|
||||
var plugin_context = this.plugin_set.get(info);
|
||||
if (plugin_context != null) {
|
||||
plugin_context.deactivate.begin(
|
||||
this.is_shutdown,
|
||||
(obj, res) => {
|
||||
on_plugin_deactivated(plugin_context, res);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_plugin_activated(PluginContext context,
|
||||
GLib.AsyncResult result) {
|
||||
try {
|
||||
context.activate.end(result);
|
||||
this.plugin_set.set(context.info, context);
|
||||
plugin_activated(context.info);
|
||||
} catch (GLib.Error err) {
|
||||
plugin_error(context.info, err);
|
||||
warning(
|
||||
"Activating plugin %s threw error, unloading: %s",
|
||||
context.info.get_module_name(),
|
||||
err.message
|
||||
);
|
||||
this.plugins.unload_plugin(context.info);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_plugin_deactivated(PluginContext context,
|
||||
GLib.AsyncResult result) {
|
||||
GLib.Error? error = null;
|
||||
try {
|
||||
context.deactivate.end(result);
|
||||
} catch (GLib.Error err) {
|
||||
warning(
|
||||
"Deactivating plugin %s threw error: %s",
|
||||
context.info.get_module_name(),
|
||||
err.message
|
||||
);
|
||||
error = err;
|
||||
}
|
||||
|
||||
var notification = context.plugin as Plugin.NotificationExtension;
|
||||
if (notification != null) {
|
||||
var notifications = this.notification_contexts.get(context.info);
|
||||
if (notifications != null) {
|
||||
this.notification_contexts.unset(context.info);
|
||||
notifications.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
plugin_deactivated(context.info, error);
|
||||
this.plugin_set.unset(context.info);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,18 +23,27 @@ public class Components.PreferencesWindow : Hdy.PreferencesWindow {
|
|||
|
||||
|
||||
/** Returns the window's associated client application instance. */
|
||||
public new Application.Client application {
|
||||
public new Application.Client? application {
|
||||
get { return (Application.Client) base.get_application(); }
|
||||
set { base.set_application(value); }
|
||||
}
|
||||
|
||||
private Application.PluginManager plugins;
|
||||
|
||||
public PreferencesWindow(Application.MainWindow parent) {
|
||||
|
||||
public PreferencesWindow(Application.MainWindow parent,
|
||||
Application.PluginManager plugins) {
|
||||
Object(
|
||||
application: parent.application,
|
||||
transient_for: parent
|
||||
);
|
||||
this.plugins = plugins;
|
||||
|
||||
add_general_pane();
|
||||
add_plugin_pane();
|
||||
}
|
||||
|
||||
private void add_general_pane() {
|
||||
var autoselect = new Gtk.Switch();
|
||||
autoselect.valign = CENTER;
|
||||
|
||||
|
|
@ -104,6 +113,8 @@ public class Components.PreferencesWindow : Hdy.PreferencesWindow {
|
|||
group.add(startup_notifications_row);
|
||||
|
||||
var page = new Hdy.PreferencesPage();
|
||||
/// Translators: Preferences page title
|
||||
page.title = _("Preferences");
|
||||
page.propagate_natural_height = true;
|
||||
page.propagate_natural_width = true;
|
||||
page.add(group);
|
||||
|
|
@ -115,43 +126,122 @@ public class Components.PreferencesWindow : Hdy.PreferencesWindow {
|
|||
window_actions.add_action_entries(WINDOW_ACTIONS, this);
|
||||
insert_action_group(Action.Window.GROUP_NAME, window_actions);
|
||||
|
||||
Application.Configuration config = this.application.config;
|
||||
config.bind(
|
||||
Application.Configuration.AUTOSELECT_KEY,
|
||||
autoselect,
|
||||
"state"
|
||||
);
|
||||
config.bind(
|
||||
Application.Configuration.DISPLAY_PREVIEW_KEY,
|
||||
display_preview,
|
||||
"state"
|
||||
);
|
||||
config.bind(
|
||||
Application.Configuration.FOLDER_LIST_PANE_HORIZONTAL_KEY,
|
||||
three_pane_view,
|
||||
"state"
|
||||
);
|
||||
config.bind(
|
||||
Application.Configuration.SINGLE_KEY_SHORTCUTS,
|
||||
single_key_shortucts,
|
||||
"state"
|
||||
);
|
||||
config.bind(
|
||||
Application.Configuration.STARTUP_NOTIFICATIONS_KEY,
|
||||
startup_notifications,
|
||||
"state"
|
||||
);
|
||||
Application.Client? application = this.application;
|
||||
if (application != null) {
|
||||
Application.Configuration config = application.config;
|
||||
config.bind(
|
||||
Application.Configuration.AUTOSELECT_KEY,
|
||||
autoselect,
|
||||
"state"
|
||||
);
|
||||
config.bind(
|
||||
Application.Configuration.DISPLAY_PREVIEW_KEY,
|
||||
display_preview,
|
||||
"state"
|
||||
);
|
||||
config.bind(
|
||||
Application.Configuration.FOLDER_LIST_PANE_HORIZONTAL_KEY,
|
||||
three_pane_view,
|
||||
"state"
|
||||
);
|
||||
config.bind(
|
||||
Application.Configuration.SINGLE_KEY_SHORTCUTS,
|
||||
single_key_shortucts,
|
||||
"state"
|
||||
);
|
||||
config.bind(
|
||||
Application.Configuration.STARTUP_NOTIFICATIONS_KEY,
|
||||
startup_notifications,
|
||||
"state"
|
||||
);
|
||||
}
|
||||
|
||||
this.delete_event.connect(on_delete);
|
||||
}
|
||||
|
||||
private void add_plugin_pane() {
|
||||
var group = new Hdy.PreferencesGroup();
|
||||
/// Translators: Preferences group title
|
||||
//group.title = _("Plugins");
|
||||
/// Translators: Preferences group description
|
||||
//group.description = _("Optional features for Geary");
|
||||
|
||||
Application.Client? application = this.application;
|
||||
if (application != null) {
|
||||
foreach (Peas.PluginInfo plugin in
|
||||
this.plugins.get_optional_plugins()) {
|
||||
group.add(new_plugin_row(plugin));
|
||||
}
|
||||
}
|
||||
|
||||
var page = new Hdy.PreferencesPage();
|
||||
/// Translators: Preferences page title
|
||||
page.title = _("Plugins");
|
||||
page.propagate_natural_width = true;
|
||||
page.add(group);
|
||||
page.show_all();
|
||||
|
||||
add(page);
|
||||
}
|
||||
|
||||
private Hdy.ActionRow new_plugin_row(Peas.PluginInfo plugin) {
|
||||
var @switch = new Gtk.Switch();
|
||||
@switch.active = plugin.is_loaded();
|
||||
@switch.notify["active"].connect_after(
|
||||
() => enable_plugin(plugin, switch)
|
||||
);
|
||||
@switch.valign = CENTER;
|
||||
|
||||
var row = new Hdy.ActionRow();
|
||||
row.title = plugin.get_name();
|
||||
row.subtitle = plugin.get_description();
|
||||
row.activatable_widget = @switch;
|
||||
row.add_action(@switch);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private void enable_plugin(Peas.PluginInfo plugin, Gtk.Switch @switch) {
|
||||
if (@switch.active && !plugin.is_loaded()) {
|
||||
bool loaded = false;
|
||||
try {
|
||||
loaded = this.plugins.load_optional(plugin);
|
||||
} catch (GLib.Error err) {
|
||||
warning(
|
||||
"Plugin %s not able to be loaded: %s",
|
||||
plugin.get_name(), err.message
|
||||
);
|
||||
}
|
||||
if (!loaded) {
|
||||
@switch.active = false;
|
||||
}
|
||||
} else if (!@switch.active && plugin.is_loaded()) {
|
||||
bool unloaded = false;
|
||||
try {
|
||||
unloaded = this.plugins.unload_optional(plugin);
|
||||
} catch (GLib.Error err) {
|
||||
warning(
|
||||
"Plugin %s not able to be loaded: %s",
|
||||
plugin.get_name(), err.message
|
||||
);
|
||||
}
|
||||
if (!unloaded) {
|
||||
@switch.active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void on_close() {
|
||||
close();
|
||||
}
|
||||
|
||||
private bool on_delete() {
|
||||
// Sync startup notification option with file state
|
||||
this.application.autostart.sync_with_config();
|
||||
Application.Client? application = this.application;
|
||||
if (application != null) {
|
||||
application.autostart.sync_with_config();
|
||||
}
|
||||
return Gdk.EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
*/
|
||||
|
||||
public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
|
||||
|
||||
|
||||
public const Gtk.TargetEntry[] TARGET_ENTRY_LIST = {
|
||||
{ "application/x-geary-mail", Gtk.TargetFlags.SAME_APP, 0 }
|
||||
};
|
||||
|
|
@ -12,6 +14,7 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
|
|||
private const int INBOX_ORDINAL = -2; // First account branch is zero
|
||||
private const int SEARCH_ORDINAL = -1;
|
||||
|
||||
|
||||
public signal void folder_selected(Geary.Folder? folder);
|
||||
public signal void copy_conversation(Geary.Folder folder);
|
||||
public signal void move_conversation(Geary.Folder folder);
|
||||
|
|
@ -22,7 +25,7 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
|
|||
= new Gee.HashMap<Geary.Account, AccountBranch>();
|
||||
private InboxesBranch inboxes_branch = new InboxesBranch();
|
||||
private SearchBranch? search_branch = null;
|
||||
private Application.NotificationContext? monitor = null;
|
||||
|
||||
|
||||
public Tree() {
|
||||
base(TARGET_ENTRY_LIST, Gdk.DragAction.COPY | Gdk.DragAction.MOVE, drop_handler);
|
||||
|
|
@ -39,10 +42,24 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
|
|||
}
|
||||
|
||||
~Tree() {
|
||||
set_new_messages_monitor(null);
|
||||
base_unref();
|
||||
}
|
||||
|
||||
public void set_has_new(Geary.Folder folder, bool has_new) {
|
||||
FolderEntry? entry = get_folder_entry(folder);
|
||||
if (entry != null) {
|
||||
entry.set_has_new(has_new);
|
||||
}
|
||||
|
||||
if (folder.special_folder_type == INBOX &&
|
||||
has_branch(inboxes_branch)) {
|
||||
entry = inboxes_branch.get_entry_for_account(folder.account);
|
||||
if (entry != null) {
|
||||
entry.set_has_new(has_new);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drop_handler(Gdk.DragContext context, Sidebar.Entry? entry,
|
||||
Gtk.SelectionData data, uint info, uint time) {
|
||||
}
|
||||
|
|
@ -63,35 +80,10 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
|
|||
}
|
||||
|
||||
private void on_entry_selected(Sidebar.SelectableEntry selectable) {
|
||||
AbstractFolderEntry? abstract_folder_entry = selectable as AbstractFolderEntry;
|
||||
if (abstract_folder_entry != null) {
|
||||
this.selected = abstract_folder_entry.folder;
|
||||
folder_selected(abstract_folder_entry.folder);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_new_messages_changed(Geary.Folder folder, int count) {
|
||||
FolderEntry? entry = get_folder_entry(folder);
|
||||
if (entry != null)
|
||||
entry.set_has_new(count > 0);
|
||||
|
||||
if (has_branch(inboxes_branch)) {
|
||||
InboxFolderEntry? inbox_entry = inboxes_branch.get_entry_for_account(folder.account);
|
||||
if (inbox_entry != null)
|
||||
inbox_entry.set_has_new(count > 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void set_new_messages_monitor(Application.NotificationContext? monitor) {
|
||||
if (this.monitor != null) {
|
||||
this.monitor.new_messages_arrived.disconnect(on_new_messages_changed);
|
||||
this.monitor.new_messages_retired.disconnect(on_new_messages_changed);
|
||||
}
|
||||
|
||||
this.monitor = monitor;
|
||||
if (this.monitor != null) {
|
||||
this.monitor.new_messages_arrived.connect(on_new_messages_changed);
|
||||
this.monitor.new_messages_retired.connect(on_new_messages_changed);
|
||||
FolderEntry? entry = selectable as FolderEntry;
|
||||
if (entry != null) {
|
||||
this.selected = entry.folder;
|
||||
folder_selected(entry.folder);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,8 +93,11 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
|
|||
}
|
||||
|
||||
public void add_folder(Geary.Folder folder) {
|
||||
if (!account_branches.has_key(folder.account))
|
||||
account_branches.set(folder.account, new AccountBranch(folder.account));
|
||||
Geary.Account account = folder.account;
|
||||
if (!account_branches.has_key(account)) {
|
||||
this.account_branches.set(account, new AccountBranch(account));
|
||||
account.information.notify["ordinal"].connect(on_ordinal_changed);
|
||||
}
|
||||
|
||||
AccountBranch account_branch = account_branches.get(folder.account);
|
||||
if (!has_branch(account_branch))
|
||||
|
|
@ -113,7 +108,6 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
|
|||
if (folder.special_folder_type == Geary.SpecialFolderType.INBOX)
|
||||
inboxes_branch.add_inbox(folder);
|
||||
|
||||
folder.account.information.notify["ordinal"].connect(on_ordinal_changed);
|
||||
account_branch.add_folder(folder);
|
||||
}
|
||||
|
||||
|
|
@ -225,24 +219,6 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
|
|||
return ret;
|
||||
}
|
||||
|
||||
private void on_ordinal_changed() {
|
||||
if (account_branches.size <= 1)
|
||||
return;
|
||||
|
||||
// Remove branches where the ordinal doesn't match the graft position.
|
||||
Gee.ArrayList<AccountBranch> branches_to_reorder = new Gee.ArrayList<AccountBranch>();
|
||||
foreach (AccountBranch branch in account_branches.values) {
|
||||
if (get_position_for_branch(branch) != branch.account.information.ordinal) {
|
||||
prune(branch);
|
||||
branches_to_reorder.add(branch);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-add branches with new positions.
|
||||
foreach (AccountBranch branch in branches_to_reorder)
|
||||
graft(branch, branch.account.information.ordinal);
|
||||
}
|
||||
|
||||
public void set_search(Geary.Engine engine,
|
||||
Geary.App.SearchFolder search_folder) {
|
||||
if (search_branch != null && has_branch(search_branch)) {
|
||||
|
|
@ -268,5 +244,22 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
|
|||
search_branch = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
private void on_ordinal_changed() {
|
||||
if (account_branches.size <= 1)
|
||||
return;
|
||||
|
||||
// Remove branches where the ordinal doesn't match the graft position.
|
||||
Gee.ArrayList<AccountBranch> branches_to_reorder = new Gee.ArrayList<AccountBranch>();
|
||||
foreach (AccountBranch branch in account_branches.values) {
|
||||
if (get_position_for_branch(branch) != branch.account.information.ordinal) {
|
||||
prune(branch);
|
||||
branches_to_reorder.add(branch);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-add branches with new positions.
|
||||
foreach (AccountBranch branch in branches_to_reorder)
|
||||
graft(branch, branch.account.information.ordinal);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
# Geary client
|
||||
|
||||
geary_client_vala_sources = files(
|
||||
'application/application-attachment-manager.vala',
|
||||
'application/application-avatar-store.vala',
|
||||
|
|
@ -9,6 +10,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',
|
||||
|
|
@ -91,7 +93,17 @@ geary_client_vala_sources = files(
|
|||
'folder-list/folder-list-search-branch.vala',
|
||||
'folder-list/folder-list-special-grouping.vala',
|
||||
|
||||
'plugin/plugin-notification.vala',
|
||||
'plugin/plugin-account.vala',
|
||||
'plugin/plugin-application.vala',
|
||||
'plugin/plugin-contact-store.vala',
|
||||
'plugin/plugin-email-store.vala',
|
||||
'plugin/plugin-email.vala',
|
||||
'plugin/plugin-error.vala',
|
||||
'plugin/plugin-folder-store.vala',
|
||||
'plugin/plugin-folder.vala',
|
||||
'plugin/plugin-notification-extension.vala',
|
||||
'plugin/plugin-plugin-base.vala',
|
||||
'plugin/plugin-trusted-extension.vala',
|
||||
|
||||
'sidebar/sidebar-branch.vala',
|
||||
'sidebar/sidebar-common.vala',
|
||||
|
|
@ -133,6 +145,7 @@ geary_client_dependencies = [
|
|||
libhandy,
|
||||
libmath,
|
||||
libpeas,
|
||||
libpeas_gtk,
|
||||
libsecret,
|
||||
libsoup,
|
||||
libxml,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
[Plugin]
|
||||
Module=libdesktop-notifications.so
|
||||
Module=desktop-notifications
|
||||
Name=Desktop Notifications
|
||||
Description=Displays desktop notifications when new email is delivered
|
||||
Builtin=true
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
public void peas_register_types(TypeModule module) {
|
||||
Peas.ObjectModule obj = module as Peas.ObjectModule;
|
||||
obj.register_extension_type(
|
||||
typeof(Plugin.Notification),
|
||||
typeof(Plugin.PluginBase),
|
||||
typeof(Plugin.DesktopNotifications)
|
||||
);
|
||||
}
|
||||
|
|
@ -18,36 +18,55 @@ public void peas_register_types(TypeModule module) {
|
|||
/**
|
||||
* Manages standard desktop application notifications.
|
||||
*/
|
||||
public class Plugin.DesktopNotifications : Notification {
|
||||
public class Plugin.DesktopNotifications :
|
||||
PluginBase, NotificationExtension, TrustedExtension {
|
||||
|
||||
|
||||
public const Geary.Email.Field REQUIRED_FIELDS =
|
||||
Geary.Email.Field.ORIGINATORS | Geary.Email.Field.SUBJECT;
|
||||
private const Geary.SpecialFolderType[] MONITORED_TYPES = {
|
||||
INBOX, NONE
|
||||
};
|
||||
|
||||
public override Application.Client application {
|
||||
get; construct set;
|
||||
public NotificationContext notifications {
|
||||
get; set construct;
|
||||
}
|
||||
|
||||
public override Application.NotificationContext context {
|
||||
get; construct set;
|
||||
public global::Application.Client client_application {
|
||||
get; set construct;
|
||||
}
|
||||
|
||||
public global::Application.PluginManager client_plugins {
|
||||
get; set construct;
|
||||
}
|
||||
|
||||
private const string ARRIVED_ID = "email-arrived";
|
||||
|
||||
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);
|
||||
public override async void activate() throws GLib.Error {
|
||||
this.cancellable = new GLib.Cancellable();
|
||||
this.email = yield this.notifications.get_email();
|
||||
|
||||
this.notifications.new_messages_arrived.connect(on_new_messages_arrived);
|
||||
this.notifications.new_messages_retired.connect(on_new_messages_retired);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
public override void deactivate(bool is_shutdown) {
|
||||
public override async void deactivate(bool is_shutdown) throws GLib.Error {
|
||||
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.
|
||||
|
|
@ -57,95 +76,85 @@ public class Plugin.DesktopNotifications : Notification {
|
|||
}
|
||||
|
||||
private void clear_arrived_notification() {
|
||||
this.application.withdraw_notification(ARRIVED_ID);
|
||||
this.client_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);
|
||||
|
||||
int total = 0;
|
||||
try {
|
||||
total = this.context.get_new_message_count(folder);
|
||||
} catch (Geary.EngineError err) {
|
||||
// All good
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
||||
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 and the second is the number of unseen
|
||||
/// messages
|
||||
"%s\n(%d other new message)",
|
||||
"%s\n(%d other new messages)",
|
||||
total - 1
|
||||
).printf(
|
||||
body,
|
||||
total - 1
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
"%s, %d new message total", "%s, %d new messages total",
|
||||
/// 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,13 +181,15 @@ public class Plugin.DesktopNotifications : 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 */
|
||||
|
||||
if (this.application.config.desktop_environment == UNITY) {
|
||||
this.application.send_notification(id, notification);
|
||||
// Do not show notification actions under Unity, it's
|
||||
// notifications daemon doesn't support them.
|
||||
if (this.client_application.config.desktop_environment == UNITY) {
|
||||
this.client_application.send_notification(id, notification);
|
||||
return notification;
|
||||
} else {
|
||||
if (action != null) {
|
||||
|
|
@ -187,27 +198,68 @@ public class Plugin.DesktopNotifications : Notification {
|
|||
);
|
||||
}
|
||||
|
||||
this.application.send_notification(id, notification);
|
||||
this.client_application.send_notification(id, notification);
|
||||
return 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
|
||||
"New message", "New messages", count
|
||||
);
|
||||
}
|
||||
|
||||
private void on_new_messages_arrived(Folder folder,
|
||||
int total,
|
||||
Gee.Collection<EmailIdentifier> added) {
|
||||
this.handle_new_messages.begin(folder, total, added);
|
||||
}
|
||||
|
||||
private void on_new_messages_retired(Folder folder, int total) {
|
||||
clear_arrived_notification();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
[Plugin]
|
||||
Module=folder-highlight
|
||||
Name=Folder Highlight
|
||||
Description=Highlights folders that have newly delivered mail
|
||||
95
src/client/plugin/folder-highlight/folder-highlight.vala
Normal file
95
src/client/plugin/folder-highlight/folder-highlight.vala
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
[ModuleInit]
|
||||
public void peas_register_types(TypeModule module) {
|
||||
Peas.ObjectModule obj = module as Peas.ObjectModule;
|
||||
obj.register_extension_type(
|
||||
typeof(Plugin.PluginBase),
|
||||
typeof(Plugin.FolderHighlight)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages highlighting folders that have newly delivered mail
|
||||
*/
|
||||
public class Plugin.FolderHighlight :
|
||||
PluginBase, NotificationExtension, TrustedExtension {
|
||||
|
||||
|
||||
private const Geary.SpecialFolderType[] MONITORED_TYPES = {
|
||||
INBOX, NONE
|
||||
};
|
||||
|
||||
|
||||
public NotificationContext notifications {
|
||||
get; construct set;
|
||||
}
|
||||
|
||||
public global::Application.Client client_application {
|
||||
get; construct set;
|
||||
}
|
||||
|
||||
public global::Application.PluginManager client_plugins {
|
||||
get; construct set;
|
||||
}
|
||||
|
||||
public override async void activate() throws GLib.Error {
|
||||
this.notifications.new_messages_arrived.connect(on_new_messages_arrived);
|
||||
this.notifications.new_messages_retired.connect(on_new_messages_retired);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
public override async void deactivate(bool is_shutdown) throws GLib.Error {
|
||||
// no-op
|
||||
}
|
||||
|
||||
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 on_new_messages_arrived(Folder folder,
|
||||
int total,
|
||||
Gee.Collection<EmailIdentifier> added) {
|
||||
Geary.Folder? engine = this.client_plugins.get_engine_folder(folder);
|
||||
if (engine != null) {
|
||||
foreach (global::Application.MainWindow window
|
||||
in this.client_application.get_main_windows()) {
|
||||
window.folder_list.set_has_new(engine, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void on_new_messages_retired(Folder folder, int total) {
|
||||
Geary.Folder? engine = this.client_plugins.get_engine_folder(folder);
|
||||
if (engine != null) {
|
||||
foreach (global::Application.MainWindow window
|
||||
in this.client_application.get_main_windows()) {
|
||||
window.folder_list.set_has_new(engine, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
26
src/client/plugin/folder-highlight/meson.build
Normal file
26
src/client/plugin/folder-highlight/meson.build
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
plugin_name = 'folder-highlight'
|
||||
|
||||
plugin_src = join_paths(plugin_name + '.vala')
|
||||
plugin_data = join_paths(plugin_name + '.plugin')
|
||||
plugin_dest = join_paths(plugins_dir, plugin_name)
|
||||
|
||||
shared_module(
|
||||
plugin_name,
|
||||
sources: plugin_src,
|
||||
dependencies: plugin_dependencies,
|
||||
include_directories: config_h_dir,
|
||||
vala_args: geary_vala_args,
|
||||
c_args: plugin_c_args,
|
||||
install: true,
|
||||
install_dir: plugin_dest
|
||||
)
|
||||
|
||||
i18n.merge_file(
|
||||
input: plugin_data + '.in',
|
||||
output: plugin_data,
|
||||
type: 'desktop',
|
||||
po_dir: po_dir,
|
||||
install: true,
|
||||
install_dir: plugin_dest
|
||||
)
|
||||
|
|
@ -23,5 +23,6 @@ plugin_dependencies = [
|
|||
plugin_c_args = geary_c_args
|
||||
|
||||
subdir('desktop-notifications')
|
||||
subdir('folder-highlight')
|
||||
subdir('messaging-menu')
|
||||
subdir('notification-badge')
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ if libmessagingmenu_dep.found()
|
|||
shared_module(
|
||||
# Use a non-standard name for the lib since the standard one
|
||||
# conflicts with libmessagingmenu and causes linking to fail
|
||||
plugin_name + '-geary',
|
||||
'unity-' + plugin_name,
|
||||
sources: plugin_src,
|
||||
dependencies: messaging_menu_dependencies,
|
||||
include_directories: config_h_dir,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
[Plugin]
|
||||
Module=libmessaging-menu-geary.so
|
||||
Module=unity-messaging-menu
|
||||
Name=Messaging Menu
|
||||
Description=Displays Unity Messaging Menu notifications for new email
|
||||
Builtin=true
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -10,75 +10,55 @@
|
|||
public void peas_register_types(TypeModule module) {
|
||||
Peas.ObjectModule obj = module as Peas.ObjectModule;
|
||||
obj.register_extension_type(
|
||||
typeof(Plugin.Notification),
|
||||
typeof(Plugin.PluginBase),
|
||||
typeof(Plugin.MessagingMenu)
|
||||
);
|
||||
}
|
||||
|
||||
/** Updates the Unity messaging menu when new mail arrives. */
|
||||
public class Plugin.MessagingMenu : Notification {
|
||||
public class Plugin.MessagingMenu : PluginBase, NotificationExtension {
|
||||
|
||||
|
||||
public override Application.Client application {
|
||||
get; construct set;
|
||||
public NotificationContext notifications {
|
||||
get; set construct;
|
||||
}
|
||||
|
||||
public override Application.NotificationContext context {
|
||||
get; construct set;
|
||||
}
|
||||
|
||||
private global::MessagingMenu.App? app = null;
|
||||
private FolderStore? folders = null;
|
||||
|
||||
|
||||
public override void activate() {
|
||||
public override async void activate() throws GLib.Error {
|
||||
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.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());
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
public override async void deactivate(bool is_shutdown) throws GLib.Error {
|
||||
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 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 +67,7 @@ public class Plugin.MessagingMenu : 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 +75,7 @@ public class Plugin.MessagingMenu : 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 +83,37 @@ public class Plugin.MessagingMenu : 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.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
[Plugin]
|
||||
Module=libnotification-badge.so
|
||||
Module=notification-badge
|
||||
Name=Notification Badge
|
||||
Description=Displays an application badge showing the number of unread messages
|
||||
Builtin=true
|
||||
Description=Displays a dock badge showing the number of new messages
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -10,59 +10,85 @@
|
|||
public void peas_register_types(TypeModule module) {
|
||||
Peas.ObjectModule obj = module as Peas.ObjectModule;
|
||||
obj.register_extension_type(
|
||||
typeof(Plugin.Notification),
|
||||
typeof(Plugin.PluginBase),
|
||||
typeof(Plugin.NotificationBadge)
|
||||
);
|
||||
}
|
||||
|
||||
/** Updates Unity application badge with total new message count. */
|
||||
public class Plugin.NotificationBadge : Notification {
|
||||
public class Plugin.NotificationBadge :
|
||||
PluginBase, NotificationExtension, TrustedExtension {
|
||||
|
||||
|
||||
public override Application.Client application {
|
||||
get; construct set;
|
||||
private const Geary.SpecialFolderType[] MONITORED_TYPES = {
|
||||
INBOX, NONE
|
||||
};
|
||||
|
||||
public NotificationContext notifications {
|
||||
get; set construct;
|
||||
}
|
||||
|
||||
public override Application.NotificationContext context {
|
||||
get; construct set;
|
||||
public global::Application.Client client_application {
|
||||
get; set construct;
|
||||
}
|
||||
|
||||
public global::Application.PluginManager client_plugins {
|
||||
get; set construct;
|
||||
}
|
||||
|
||||
private UnityLauncherEntry? entry = null;
|
||||
|
||||
|
||||
public override void activate() {
|
||||
var connection = this.application.get_dbus_connection();
|
||||
var path = this.application.get_dbus_object_path();
|
||||
try {
|
||||
if (connection == null || path == null) {
|
||||
throw new GLib.IOError.NOT_CONNECTED(
|
||||
"Application does not have a DBus connection or path"
|
||||
);
|
||||
}
|
||||
this.entry = new UnityLauncherEntry(
|
||||
connection,
|
||||
path + "/plugin/notificationbadge",
|
||||
Application.Client.APP_ID + ".desktop"
|
||||
);
|
||||
} catch (GLib.Error error) {
|
||||
warning(
|
||||
"Failed to register Unity Launcher Entry: %s",
|
||||
error.message
|
||||
public override async void activate() throws GLib.Error {
|
||||
var connection = this.client_application.get_dbus_connection();
|
||||
var path = this.client_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"
|
||||
);
|
||||
}
|
||||
this.entry = new UnityLauncherEntry(
|
||||
connection,
|
||||
path + "/plugin/notificationbadge",
|
||||
global::Application.Client.APP_ID + ".desktop"
|
||||
);
|
||||
|
||||
this.context.notify["total-new-messages"].connect(on_total_changed);
|
||||
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());
|
||||
|
||||
this.notifications.notify["total-new-messages"].connect(on_total_changed);
|
||||
update_count();
|
||||
}
|
||||
|
||||
public override void deactivate(bool is_shutdown) {
|
||||
this.context.notify["total-new-messages"].disconnect(on_total_changed);
|
||||
public override async void deactivate(bool is_shutdown) throws GLib.Error {
|
||||
this.notifications.notify["total-new-messages"].disconnect(
|
||||
on_total_changed
|
||||
);
|
||||
this.entry = null;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
|||
21
src/client/plugin/plugin-account.vala
Normal file
21
src/client/plugin/plugin-account.vala
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An object representing an account for use by plugins.
|
||||
*
|
||||
* Instances of these may be obtained from their respective {@link
|
||||
* Folder} objects.
|
||||
*/
|
||||
public interface Plugin.Account : Geary.BaseObject {
|
||||
|
||||
|
||||
/** Returns the human-readable name of this account. */
|
||||
public abstract string display_name { get; }
|
||||
|
||||
|
||||
}
|
||||
20
src/client/plugin/plugin-application.vala
Normal file
20
src/client/plugin/plugin-application.vala
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An object representing the client application for use by plugins.
|
||||
*
|
||||
* Plugins may obtain instances of this object from their context
|
||||
* objects, for example {@link
|
||||
* Application.NotificationContext.get_application}.
|
||||
*/
|
||||
public interface Plugin.Application : Geary.BaseObject {
|
||||
|
||||
|
||||
public abstract void show_folder(Folder folder);
|
||||
|
||||
}
|
||||
41
src/client/plugin/plugin-contact-store.vala
Normal file
41
src/client/plugin/plugin-contact-store.vala
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides plugins with access to contact information.
|
||||
*
|
||||
* Plugins may obtain instances of this object from their context
|
||||
* objects, for example {@link
|
||||
* Application.NotificationContext.get_contacts_for_folder}.
|
||||
*/
|
||||
public interface Plugin.ContactStore : Geary.BaseObject {
|
||||
|
||||
|
||||
/** Searches for contacts based on a specific string */
|
||||
public abstract async Gee.Collection<global::Application.Contact> search(
|
||||
string query,
|
||||
uint min_importance,
|
||||
uint limit,
|
||||
GLib.Cancellable? cancellable
|
||||
) throws GLib.Error;
|
||||
|
||||
|
||||
/**
|
||||
* Returns a contact for a specific mailbox.
|
||||
*
|
||||
* Returns a contact that has the given mailbox address listed as
|
||||
* a primary or secondary email. A contact will always be
|
||||
* returned, so if no matching contact already exists a new,
|
||||
* non-persistent contact will be returned.
|
||||
*/
|
||||
public abstract async global::Application.Contact load(
|
||||
Geary.RFC822.MailboxAddress mailbox,
|
||||
GLib.Cancellable? cancellable
|
||||
) throws GLib.Error;
|
||||
|
||||
|
||||
}
|
||||
27
src/client/plugin/plugin-email-store.vala
Normal file
27
src/client/plugin/plugin-email-store.vala
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides plugins with access to email.
|
||||
*
|
||||
* Plugins may obtain instances of this object from their context
|
||||
* objects, for example {@link
|
||||
* Application.NotificationContext.get_email}.
|
||||
*/
|
||||
public interface Plugin.EmailStore : Geary.BaseObject {
|
||||
|
||||
|
||||
/** Emitted when an email message has been sent. */
|
||||
public signal void email_sent(Email message);
|
||||
|
||||
/** Returns a read-only set of all known folders. */
|
||||
public async abstract Gee.Collection<Email> get_email(
|
||||
Gee.Collection<EmailIdentifier> ids,
|
||||
GLib.Cancellable? cancellable
|
||||
) throws GLib.Error;
|
||||
|
||||
}
|
||||
53
src/client/plugin/plugin-email.vala
Normal file
53
src/client/plugin/plugin-email.vala
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An object representing an email for use by plugins.
|
||||
*
|
||||
* Instances of these may be obtained from {@link EmailStore}.
|
||||
*/
|
||||
public interface Plugin.Email : Geary.BaseObject {
|
||||
|
||||
|
||||
/** Returns a unique identifier for this email. */
|
||||
public abstract EmailIdentifier identifier { get; }
|
||||
|
||||
/** Returns the subject header value for the this email. */
|
||||
public abstract string subject { get; }
|
||||
|
||||
/**
|
||||
* Returns the email's primary originator.
|
||||
*
|
||||
* This method returns the mailbox of the best originator of the
|
||||
* email, if any.
|
||||
*
|
||||
* @see Util.Email.get_primary_originator
|
||||
*/
|
||||
public abstract Geary.RFC822.MailboxAddress? get_primary_originator();
|
||||
|
||||
}
|
||||
|
||||
|
||||
// XXX this should be an inner interface of Email, but GNOME/vala#918
|
||||
// prevents that.
|
||||
|
||||
/**
|
||||
* An object representing an email's identifier.
|
||||
*/
|
||||
public interface Plugin.EmailIdentifier :
|
||||
Geary.BaseObject, Gee.Hashable<EmailIdentifier> {
|
||||
|
||||
/**
|
||||
* Returns a variant version of this identifier.
|
||||
*
|
||||
* This value is suitable to be used as the `show-email`
|
||||
* application action parameter.
|
||||
*/
|
||||
public abstract GLib.Variant to_variant();
|
||||
|
||||
|
||||
}
|
||||
27
src/client/plugin/plugin-error.vala
Normal file
27
src/client/plugin/plugin-error.vala
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The base class for objects implementing a client plugin.
|
||||
*
|
||||
* To implement a new plugin, have it derive from this type and
|
||||
* implement any additional extension interfaces (such as {@link
|
||||
* NotificationExtension}) as required.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Errors when plugins request resources from their contexts.
|
||||
*/
|
||||
public errordomain Plugin.Error {
|
||||
|
||||
/** Raised when access to a requested resource was denied. */
|
||||
PERMISSION_DENIED,
|
||||
|
||||
/** Raised when a requested resource was not found. */
|
||||
NOT_FOUND;
|
||||
|
||||
}
|
||||
32
src/client/plugin/plugin-folder-store.vala
Normal file
32
src/client/plugin/plugin-folder-store.vala
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides plugins with access to folders.
|
||||
*
|
||||
* Plugins may obtain instances of this object from their context
|
||||
* objects, for example {@link
|
||||
* Application.NotificationContext.get_folder_store}.
|
||||
*/
|
||||
public interface Plugin.FolderStore : Geary.BaseObject {
|
||||
|
||||
|
||||
/** Emitted when new folders are available. */
|
||||
public signal void folders_available(Gee.Collection<Folder> available);
|
||||
|
||||
/** Emitted when existing folders have become unavailable. */
|
||||
public signal void folders_unavailable(Gee.Collection<Folder> unavailable);
|
||||
|
||||
/** Emitted when existing folders have become unavailable. */
|
||||
public signal void folders_type_changed(Gee.Collection<Folder> changed);
|
||||
|
||||
|
||||
/** Returns a read-only set of all known folders. */
|
||||
public abstract Gee.Collection<Folder> get_folders();
|
||||
|
||||
|
||||
}
|
||||
40
src/client/plugin/plugin-folder.vala
Normal file
40
src/client/plugin/plugin-folder.vala
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An object representing a folder for use by plugins.
|
||||
*
|
||||
* Instances of these may be obtained from {@link FolderStore}.
|
||||
*/
|
||||
public interface Plugin.Folder : Geary.BaseObject {
|
||||
|
||||
|
||||
/**
|
||||
* Returns a unique identifier for this account and folder.
|
||||
*
|
||||
* The value returned is persistent across application restarts.
|
||||
*/
|
||||
public abstract string persistent_id { get; }
|
||||
|
||||
/** Returns the human-readable name of this folder. */
|
||||
public abstract string display_name { get; }
|
||||
|
||||
/** Returns the type of this folder. */
|
||||
public abstract Geary.SpecialFolderType folder_type { get; }
|
||||
|
||||
/** Returns the account the folder belongs to, if any. */
|
||||
public abstract Account? account { get; }
|
||||
|
||||
/**
|
||||
* Returns a variant identifying this account and folder.
|
||||
*
|
||||
* This value is suitable to be used as the `show-folder`
|
||||
* application action parameter.
|
||||
*/
|
||||
public abstract GLib.Variant to_variant();
|
||||
|
||||
}
|
||||
148
src/client/plugin/plugin-notification-extension.vala
Normal file
148
src/client/plugin/plugin-notification-extension.vala
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A plugin extension point for notifying of mail sending or arriving.
|
||||
*/
|
||||
public interface Plugin.NotificationExtension : PluginBase {
|
||||
|
||||
/**
|
||||
* Context object for notifications.
|
||||
*
|
||||
* This will be set during (or just after) plugin construction,
|
||||
* before {@link PluginBase.activate} is called.
|
||||
*/
|
||||
public abstract NotificationContext notifications {
|
||||
get; set construct;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// XXX this should be an inner interface of NotificationExtension, but
|
||||
// GNOME/vala#918 prevents that.
|
||||
|
||||
/**
|
||||
* Provides a context for notification plugins.
|
||||
*
|
||||
* The context provides an interface for notification plugins to
|
||||
* interface with the Geary client application. Plugins that implement
|
||||
* the plugins will be passed an instance of this class as the
|
||||
* `context` property.
|
||||
*
|
||||
* 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.NotificationExtension.notifications
|
||||
*/
|
||||
public interface Plugin.NotificationContext : Geary.BaseObject {
|
||||
|
||||
|
||||
/**
|
||||
* Current total new message count for all monitored folders.
|
||||
*
|
||||
* 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 abstract int total_new_messages { get; default = 0; }
|
||||
|
||||
/**
|
||||
* 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 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);
|
||||
|
||||
|
||||
/**
|
||||
* Returns a store to lookup email for notifications.
|
||||
*
|
||||
* This method may prompt for permission before returning.
|
||||
*
|
||||
* @throws Error.PERMISSIONS if permission to access
|
||||
* this resource was not given
|
||||
*/
|
||||
public abstract async Plugin.EmailStore get_email()
|
||||
throws Error.PERMISSION_DENIED;
|
||||
|
||||
/**
|
||||
* Returns a store to lookup folders for notifications.
|
||||
*
|
||||
* This method may prompt for permission before returning.
|
||||
*
|
||||
* @throws Error.PERMISSIONS if permission to access
|
||||
* this resource was not given
|
||||
*/
|
||||
public abstract async Plugin.FolderStore get_folders()
|
||||
throws Error.PERMISSION_DENIED;
|
||||
|
||||
/**
|
||||
* Returns a store to lookup contacts for notifications.
|
||||
*
|
||||
* This method may prompt for permission before returning.
|
||||
*
|
||||
* @throws Error.NOT_FOUND if the given account does
|
||||
* not exist
|
||||
* @throws Error.PERMISSIONS if permission to access
|
||||
* this resource was not given
|
||||
*/
|
||||
public abstract async Plugin.ContactStore get_contacts_for_folder(Plugin.Folder source)
|
||||
throws Error.NOT_FOUND, Error.PERMISSION_DENIED;
|
||||
|
||||
/**
|
||||
* 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 abstract bool should_notify_new_messages(Plugin.Folder target);
|
||||
|
||||
/**
|
||||
* 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 abstract int get_new_message_count(Plugin.Folder target)
|
||||
throws Error.NOT_FOUND;
|
||||
|
||||
/**
|
||||
* Starts monitoring a folder for new messages.
|
||||
*
|
||||
* Notification plugins should call this to start the context
|
||||
* recording new messages for a specific folder.
|
||||
*/
|
||||
public abstract void start_monitoring_folder(Plugin.Folder target);
|
||||
|
||||
/** Stops monitoring a folder for new messages. */
|
||||
public abstract void stop_monitoring_folder(Plugin.Folder target);
|
||||
|
||||
/** Determines if a folder is curently being monitored. */
|
||||
public abstract bool is_monitoring_folder(Plugin.Folder target);
|
||||
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright 2019 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 plugin for notifying of new mail being delivered.
|
||||
*/
|
||||
public abstract class Plugin.Notification : GLib.Object {
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
/* Invoked to activate the plugin, after loading. */
|
||||
public abstract void activate();
|
||||
|
||||
/* Invoked to deactivate the plugin, prior to unloading */
|
||||
public abstract void deactivate(bool is_shutdown);
|
||||
|
||||
}
|
||||
47
src/client/plugin/plugin-plugin-base.vala
Normal file
47
src/client/plugin/plugin-plugin-base.vala
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The base class for objects implementing a client plugin.
|
||||
*
|
||||
* To implement a new plugin, have it derive from this type and
|
||||
* implement any additional extension interfaces (such as {@link
|
||||
* NotificationExtension}) as required.
|
||||
*/
|
||||
public abstract class Plugin.PluginBase : Geary.BaseObject {
|
||||
|
||||
/**
|
||||
* Returns an object for interacting with the client application.
|
||||
*
|
||||
* No special permissions are required to use access this
|
||||
* resource.
|
||||
*
|
||||
* This will be set during (or just after) plugin construction,
|
||||
* before {@link PluginBase.activate} is called.
|
||||
*/
|
||||
public Plugin.Application plugin_application {
|
||||
get; construct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked to activate the plugin, after loading.
|
||||
*
|
||||
* If this method raises an error, it will be unloaded without
|
||||
* deactivating.
|
||||
*/
|
||||
public abstract async void activate() throws GLib.Error;
|
||||
|
||||
/**
|
||||
* Invoked to deactivate the plugin, prior to unloading.
|
||||
*
|
||||
* If `is_shutdown` is true, the plugin is being unloaded because
|
||||
* the client application is quitting. Otherwise, the plugin is
|
||||
* being unloaded at end-user request.
|
||||
*/
|
||||
public abstract async void deactivate(bool is_shutdown) throws GLib.Error;
|
||||
|
||||
}
|
||||
40
src/client/plugin/plugin-trusted-extension.vala
Normal file
40
src/client/plugin/plugin-trusted-extension.vala
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 plugin extension point for trusted plugins.
|
||||
*
|
||||
* In-tree plugins may implement this interface if they require access
|
||||
* to the client application's internal machinery.
|
||||
*
|
||||
* Since the client application and engine objects have no API
|
||||
* stability guarantee, Geary will refuse to load out-of-tree plugins
|
||||
* that implement this extension point.
|
||||
*/
|
||||
public interface Plugin.TrustedExtension : PluginBase {
|
||||
|
||||
/**
|
||||
* Client application object.
|
||||
*
|
||||
* This will be set during (or just after) plugin construction,
|
||||
* before {@link PluginBase.activate} is called.
|
||||
*/
|
||||
public abstract global::Application.Client client_application {
|
||||
get; set construct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client plugin manager object.
|
||||
*
|
||||
* This will be set during (or just after) plugin construction,
|
||||
* before {@link PluginBase.activate} is called.
|
||||
*/
|
||||
public abstract global::Application.PluginManager client_plugins {
|
||||
get; set construct;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ namespace Util.Email {
|
|||
/** Returns the stripped subject line, or a placeholder if none. */
|
||||
public string strip_subject_prefixes(Geary.Email email) {
|
||||
string? cleaned = (email.subject != null) ? email.subject.strip_prefixes() : null;
|
||||
return !Geary.String.is_empty(cleaned) ? cleaned : _("(no subject)");
|
||||
return !Geary.String.is_empty(cleaned) ? cleaned : _("(No subject)");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ public abstract class Geary.BaseObject : Geary.BaseInterface, Object {
|
|||
* by calling {@link BaseInterface.base_ref} if reference tracking
|
||||
* is enabled at compile-time, otherwise this is a no-op.
|
||||
*/
|
||||
protected BaseObject() {
|
||||
construct {
|
||||
#if REF_TRACKING
|
||||
base_ref();
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue