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.
This commit is contained in:
Michael Gratton 2020-04-19 15:51:03 +10:00 committed by Michael James Gratton
parent a98f953237
commit aac59ec53b
10 changed files with 417 additions and 482 deletions

View file

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

View file

@ -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<AccountContext> 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<Composer.Widget> 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<AccountContext> 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<Geary.EmailIdentifier>? refers_to,
MainWindow? show_on) {
var target = show_on;
if (target == null) {
target = this.application.get_active_main_window();
}
Gee.Collection<Geary.EmailIdentifier>? 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.

View file

@ -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<Geary.EmailIdentifier>? refers_to) {
internal void show_composer(Composer.Widget composer,
Gee.Collection<Geary.EmailIdentifier>? 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
);
}
}

View file

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

View file

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

View file

@ -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 = """<html><body class="%s">""";
const string HTML_POST = """</body></html>""";
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);

View file

@ -1,6 +1,6 @@
/*
* Copyright 2016 Software Freedom Conservancy Inc.
* Copyright 2017-2019 Michael Gratton <mike@vee.net>
* Copyright © 2016 Software Freedom Conservancy Inc.
* Copyright © 2017-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.
@ -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<Geary.Account> 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<Geary.EmailIdentifier> 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<string, string> headers = new Gee.HashMultiMap<string, string>();
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<string> attachments = new Gee.LinkedList<string>();
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<Geary.RFC822.MailboxAddress> 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) {

View file

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

View file

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

View file

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