Merge branch 'mjog/user-plugins' into 'mainline'

Support optional (non-builtin) plugins

See merge request GNOME/geary!441
This commit is contained in:
Michael Gratton 2020-03-17 08:45:46 +00:00
commit 3f81b7d507
40 changed files with 2152 additions and 713 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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(

View file

@ -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

View file

@ -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);

View file

@ -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.
*

View file

@ -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)

View file

@ -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);
}
}

View 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);
}
}
}

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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,

View file

@ -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

View file

@ -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();
}
}

View file

@ -0,0 +1,4 @@
[Plugin]
Module=folder-highlight
Name=Folder Highlight
Description=Highlights folders that have newly delivered mail

View 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);
}
}
}
}

View 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
)

View file

@ -23,5 +23,6 @@ plugin_dependencies = [
plugin_c_args = geary_c_args
subdir('desktop-notifications')
subdir('folder-highlight')
subdir('messaging-menu')
subdir('notification-badge')

View file

@ -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,

View file

@ -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

View file

@ -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);
}
}
}
}

View file

@ -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

View file

@ -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 {

View 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; }
}

View 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);
}

View 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;
}

View 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;
}

View 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();
}

View 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;
}

View 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();
}

View 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();
}

View 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);
}

View file

@ -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);
}

View 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;
}

View 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;
}
}

View file

@ -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)");
}
/**

View file

@ -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