From aac59ec53bdecf11c0a3dd8667b95e863f24113d Mon Sep 17 00:00:00 2001 From: Michael Gratton Date: Sun, 19 Apr 2020 15:51:03 +1000 Subject: [PATCH] Application.Controller, Composer.Widget: Clean up composer construction Clean up and simplify how composers are constructed. Ensure all composers are constructed via Application.Composer. Provide a set of APIs for constructing different kinds of composers with minimal parameters, rather than having one method with eleventy different parameters. Mirror API changes in Composer.Widget by splitting `load` method up into a method for each different composer type. Clean up internals a bit as a result. Rename `ComposeType` enum and its values to `ContextType` to better reflect what it does. --- .../application/application-client.vala | 16 +- .../application/application-controller.vala | 412 ++++++++---------- .../application/application-main-window.vala | 90 +--- .../application-plugin-manager.vala | 7 +- src/client/composer/composer-embed.vala | 2 +- src/client/composer/composer-web-view.vala | 6 +- src/client/composer/composer-widget.vala | 358 ++++++++------- .../conversation-contact-popover.vala | 2 +- .../conversation-list-box.vala | 4 +- .../conversation-viewer.vala | 2 +- 10 files changed, 417 insertions(+), 482 deletions(-) diff --git a/src/client/application/application-client.vala b/src/client/application/application-client.vala index 7a784c90..a5dc995b 100644 --- a/src/client/application/application-client.vala +++ b/src/client/application/application-client.vala @@ -513,7 +513,7 @@ public class Application.Client : Gtk.Application { mailto.substring(B0RKED_GLIB_MAILTO_PREFIX.length) ); } - this.new_composer.begin(mailto); + this.new_composer_mailto.begin(mailto); } } } @@ -725,10 +725,14 @@ public class Application.Client : Gtk.Application { prefs.show(); } - public async void new_composer(string? mailto) { + public async void new_composer(Geary.RFC822.MailboxAddress? to = null) { yield this.present(); + yield this.controller.compose_new_email(to); + } - this.controller.compose(mailto); + public async void new_composer_mailto(string? mailto) { + yield this.present(); + yield this.controller.compose_mailto(mailto); } public async void new_window(Geary.Folder? select_folder, @@ -824,7 +828,7 @@ public class Application.Client : Gtk.Application { yield create_controller(); if (uri.down().has_prefix(MAILTO_URI_SCHEME_PREFIX)) { - yield this.new_composer(uri); + yield this.new_composer_mailto(uri); } else { string uri_ = uri; // Support web URLs that omit the protocol. @@ -1146,7 +1150,7 @@ public class Application.Client : Gtk.Application { } private void on_activate_compose() { - this.new_composer.begin(null); + this.new_composer.begin(); } private void on_activate_inspect() { @@ -1155,7 +1159,7 @@ public class Application.Client : Gtk.Application { private void on_activate_mailto(SimpleAction action, Variant? param) { if (param != null) { - this.new_composer.begin(param.get_string()); + this.new_composer_mailto.begin(param.get_string()); } } diff --git a/src/client/application/application-controller.vala b/src/client/application/application-controller.vala index 6f534d47..530dec32 100644 --- a/src/client/application/application-controller.vala +++ b/src/client/application/application-controller.vala @@ -212,16 +212,6 @@ internal class Application.Controller : Geary.BaseObject { this.expunge_accounts.begin(); } - /** Returns a context for an account, if any. */ - internal AccountContext? get_context_for_account(Geary.AccountInformation account) { - return this.accounts.get(account); - } - - /** Returns a read-only collection of contexts each active account. */ - internal Gee.Collection get_account_contexts() { - return this.accounts.values.read_only_view; - } - /** Closes all windows and accounts, releasing held resources. */ public async void close() { // Stop listening for account changes up front so we don't @@ -333,20 +323,48 @@ internal class Application.Controller : Geary.BaseObject { } /** - * Opens or queues a new composer addressed to a specific email address. + * Opens a composer for writing a new, blank message. */ - public void compose(string? mailto = null) { + public async void compose_new_email(Geary.RFC822.MailboxAddress? to = null) { + // If there's already an empty composer open, just use that + foreach (Composer.Widget existing in this.composer_widgets) { + if (existing != null && + existing.current_mode == PANED && + existing.is_blank) { + existing.present(); + } + } + + var composer = new Composer.Widget( + this.application, + this.application.get_active_main_window().selected_account + ); + register_composer(composer); + show_composer(composer, null); + try { + yield composer.load_empty_body(to); + } catch (GLib.Error err) { + report_problem(new Geary.ProblemReport(err)); + } + } + + /** + * Opens a composer with the given `mailto:` URL. + */ + public async void compose_mailto(string mailto) { MainWindow? window = this.application.last_active_main_window; if (window != null && window.selected_account != null) { - create_compose_widget( - window, - window.selected_account, - NEW_MESSAGE, - mailto, - null, - null, - false + var composer = new Composer.Widget( + this.application, window.selected_account ); + register_composer(composer); + show_composer(composer, null); + + try { + yield composer.load_mailto(mailto); + } catch (GLib.Error err) { + report_problem(new Geary.ProblemReport(err)); + } } else { // Schedule the send for after we have an account open. this.pending_mailtos.add(mailto); @@ -355,90 +373,67 @@ internal class Application.Controller : Geary.BaseObject { /** * Opens new composer with an existing message as context. + * + * If the given type is {@link Composer.Widget.ContextType.EDIT}, + * the context is loaded to be edited (e.g. for drafts, templates, + * sending again. Otherwise the context is treated as the email to + * be replied to, etc. */ - public void compose_with_context_email(MainWindow to_show, - Geary.Account account, - Composer.Widget.ComposeType type, - Geary.Email context, - string? quote, - bool is_draft) { - create_compose_widget( - to_show, account, type, null, context, quote, is_draft - ); - } + public async void compose_with_context_email(Composer.Widget.ContextType type, + Geary.Email context, + string? quote) { + MainWindow show_on = this.application.get_active_main_window(); + if (type == EDIT) { + // Check all known composers since the context may be open + // an existing composer already. + foreach (Composer.Widget composer in this.composer_widgets) { + if (composer.saved_id != null && + composer.saved_id.equal_to(context.id)) { + composer.present(); + composer.set_focus(); + return; + } + } + } else { + // See whether there is already an inline message in the + // current window that is either a reply/forward for that + // message, or there is a quote to insert into it. + foreach (Composer.Widget existing in this.composer_widgets) { + if (existing.get_toplevel() == show_on && + (existing.current_mode == INLINE || + existing.current_mode == INLINE_COMPACT) && + (context.id in existing.get_referred_ids() || + quote != null)) { + try { + existing.append_to_email(context, quote, type); + existing.present(); + return; + } catch (Geary.EngineError error) { + report_problem(new Geary.ProblemReport(error)); + } + } + } - /** Adds a new composer to be kept track of. */ - public void add_composer(Composer.Widget widget) { - debug(@"Added composer of type $(widget.compose_type); $(this.composer_widgets.size) composers total"); - widget.destroy.connect_after(this.on_composer_widget_destroy); - this.composer_widgets.add(widget); - } - - /** Returns a read-only collection of currently open composers .*/ - public Gee.Collection get_composers() { - return this.composer_widgets.read_only_view; - } - - /** Opens any pending composers. */ - public void process_pending_composers() { - foreach (string? mailto in this.pending_mailtos) { - compose(mailto); - } - this.pending_mailtos.clear(); - } - - /** Queues the email in a composer for delivery. */ - public async void send_composed_email(Composer.Widget composer) { - AccountContext? context = this.accounts.get( - composer.account.information - ); - if (context != null) { - try { - yield context.commands.execute( - new SendComposerCommand(this.application, context, composer), - context.cancellable - ); - } catch (GLib.Error err) { - report_problem(new Geary.ProblemReport(err)); + // Can't re-use an existing composer, so need to create a + // new one. Replies must open inline in the main window, + // so we need to ensure there are no composers open there + // first. + if (!show_on.close_composer(true)) { + return; } } - } - /** Saves the email in a composer as a draft on the server. */ - public async void save_composed_email(Composer.Widget composer) { - // XXX this doesn't actually do what it says on the tin, since - // the composer's draft manager is already saving drafts on - // the server. Until we get that saving local-only, this will - // only be around for pushing the composer onto the undo stack - AccountContext? context = this.accounts.get( - composer.account.information + var composer = new Composer.Widget( + this.application, + this.application.get_active_main_window().selected_account ); - if (context != null) { - try { - yield context.commands.execute( - new SaveComposerCommand(this, composer), - context.cancellable - ); - } catch (GLib.Error err) { - report_problem(new Geary.ProblemReport(err)); - } - } - } + register_composer(composer); + show_composer(composer, Geary.Collection.single(context.id)); - /** Queues a composer to be discarded. */ - public async void discard_composed_email(Composer.Widget composer) { - AccountContext? context = this.accounts.get( - composer.account.information - ); - if (context != null) { - try { - yield context.commands.execute( - new DiscardComposerCommand(this, composer), - context.cancellable - ); - } catch (GLib.Error err) { - report_problem(new Geary.ProblemReport(err)); - } + try { + yield composer.load_context(type, context, quote); + } catch (GLib.Error err) { + report_problem(new Geary.ProblemReport(err)); } } @@ -858,6 +853,16 @@ internal class Application.Controller : Geary.BaseObject { } } + /** Returns a context for an account, if any. */ + internal AccountContext? get_context_for_account(Geary.AccountInformation account) { + return this.accounts.get(account); + } + + /** Returns a read-only collection of contexts each active account. */ + internal Gee.Collection get_account_contexts() { + return this.accounts.values.read_only_view; + } + internal void register_window(MainWindow window) { window.retry_service_problem.connect(on_retry_service_problem); } @@ -866,6 +871,69 @@ internal class Application.Controller : Geary.BaseObject { window.retry_service_problem.disconnect(on_retry_service_problem); } + /** Opens any pending composers. */ + internal async void process_pending_composers() { + foreach (string? mailto in this.pending_mailtos) { + yield compose_mailto(mailto); + } + this.pending_mailtos.clear(); + } + + /** Queues the email in a composer for delivery. */ + internal async void send_composed_email(Composer.Widget composer) { + AccountContext? context = this.accounts.get( + composer.account.information + ); + if (context != null) { + try { + yield context.commands.execute( + new SendComposerCommand(this.application, context, composer), + context.cancellable + ); + } catch (GLib.Error err) { + report_problem(new Geary.ProblemReport(err)); + } + } + } + + /** Saves the email in a composer as a draft on the server. */ + internal async void save_composed_email(Composer.Widget composer) { + // XXX this doesn't actually do what it says on the tin, since + // the composer's draft manager is already saving drafts on + // the server. Until we get that saving local-only, this will + // only be around for pushing the composer onto the undo stack + AccountContext? context = this.accounts.get( + composer.account.information + ); + if (context != null) { + try { + yield context.commands.execute( + new SaveComposerCommand(this, composer), + context.cancellable + ); + } catch (GLib.Error err) { + report_problem(new Geary.ProblemReport(err)); + } + } + } + + /** Queues a composer to be discarded. */ + internal async void discard_composed_email(Composer.Widget composer) { + AccountContext? context = this.accounts.get( + composer.account.information + ); + if (context != null) { + try { + yield context.commands.execute( + new DiscardComposerCommand(this, composer), + context.cancellable + ); + } catch (GLib.Error err) { + report_problem(new Geary.ProblemReport(err)); + } + } + } + /** Expunges removed accounts while the controller remains open. */ internal async void expunge_accounts() { try { @@ -1330,13 +1398,8 @@ internal class Application.Controller : Geary.BaseObject { /** Displays a composer on the last active main window. */ internal void show_composer(Composer.Widget composer, - Gee.Collection? refers_to, - MainWindow? show_on) { - var target = show_on; - if (target == null) { - target = this.application.get_active_main_window(); - } - + Gee.Collection? refers_to) { + var target = this.application.get_active_main_window(); target.show_composer(composer, refers_to); composer.set_focus(); } @@ -1352,135 +1415,17 @@ internal class Application.Controller : Geary.BaseObject { return do_quit; } - /** - * Creates a composer widget. - * - * Depending on the arguments, this can be inline in the - * conversation or as a new window. - * - * @param compose_type - Whether it's a new message, a reply, a - * forwarded mail, ... - * @param referred - The mail of which we should copy the from/to/... - * addresses - * @param quote - The quote after the mail body - * @param mailto - A "mailto:"-link - * @param is_draft - Whether we're starting from a draft (true) or - * a new mail (false) - */ - private void create_compose_widget(MainWindow show_on, - Geary.Account account, - Composer.Widget.ComposeType compose_type, - string? mailto, - Geary.Email? referred, - string? quote, - bool is_draft) { - // There's a few situations where we can re-use an existing - // composer, check for these first. - if (compose_type == NEW_MESSAGE && !is_draft) { - // We're creating a new message that isn't a draft, if - // there's already an empty composer open, just use - // that - foreach (Composer.Widget existing in this.composer_widgets) { - if (existing != null && - existing.current_mode == PANED && - existing.is_blank) { - existing.present(); - return; - } - } - } else if (compose_type != NEW_MESSAGE && referred != null) { - // A reply/forward was requested, see whether there is - // already an inline message in the target window that is - // either a reply/forward for that message, or there is a - // quote to insert into it. - foreach (Composer.Widget existing in this.composer_widgets) { - if (existing.get_toplevel() == show_on && - (existing.current_mode == INLINE || - existing.current_mode == INLINE_COMPACT) && - (referred.id in existing.get_referred_ids() || - quote != null)) { - try { - existing.append_to_email(referred, quote, compose_type); - existing.present(); - return; - } catch (Geary.EngineError error) { - report_problem(new Geary.ProblemReport(error)); - } - } - } - - // Can't re-use an existing composer, so need to create a - // new one. Replies must open inline in the main window, - // so we need to ensure there are no composers open there - // first. - if (!show_on.close_composer(true)) { - return; - } - } - - Composer.Widget widget; - if (mailto != null) { - widget = new Composer.Widget.from_mailto( - this.application, account, mailto - ); - } else { - widget = new Composer.Widget( - this.application, account, compose_type - ); - } - - add_composer(widget); - show_composer( - widget, - referred != null ? Geary.Collection.single(referred.id) : null, - show_on - ); - - this.load_composer.begin( - account, - widget, - referred, - is_draft, - quote - ); - } - - private async void load_composer(Geary.Account account, - Composer.Widget widget, - Geary.Email? referred = null, - bool is_draft, - string? quote = null) { - Geary.Email? full = null; - GLib.Cancellable? cancellable = null; - if (referred != null) { - AccountContext? context = this.accounts.get(account.information); - if (context != null) { - cancellable = context.cancellable; - try { - full = yield context.emails.fetch_email_async( - referred.id, - Geary.ComposedEmail.REQUIRED_REPLY_FIELDS | - Composer.Widget.REQUIRED_FIELDS, - NONE, - cancellable - ); - } catch (Error e) { - message("Could not load full message: %s", e.message); - } - } - } - try { - yield widget.load(full, is_draft, quote, cancellable); - } catch (GLib.Error err) { - report_problem(new Geary.ProblemReport(err)); - } + internal void register_composer(Composer.Widget widget) { + debug(@"Registered composer of type $(widget.context_type); $(this.composer_widgets.size) composers total"); + widget.destroy.connect_after(this.on_composer_widget_destroy); + this.composer_widgets.add(widget); } private void on_composer_widget_destroy(Gtk.Widget sender) { Composer.Widget? composer = sender as Composer.Widget; if (composer != null) { composer_widgets.remove((Composer.Widget) sender); - debug(@"Composer type $(composer.compose_type) destroyed; " + + debug(@"Composer type $(composer.context_type) destroyed; " + @"$(this.composer_widgets.size) composers remaining"); } } @@ -2485,7 +2430,6 @@ private class Application.SendComposerCommand : ComposerCommand { public override async void execute(GLib.Cancellable? cancellable) throws GLib.Error { Geary.ComposedEmail email = yield this.composer.get_composed_email(); - if (this.can_undo) { /// Translators: The label for an in-app notification. The /// string substitution is a list of recipients of the email. @@ -2510,7 +2454,9 @@ private class Application.SendComposerCommand : ComposerCommand { this.saved = null; this.composer.set_enabled(true); - this.application.controller.show_composer(this.composer, null, null); + this.application.controller.show_composer( + this.composer, this.composer.get_referred_ids() + ); clear_composer(); } @@ -2564,7 +2510,9 @@ private class Application.SaveComposerCommand : ComposerCommand { if (this.composer != null) { this.destroy_timer.reset(); this.composer.set_enabled(true); - this.controller.show_composer(this.composer, null, null); + this.controller.show_composer( + this.composer, this.composer.get_referred_ids() + ); clear_composer(); } else { /// Translators: A label for an in-app notification. @@ -2622,7 +2570,9 @@ private class Application.DiscardComposerCommand : ComposerCommand { if (this.composer != null) { this.destroy_timer.reset(); this.composer.set_enabled(true); - this.controller.show_composer(this.composer, null, null); + this.controller.show_composer( + this.composer, this.composer.get_referred_ids() + ); clear_composer(); } else { /// Translators: A label for an in-app notification. diff --git a/src/client/application/application-main-window.vala b/src/client/application/application-main-window.vala index 9fe6ab19..1a3fc269 100644 --- a/src/client/application/application-main-window.vala +++ b/src/client/application/application-main-window.vala @@ -766,7 +766,7 @@ public class Application.MainWindow : ); yield open_conversation_monitor(this.conversations, cancellable); - this.controller.process_pending_composers(); + yield this.controller.process_pending_composers(); } } @@ -864,16 +864,6 @@ public class Application.MainWindow : } } - /** Displays a composer addressed to a specific email address. */ - public void open_composer_for_mailbox(Geary.RFC822.MailboxAddress to) { - var composer = new Composer.Widget.from_mailbox( - this.application, this.selected_folder.account, to - ); - this.controller.add_composer(composer); - show_composer(composer, null); - composer.load.begin(null, false, null, null); - } - /** * Displays a composer in the window if possible, else in a new window. * @@ -883,8 +873,8 @@ public class Application.MainWindow : * the composer's {@link Composer.Widget.get_referred_ids} will be * used. */ - public void show_composer(Composer.Widget composer, - Gee.Collection? refers_to) { + internal void show_composer(Composer.Widget composer, + Gee.Collection? refers_to) { if (this.has_composer) { composer.detach(); } else { @@ -923,7 +913,7 @@ public class Application.MainWindow : * Returns true if none were open or the user approved closing * them. */ - public bool close_composer(bool should_prompt, bool is_shutdown = false) { + internal bool close_composer(bool should_prompt, bool is_shutdown = false) { bool closed = true; Composer.Widget? composer = this.conversation_viewer.current_composer; if (composer != null && @@ -1547,7 +1537,7 @@ public class Application.MainWindow : ); } - private void create_composer_from_viewer(Composer.Widget.ComposeType compose_type) { + private void create_composer_from_viewer(Composer.Widget.ContextType type) { Geary.Account? account = this.selected_account; ConversationEmail? email_view = null; ConversationListBox? list_view = this.conversation_viewer.current_list; @@ -1557,13 +1547,8 @@ public class Application.MainWindow : if (account != null && email_view != null) { email_view.get_selection_for_quoting.begin((obj, res) => { string? quote = email_view.get_selection_for_quoting.end(res); - this.controller.compose_with_context_email( - this, - account, - compose_type, - email_view.email, - quote, - false + this.controller.compose_with_context_email.begin( + type, email_view.email, quote ?? "" ); }); } @@ -2043,7 +2028,6 @@ public class Application.MainWindow : list.reply_to_all_email.connect(on_email_reply_to_all); list.reply_to_sender_email.connect(on_email_reply_to_sender); list.forward_email.connect(on_email_forward); - list.edit_email.connect(on_email_edit); list.trash_email.connect(on_email_trash); list.delete_email.connect(on_email_delete); } @@ -2102,31 +2086,9 @@ public class Application.MainWindow : // TODO: Determine how to map between conversations // and drafts correctly. Geary.Email draft = activated.get_latest_recv_email(IN_FOLDER); - - // Check all known composers since the draft may be - // open in a detached composer - bool already_open = false; - foreach (Composer.Widget composer - in this.controller.get_composers()) { - if (composer.current_draft_id != null && - composer.current_draft_id.equal_to(draft.id)) { - already_open = true; - composer.present(); - composer.set_focus(); - break; - } - } - - if (!already_open) { - this.controller.compose_with_context_email( - this, - activated.base_folder.account, - NEW_MESSAGE, - draft, - null, - true - ); - } + this.controller.compose_with_context_email.begin( + EDIT, draft, null + ); } } } @@ -2153,7 +2115,7 @@ public class Application.MainWindow : } private void on_reply_conversation() { - create_composer_from_viewer(REPLY); + create_composer_from_viewer(REPLY_SENDER); } private void on_reply_all_conversation() { @@ -2476,37 +2438,25 @@ public class Application.MainWindow : } private void on_email_reply_to_sender(Geary.Email target, string? quote) { - Geary.Account? account = this.selected_account; - if (account != null) { - this.controller.compose_with_context_email( - this, account, REPLY, target, quote, false + if (this.selected_account != null) { + this.controller.compose_with_context_email.begin( + REPLY_SENDER, target, quote ); } } private void on_email_reply_to_all(Geary.Email target, string? quote) { - Geary.Account? account = this.selected_account; - if (account != null) { - this.controller.compose_with_context_email( - this, account, REPLY_ALL, target, quote, false + if (this.selected_account != null) { + this.controller.compose_with_context_email.begin( + REPLY_ALL, target, quote ); } } private void on_email_forward(Geary.Email target, string? quote) { - Geary.Account? account = this.selected_account; - if (account != null) { - this.controller.compose_with_context_email( - this, account, FORWARD, target, quote, false - ); - } - } - - private void on_email_edit(Geary.Email target) { - Geary.Account? account = this.selected_account; - if (account != null) { - this.controller.compose_with_context_email( - this, account, NEW_MESSAGE, target, null, true + if (this.selected_account != null) { + this.controller.compose_with_context_email.begin( + FORWARD, target, quote ); } } diff --git a/src/client/application/application-plugin-manager.vala b/src/client/application/application-plugin-manager.vala index 378e65f4..b36e9e1a 100644 --- a/src/client/application/application-plugin-manager.vala +++ b/src/client/application/application-plugin-manager.vala @@ -183,12 +183,7 @@ public class Application.PluginManager : GLib.Object { } public void show() { - var composer = new Composer.Widget( - this.application, this.account.account, NEW_MESSAGE - ); - var main_window = this.application.get_active_main_window(); - main_window.show_composer(composer, null); - composer.load.begin(null, false, null, null); + this.application.controller.compose_new_email.begin(); } } diff --git a/src/client/composer/composer-embed.vala b/src/client/composer/composer-embed.vala index d92aca6d..26f6a336 100644 --- a/src/client/composer/composer-embed.vala +++ b/src/client/composer/composer-embed.vala @@ -43,7 +43,7 @@ public class Composer.Embed : Gtk.EventBox, Container { this.composer.embed_header(); Widget.PresentationMode mode = INLINE_COMPACT; - if (composer.compose_type == FORWARD || + if (composer.context_type == FORWARD || composer.has_multiple_from_addresses) { mode = INLINE; } diff --git a/src/client/composer/composer-web-view.vala b/src/client/composer/composer-web-view.vala index 9c78ec75..d187759c 100644 --- a/src/client/composer/composer-web-view.vala +++ b/src/client/composer/composer-web-view.vala @@ -148,7 +148,7 @@ public class Composer.WebView : ClientWebView { public new void load_html(string body, string quote, bool top_posting, - bool is_draft) { + bool body_complete) { const string HTML_PRE = """"""; const string HTML_POST = """"""; const string BODY_PRE = """ @@ -165,7 +165,7 @@ public class Composer.WebView : ClientWebView { StringBuilder html = new StringBuilder(); string body_class = (this.is_rich_text) ? "" : "plain"; html.append(HTML_PRE.printf(body_class)); - if (!is_draft) { + if (!body_complete) { html.append(BODY_PRE); bool have_body = !Geary.String.is_empty(body); if (have_body) { @@ -185,7 +185,7 @@ public class Composer.WebView : ClientWebView { html.append_printf(QUOTE, quote); } } else { - html.append(quote); + html.append(body); } html.append(HTML_POST); base.load_html((string) html.data); diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala index 6cceec72..ee169448 100644 --- a/src/client/composer/composer-widget.vala +++ b/src/client/composer/composer-widget.vala @@ -1,6 +1,6 @@ /* - * Copyright 2016 Software Freedom Conservancy Inc. - * Copyright 2017-2019 Michael Gratton + * Copyright © 2016 Software Freedom Conservancy Inc. + * Copyright © 2017-2020 Michael Gratton * * This software is licensed under the GNU Lesser General Public License * (version 2.1 or later). See the COPYING file in this distribution. @@ -25,15 +25,31 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { /** The email fields the composer requires for referred email. */ - public const Geary.Email.Field REQUIRED_FIELDS = ENVELOPE | BODY; + public const Geary.Email.Field REQUIRED_FIELDS = ENVELOPE | HEADER | BODY; /// Translators: Title for an empty composer window private const string DEFAULT_TITLE = _("New Message"); + + /** + * Determines the type of the context email passed to the composer + * + * @see context_type + * @see load_context + */ + public enum ContextType { + /** No context mail was provided. */ + NONE, - public enum ComposeType { - NEW_MESSAGE, - REPLY, + /** Context is an email to edited, for example a draft or template. */ + EDIT, + + /** Context is an email being replied to the sender only. */ + REPLY_SENDER, + + /** Context is an email being replied to all recipients. */ REPLY_ALL, + + /** Context is an email being forwarded. */ FORWARD } @@ -242,17 +258,17 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { /** The account the email is being sent from. */ public Geary.Account account { get; private set; } - /** The identifier of the draft this composer holds, if any. */ - public Geary.EmailIdentifier? current_draft_id { + /** The identifier of the saved email this composer holds, if any. */ + public Geary.EmailIdentifier? saved_id { get; private set; default = null; } + /** Determines the type of the context email. */ + public ContextType context_type { get; private set; default = NONE; } + /** Determines the composer's current presentation mode. */ public PresentationMode current_mode { get; set; default = NONE; } - /** Determines the type of email being composed. */ - public ComposeType compose_type { get; private set; default = ComposeType.NEW_MESSAGE; } - /** Determines if the composer is completely empty. */ public bool is_blank { get { @@ -431,17 +447,12 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { private Gee.Collection accounts; - private string body_html = ""; - private string? pointer_url = null; private string? cursor_url = null; private bool is_attachment_overlay_visible = false; private Geary.RFC822.MailboxAddresses reply_to_addresses; private Geary.RFC822.MailboxAddresses reply_cc_addresses; - private string reply_subject = ""; - private string forward_subject = ""; private bool top_posting = true; - private string? last_quote = null; // The message(s) this email is in reply to/forwarded from private Gee.Set referred_ids = @@ -491,8 +502,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { public Widget(Application.Client application, - Geary.Account initial_account, - ComposeType compose_type) { + Geary.Account initial_account) { components_reflow_box_get_type(); base_ref(); this.application = application; @@ -504,8 +514,6 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { warning("Could not fetch account info: %s", e.message); } - this.compose_type = compose_type; - this.header = new Headerbar(application.config); this.header.expand_composer.connect(on_expand_compact_headers); @@ -642,18 +650,20 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { update_color_icon.begin(Util.Gtk.rgba(0, 0, 0, 0)); } - public Widget.from_mailbox(Application.Client application, - Geary.Account initial_account, - Geary.RFC822.MailboxAddress to) { - this(application, initial_account, ComposeType.NEW_MESSAGE); - this.to = to.to_full_display(); + ~Widget() { + base_unref(); } - public Widget.from_mailto(Application.Client application, - Geary.Account initial_account, - string mailto) { - this(application, initial_account, ComposeType.NEW_MESSAGE); + public async void load_empty_body(Geary.RFC822.MailboxAddress? to = null) + throws GLib.Error { + if (to != null) { + this.to = to.to_full_display(); + } + yield finish_loading("", "", false, null); + } + public async void load_mailto(string mailto) + throws GLib.Error { Gee.HashMultiMap headers = new Gee.HashMultiMap(); if (mailto.has_prefix(MAILTO_URI_PREFIX)) { // Parse the mailto link. @@ -687,9 +697,14 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { if (headers.contains("subject")) this.subject = Geary.Collection.first(headers.get("subject")); - if (headers.contains("body")) - this.body_html = Geary.HTML.preserve_whitespace(Geary.HTML.escape_markup( - Geary.Collection.first(headers.get("body")))); + var body = ""; + if (headers.contains("body")) { + body = Geary.HTML.preserve_whitespace( + Geary.HTML.escape_markup( + Geary.Collection.first(headers.get("body")) + ) + ); + } Gee.List attachments = new Gee.LinkedList(); attachments.add_all(headers.get("attach")); @@ -701,11 +716,75 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { attachment_failed(err.message); } } + yield finish_loading(body, "", false, null); } } - ~Widget() { - base_unref(); + /** + * Loads the message into the composer editor. + */ + public async void load_context(ContextType type, + Geary.Email context, + string? quote) + throws GLib.Error { + if (type == NONE) { + throw new Geary.EngineError.BAD_PARAMETERS( + "Invalid context type: %s", type.to_string() + ); + } + + if (!context.fields.is_all_set(REQUIRED_FIELDS)) { + throw new Geary.EngineError.INCOMPLETE_MESSAGE( + "Required fields not met: %s", context.fields.to_string() + ); + } + + this.context_type = type; + + if (type == EDIT || + type == FORWARD) { + this.pending_include = AttachPending.ALL; + } + + var body = ""; + var complete_quote = ""; + switch (type) { + case EDIT: + this.saved_id = context.id; + yield restore_reply_to_state(); + fill_in_from_context(context); + Geary.RFC822.Message message = context.get_message(); + body = ( + message.has_html_body() + ? message.get_html_body(null) + : message.get_plain_body(true, null) + ); + break; + + case REPLY_SENDER: + case REPLY_ALL: + add_recipients_and_ids(this.context_type, context); + fill_in_from_context(context); + complete_quote = Util.Email.quote_email_for_reply( + context, quote, this.application.config.clock_format, HTML + ); + if (!Geary.String.is_empty(quote)) { + this.top_posting = false; + } else { + this.can_delete_quote = true; + } + break; + + case FORWARD: + add_recipients_and_ids(this.context_type, context); + fill_in_from_context(context); + complete_quote = Util.Email.quote_email_for_forward( + context, quote, HTML + ); + break; + } + + yield finish_loading(body, complete_quote, (type == EDIT), null); } /** @@ -719,51 +798,6 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { return this.referred_ids.read_only_view; } - /** - * Loads the message into the composer editor. - */ - public async void load(Geary.Email? referred = null, - bool is_draft, - string? quote = null, - GLib.Cancellable? cancellable) - throws GLib.Error { - if (referred != null && - !referred.fields.is_all_set(REQUIRED_FIELDS)) { - throw new Geary.EngineError.INCOMPLETE_MESSAGE( - "Required fields not met: %s", referred.fields.to_string() - ); - } - string referred_quote = ""; - this.last_quote = quote; - if (referred != null) { - referred_quote = fill_in_from_referred(referred, quote); - if (is_draft || - compose_type == ComposeType.NEW_MESSAGE || - compose_type == ComposeType.FORWARD) { - this.pending_include = AttachPending.ALL; - } - if (is_draft) { - yield restore_reply_to_state(); - } - } - - update_attachments_view(); - update_pending_attachments(this.pending_include, true); - - this.editor.load_html( - this.body_html, - referred_quote, - this.top_posting, - is_draft - ); - - try { - yield open_draft_manager(is_draft ? referred.id : null, cancellable); - } catch (Error e) { - debug("Could not open draft manager: %s", e.message); - } - } - /** Detaches the composer and opens it in a new window. */ public void detach() { Gtk.Widget? focused_widget = null; @@ -954,7 +988,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { this.header.set_sensitive(enabled); if (enabled) { - this.open_draft_manager.begin(this.current_draft_id, null); + this.open_draft_manager.begin(this.saved_id, null); } else { if (this.container != null) { this.container.close(); @@ -1006,25 +1040,18 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { if (email == null) continue; - // XXX pretty sure we are calling this only to update the - // composer's internal set of ids - we really shouldn't be - // messing around with the draft's recipients since the - // user may have already updated them. - add_recipients_and_ids(this.compose_type, email, false); + add_recipients_and_ids(this.context_type, email, false); - if (first_email) { - this.reply_subject = Geary.RFC822.Utils.create_subject_for_reply(email); - this.forward_subject = Geary.RFC822.Utils.create_subject_for_forward(email); - first_email = false; - } + first_email = false; } if (first_email) // Either no referenced emails, or we don't have them. Treat as new. return; - if (this.cc == "") - this.compose_type = ComposeType.REPLY; - else - this.compose_type = ComposeType.REPLY_ALL; + if (this.cc == "") { + this.context_type = REPLY_SENDER; + } else { + this.context_type = REPLY_ALL; + } if (!to_entry.addresses.equal_to(reply_to_addresses)) this.to_entry.set_modified(); @@ -1054,18 +1081,12 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } } - // Copies the addresses (e.g. From/To/CC) and content from referred into this one - private string fill_in_from_referred(Geary.Email referred, string? quote) { - string referred_quote = ""; - if (this.compose_type != ComposeType.NEW_MESSAGE) { - add_recipients_and_ids(this.compose_type, referred); - this.reply_subject = Geary.RFC822.Utils.create_subject_for_reply(referred); - this.forward_subject = Geary.RFC822.Utils.create_subject_for_forward(referred); - } + // Copies the addresses (e.g. From/To/CC) and content from + // referred into this one + private void fill_in_from_context(Geary.Email referred) { this.pending_attachments = referred.attachments; - switch (this.compose_type) { - // Restoring a draft - case ComposeType.NEW_MESSAGE: + switch (this.context_type) { + case EDIT: if (referred.from != null) this.from = referred.from; if (referred.to != null) @@ -1084,41 +1105,26 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { this.references = referred.references.to_rfc822_string(); if (referred.subject != null) this.subject = referred.subject.value ?? ""; - try { - Geary.RFC822.Message message = referred.get_message(); - if (message.has_html_body()) { - referred_quote = message.get_html_body(null); - } else { - referred_quote = message.get_plain_body(true, null); - } - } catch (Error error) { - debug("Error getting draft message body: %s", error.message); - } break; - case ComposeType.REPLY: - case ComposeType.REPLY_ALL: - this.subject = reply_subject; - this.references = Geary.RFC822.Utils.reply_references(referred); - referred_quote = Util.Email.quote_email_for_reply(referred, quote, - this.application.config.clock_format, - Geary.RFC822.TextFormat.HTML); - if (!Geary.String.is_empty(quote)) { - this.top_posting = false; - } else { - this.can_delete_quote = true; - } + case REPLY_SENDER: + case REPLY_ALL: + this.subject = Geary.RFC822.Utils.create_subject_for_reply( + referred + ); + this.references = Geary.RFC822.Utils.reply_references( + referred + ); break; - case ComposeType.FORWARD: - this.subject = forward_subject; - referred_quote = Util.Email.quote_email_for_forward(referred, quote, - Geary.RFC822.TextFormat.HTML); + case FORWARD: + this.subject = Geary.RFC822.Utils.create_subject_for_forward( + referred + ); break; } update_extended_headers(); - return referred_quote; } public void present() { @@ -1319,7 +1325,8 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { this.subject ); - if ((this.compose_type == ComposeType.REPLY || this.compose_type == ComposeType.REPLY_ALL) && + if ((this.context_type == REPLY_SENDER || + this.context_type == REPLY_ALL) && !this.in_reply_to.is_empty) email.set_in_reply_to( new Geary.RFC822.MessageIDList.from_collection(this.in_reply_to) @@ -1359,7 +1366,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { /** Appends an email or fragment quoted into the composer. */ public void append_to_email(Geary.Email referred, string? to_quote, - ComposeType type) + ContextType type) throws Geary.EngineError { if (!referred.fields.is_all_set(REQUIRED_FIELDS)) { throw new Geary.EngineError.INCOMPLETE_MESSAGE( @@ -1371,30 +1378,29 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { add_recipients_and_ids(type, referred); } - if (this.last_quote != to_quote) { - this.last_quote = to_quote; - // Always use reply styling, since forward styling doesn't - // work for inline quotes - this.editor.insert_html( - Util.Email.quote_email_for_reply( - referred, - to_quote, - this.application.config.clock_format, - Geary.RFC822.TextFormat.HTML - ) - ); - } + // Always use reply styling, since forward styling doesn't + // work for inline quotes + this.editor.insert_html( + Util.Email.quote_email_for_reply( + referred, + to_quote, + this.application.config.clock_format, + Geary.RFC822.TextFormat.HTML + ) + ); } - private void add_recipients_and_ids(ComposeType type, Geary.Email referred, - bool modify_headers = true) { + private void add_recipients_and_ids(ContextType type, + Geary.Email referred, + bool modify_headers = true) { Gee.List sender_addresses = account.information.sender_mailboxes; // Set the preferred from address. New messages should retain // the account default and drafts should retain the draft's // from addresses, so don't update them here - if (this.compose_type != ComposeType.NEW_MESSAGE) { + if (this.context_type != NONE && + this.context_type != EDIT) { if (!check_preferred_from_address(sender_addresses, referred.to)) { if (!check_preferred_from_address(sender_addresses, referred.cc)) if (!check_preferred_from_address(sender_addresses, referred.bcc)) @@ -1417,16 +1423,25 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { bool recipients_modified = this.to_entry.is_modified || this.cc_entry.is_modified || this.bcc_entry.is_modified; if (!recipients_modified) { - if (type == ComposeType.REPLY || type == ComposeType.REPLY_ALL) - this.to_entry.addresses = Geary.RFC822.Utils.merge_addresses(to_entry.addresses, - to_addresses); - if (type == ComposeType.REPLY_ALL) + if (type == REPLY_SENDER || type == REPLY_ALL) { + this.to_entry.addresses = Geary.RFC822.Utils.merge_addresses( + to_entry.addresses, + to_addresses + ); + } + if (type == REPLY_ALL) { this.cc_entry.addresses = Geary.RFC822.Utils.remove_addresses( - Geary.RFC822.Utils.merge_addresses(this.cc_entry.addresses, cc_addresses), - this.to_entry.addresses); - else - this.cc_entry.addresses = Geary.RFC822.Utils.remove_addresses(this.cc_entry.addresses, - this.to_entry.addresses); + Geary.RFC822.Utils.merge_addresses( + this.cc_entry.addresses, cc_addresses + ), + this.to_entry.addresses + ); + } else { + this.cc_entry.addresses = Geary.RFC822.Utils.remove_addresses( + this.cc_entry.addresses, + this.to_entry.addresses + ); + } } if (referred.message_id != null) { @@ -1493,6 +1508,27 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } } + private async void finish_loading(string body, + string quote, + bool is_body_complete, + GLib.Cancellable? cancellable) { + update_attachments_view(); + update_pending_attachments(this.pending_include, true); + + this.editor.load_html( + body, + quote, + this.top_posting, + is_body_complete + ); + + try { + yield open_draft_manager(this.saved_id, cancellable); + } catch (Error e) { + debug("Could not open draft manager: %s", e.message); + } + } + private async bool should_send() { bool has_subject = !Geary.String.is_empty(subject.strip()); bool has_attachment = this.attached_files.size > 0; @@ -1626,8 +1662,8 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { this.draft_timer.reset(); this.draft_manager = null; + this.saved_id = null; this.draft_status_text = ""; - this.current_draft_id = null; old_manager.notify[Geary.App.DraftManager.PROP_DRAFT_STATE] .disconnect(on_draft_state_changed); @@ -2666,7 +2702,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface { } private void on_draft_id_changed() { - this.current_draft_id = this.draft_manager.current_draft_id; + this.saved_id = this.draft_manager.current_draft_id; } private void on_draft_manager_fatal(Error err) { diff --git a/src/client/conversation-viewer/conversation-contact-popover.vala b/src/client/conversation-viewer/conversation-contact-popover.vala index 9d58193c..0aa329a9 100644 --- a/src/client/conversation-viewer/conversation-contact-popover.vala +++ b/src/client/conversation-viewer/conversation-contact-popover.vala @@ -240,7 +240,7 @@ public class Conversation.ContactPopover : Gtk.Popover { private void on_new_conversation() { var main = this.get_toplevel() as Application.MainWindow; if (main != null) { - main.open_composer_for_mailbox(this.mailbox); + main.application.new_composer.begin(this.mailbox); } } diff --git a/src/client/conversation-viewer/conversation-list-box.vala b/src/client/conversation-viewer/conversation-list-box.vala index 81e22d86..e727a3cb 100644 --- a/src/client/conversation-viewer/conversation-list-box.vala +++ b/src/client/conversation-viewer/conversation-list-box.vala @@ -891,8 +891,8 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { add(row); this.current_composer = row; - embed.composer.notify["current-draft-id"].connect( - (id) => { this.draft_id = embed.composer.current_draft_id; } + embed.composer.notify["saved-id"].connect( + (id) => { this.draft_id = embed.composer.saved_id; } ); embed.vanished.connect(() => { this.current_composer = null; diff --git a/src/client/conversation-viewer/conversation-viewer.vala b/src/client/conversation-viewer/conversation-viewer.vala index 461c7c94..dbb50c4d 100644 --- a/src/client/conversation-viewer/conversation-viewer.vala +++ b/src/client/conversation-viewer/conversation-viewer.vala @@ -187,7 +187,7 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface { if (this.current_list != null) { this.current_list.add_embedded_composer( embed, - composer.current_draft_id != null + composer.saved_id != null ); composer.update_window_title(); }