Properly restore state of drafts

We do this heuristically, by noting which messages it is in reply to and
seeing if it matches the unmodified reply or reply all states.  This
isn't perfect; notably forwarded messages are picked up.

We hide the existing draft in the conversation viewer as soon as we open
the composer.  We also ensure that future versions of this draft will
also be hidden.  The list of emails to be hidden is cleared when the
conversation viewer is, so we query all open composers at this point to
see which email ids should be hidden.

https://bugzilla.gnome.org/show_bug.cgi?id=743067
This commit is contained in:
Robert Schroll 2015-01-16 14:35:19 -08:00
parent 526f6e7889
commit 09582736b8
7 changed files with 186 additions and 13 deletions

View file

@ -2103,7 +2103,7 @@ public class GearyController : Geary.BaseObject {
return;
bool inline;
if (!should_create_new_composer(compose_type, referred, quote, out inline))
if (!should_create_new_composer(compose_type, referred, quote, is_draft, out inline))
return;
ComposerWidget widget;
@ -2122,17 +2122,24 @@ public class GearyController : Geary.BaseObject {
}
widget = new ComposerWidget(current_account, compose_type, full, quote, is_draft);
if (is_draft) {
yield widget.restore_draft_state_async(current_account);
main_window.conversation_viewer.blacklist_by_id(referred.id);
}
}
widget.show_all();
// We want to keep track of the open composer windows, so we can allow the user to cancel
// an exit without losing their data.
composer_widgets.add(widget);
debug(@"Creating composer of type $compose_type; $(composer_widgets.size) composers total");
debug(@"Creating composer of type $(widget.compose_type); $(composer_widgets.size) composers total");
widget.destroy.connect(on_composer_widget_destroy);
if (inline) {
new ComposerEmbed(widget, main_window.conversation_viewer, referred);
if (widget.state == ComposerWidget.ComposerState.PANED)
main_window.conversation_viewer.set_paned_composer(widget);
else
new ComposerEmbed(widget, main_window.conversation_viewer, referred); // is_draft
} else {
new ComposerWindow(widget);
widget.state = ComposerWidget.ComposerState.DETACHED;
@ -2140,7 +2147,7 @@ public class GearyController : Geary.BaseObject {
}
private bool should_create_new_composer(ComposerWidget.ComposeType? compose_type,
Geary.Email? referred, string? quote, out bool inline) {
Geary.Email? referred, string? quote, bool is_draft, out bool inline) {
inline = true;
// In we're replying, see whether we already have a reply for that message.
@ -2161,6 +2168,12 @@ public class GearyController : Geary.BaseObject {
if (!any_inline_composers())
return true;
// If we're resuming a draft with open composers, open in a new window.
if (is_draft) {
inline = false;
return true;
}
// If we're creating a new message, and there's already a new message open, focus on
// it if it hasn't been modified; otherwise open a new composer in a new window.
if (compose_type == ComposerWidget.ComposeType.NEW_MESSAGE) {
@ -2197,7 +2210,7 @@ public class GearyController : Geary.BaseObject {
public bool can_switch_conversation_view() {
bool inline;
return should_create_new_composer(null, null, null, out inline);
return should_create_new_composer(null, null, null, false, out inline);
}
public bool any_inline_composers() {

View file

@ -17,6 +17,7 @@ public class ComposerEmbed : Gtk.EventBox, ComposerContainer {
private double inner_scroll_adj_value;
private int inner_view_height;
private int min_height = MIN_EDITOR_HEIGHT;
private bool has_accel_group = false;
public Gtk.Window top_window {
get { return (Gtk.Window) get_toplevel(); }
@ -30,7 +31,7 @@ public class ComposerEmbed : Gtk.EventBox, ComposerContainer {
valign = Gtk.Align.FILL;
WebKit.DOM.HTMLElement? email_element = null;
if (referred != null) {
if (referred != null && composer.state != ComposerWidget.ComposerState.INLINE_NEW) {
email_element = conversation_viewer.web_view.get_dom_document().get_element_by_id(
conversation_viewer.get_div_id(referred.id)) as WebKit.DOM.HTMLElement;
embed_id = referred.id.to_string() + "_reply";
@ -198,12 +199,16 @@ public class ComposerEmbed : Gtk.EventBox, ComposerContainer {
}
private bool on_focus_in() {
top_window.add_accel_group(composer.ui.get_accel_group());
// For some reason, on_focus_in gets called a bunch upon construction.
if (!has_accel_group)
top_window.add_accel_group(composer.ui.get_accel_group());
has_accel_group = true;
return false;
}
private bool on_focus_out() {
top_window.remove_accel_group(composer.ui.get_accel_group());
has_accel_group = false;
return false;
}

View file

@ -414,9 +414,11 @@ public class ComposerWidget : Gtk.EventBox {
from_multiple.changed.connect(on_from_changed);
if (referred != null) {
add_recipients_and_ids(compose_type, referred);
reply_subject = Geary.RFC822.Utils.create_subject_for_reply(referred);
forward_subject = Geary.RFC822.Utils.create_subject_for_forward(referred);
if (compose_type != ComposeType.NEW_MESSAGE) {
add_recipients_and_ids(compose_type, referred);
reply_subject = Geary.RFC822.Utils.create_subject_for_reply(referred);
forward_subject = Geary.RFC822.Utils.create_subject_for_forward(referred);
}
last_quote = quote;
switch (compose_type) {
case ComposeType.NEW_MESSAGE:
@ -554,6 +556,14 @@ public class ComposerWidget : Gtk.EventBox {
if (!from_multiple.visible)
open_draft_manager_async.begin(null);
// Remind the conversation viewer of draft ids when it reloads
ConversationViewer conversation_viewer =
GearyApplication.instance.controller.main_window.conversation_viewer;
conversation_viewer.cleared.connect(() => {
if (draft_manager != null)
conversation_viewer.blacklist_by_id(draft_manager.current_draft_id);
});
destroy.connect(() => { close_draft_manager_async.begin(null); });
}
@ -602,6 +612,66 @@ public class ComposerWidget : Gtk.EventBox {
}
}
public async void restore_draft_state_async(Geary.Account account) {
bool first_email = true;
foreach (Geary.RFC822.MessageID mid in in_reply_to) {
Gee.MultiMap<Geary.Email, Geary.FolderPath?>? email_map;
try {
email_map =
yield account.local_search_message_id_async(mid, Geary.Email.Field.ENVELOPE,
true, null, new Geary.EmailFlags.with(Geary.EmailFlags.DRAFT)); // TODO: Folder blacklist
} catch (Error error) {
continue;
}
if (email_map == null)
continue;
Gee.Set<Geary.Email> emails = email_map.get_keys();
Geary.Email? email = null;
foreach (Geary.Email candidate in emails) {
if (candidate.message_id.equal_to(mid)) {
email = candidate;
break;
}
}
if (email == null)
continue;
add_recipients_and_ids(compose_type, email, false);
if (first_email) {
reply_subject = Geary.RFC822.Utils.create_subject_for_reply(email);
forward_subject = Geary.RFC822.Utils.create_subject_for_forward(email);
first_email = false;
}
}
if (first_email) // Either no referenced emails, or we don't have them. Treat as new.
return;
if (cc == "")
compose_type = ComposeType.REPLY;
else
compose_type = ComposeType.REPLY_ALL;
to_entry.modified = cc_entry.modified = bcc_entry.modified = false;
if (!Geary.RFC822.Utils.equal(to_entry.addresses, reply_to_addresses))
to_entry.modified = true;
if (cc != "" && !Geary.RFC822.Utils.equal(cc_entry.addresses, reply_cc_addresses))
cc_entry.modified = true;
if (bcc != "")
bcc_entry.modified = true;
if (in_reply_to.size > 1) {
state = ComposerState.PANED;
} else if (compose_type == ComposeType.FORWARD || to_entry.modified || cc_entry.modified ||
bcc_entry.modified) {
state = ComposerState.INLINE;
} else {
state = ComposerState.INLINE_COMPACT;
// Set recipients in header
validate_send_button();
}
}
public void set_focus() {
if (Geary.String.is_empty(to)) {
to_entry.grab_focus();
@ -613,6 +683,13 @@ public class ComposerWidget : Gtk.EventBox {
}
private void on_load_finished(WebKit.WebFrame frame) {
if (get_realized())
on_load_finished_and_realized();
else
realize.connect(on_load_finished_and_realized);
}
private void on_load_finished_and_realized() {
WebKit.DOM.Document document = editor.get_dom_document();
WebKit.DOM.HTMLElement? body = document.get_element_by_id(BODY_ID) as WebKit.DOM.HTMLElement;
assert(body != null);
@ -841,7 +918,8 @@ public class ComposerWidget : Gtk.EventBox {
set_focus();
}
private void add_recipients_and_ids(ComposeType type, Geary.Email referred) {
private void add_recipients_and_ids(ComposeType type, Geary.Email referred,
bool modify_headers = true) {
string? sender_address = account.information.get_mailbox_address().address;
Geary.RFC822.MailboxAddresses to_addresses =
Geary.RFC822.Utils.create_to_addresses_for_reply(referred, sender_address);
@ -852,6 +930,9 @@ public class ComposerWidget : Gtk.EventBox {
Geary.RFC822.Utils.merge_addresses(reply_cc_addresses, cc_addresses),
reply_to_addresses);
if (!modify_headers)
return;
bool recipients_modified = to_entry.modified || cc_entry.modified || bcc_entry.modified;
if (!recipients_modified) {
if (type == ComposeType.REPLY || type == ComposeType.REPLY_ALL)
@ -975,8 +1056,8 @@ public class ComposerWidget : Gtk.EventBox {
state = ComposerWidget.ComposerState.DETACHED;
}
private void ensure_paned() {
if (state == ComposerState.PANED)
public void ensure_paned() {
if (state == ComposerState.PANED || state == ComposerState.DETACHED)
return;
container.remove_composer();
GearyApplication.instance.controller.main_window.conversation_viewer
@ -1140,12 +1221,18 @@ public class ComposerWidget : Gtk.EventBox {
}
}
private void on_draft_id_changed() {
GearyApplication.instance.controller.main_window.conversation_viewer.blacklist_by_id(
draft_manager.current_draft_id);
}
private void on_draft_manager_fatal(Error err) {
draft_save_text = DRAFT_ERROR_TEXT;
}
private void connect_to_draft_manager() {
draft_manager.notify[Geary.App.DraftManager.PROP_DRAFT_STATE].connect(on_draft_state_changed);
draft_manager.notify[Geary.App.DraftManager.PROP_CURRENT_DRAFT_ID].connect(on_draft_id_changed);
draft_manager.fatal.connect(on_draft_manager_fatal);
}
@ -1154,6 +1241,7 @@ public class ComposerWidget : Gtk.EventBox {
// be moved back into open/close methods
private void disconnect_from_draft_manager() {
draft_manager.notify[Geary.App.DraftManager.PROP_DRAFT_STATE].disconnect(on_draft_state_changed);
draft_manager.notify[Geary.App.DraftManager.PROP_CURRENT_DRAFT_ID].disconnect(on_draft_id_changed);
draft_manager.fatal.disconnect(on_draft_manager_fatal);
}
@ -1274,6 +1362,9 @@ public class ComposerWidget : Gtk.EventBox {
} catch (Error err) {
// ignored
}
if (draft_manager != null)
GearyApplication.instance.controller.main_window.conversation_viewer
.unblacklist_by_id(draft_manager.current_draft_id);
container.close_container();
}

View file

@ -124,6 +124,9 @@ public class ConversationViewer : Gtk.Box {
// Fired when the user clicks the edit draft button.
public signal void edit_draft(Geary.Email message);
// Fired when the viewer has been cleared.
public signal void cleared();
// List of emails in this view.
public Gee.TreeSet<Geary.Email> messages { get; private set; default =
new Gee.TreeSet<Geary.Email>(Geary.Email.compare_date_ascending); }
@ -172,6 +175,7 @@ public class ConversationViewer : Gtk.Box {
private int next_replaced_buffer_number = 0;
private Gee.HashMap<string, ReplacedImage> replaced_images = new Gee.HashMap<string, ReplacedImage>();
private Gee.HashSet<string> replaced_content_ids = new Gee.HashSet<string>();
private Gee.HashSet<string> blacklist_ids = new Gee.HashSet<string>();
public ConversationViewer() {
Object(orientation: Gtk.Orientation.VERTICAL, spacing: 0);
@ -318,8 +322,11 @@ public class ConversationViewer : Gtk.Box {
inlined_content_ids.clear();
replaced_images.clear();
replaced_content_ids.clear();
blacklist_ids.clear();
blacklist_css();
current_account_information = account_information;
cleared();
}
// Converts an email ID into HTML ID used by the <div> for the email.
@ -327,6 +334,44 @@ public class ConversationViewer : Gtk.Box {
return "message_%s".printf(id.to_string());
}
public void blacklist_by_id(Geary.EmailIdentifier? id) {
if (id == null)
return;
blacklist_ids.add(get_div_id(id));
blacklist_css();
}
public void unblacklist_by_id(Geary.EmailIdentifier? id) {
if (id == null)
return;
blacklist_ids.remove(get_div_id(id));
blacklist_css();
}
private void blacklist_css() {
GLib.StringBuilder rule = new GLib.StringBuilder();
bool first = true;
foreach (string id in blacklist_ids) {
if (!first)
rule.append(", ");
else
first = false;
rule.append("div[id=\"" + id + "\"]");
}
if (!first)
rule.append(" { display: none; }");
WebKit.DOM.HTMLElement? style_element = web_view.get_dom_document()
.get_element_by_id("blacklist_ids") as WebKit.DOM.HTMLElement;
if (style_element != null) {
try {
style_element.set_inner_html(rule.str);
} catch (Error error) {
debug("Error setting blaklist CSS: %s", error.message);
}
}
}
private void show_special_message(string msg) {
// Remove any messages and hide the message container, then show the special message.
clear(current_folder, current_account_information);

View file

@ -28,6 +28,7 @@
public class Geary.App.DraftManager : BaseObject {
public const string PROP_IS_OPEN = "is-open";
public const string PROP_DRAFT_STATE = "draft-state";
public const string PROP_CURRENT_DRAFT_ID = "current-draft-id";
public const string PROP_VERSIONS_SAVED = "versions-saved";
public const string PROP_VERSIONS_DROPPED = "versions-dropped";
public const string PROP_DISCARD_ON_CLOSE = "discard-on-close";

View file

@ -140,6 +140,23 @@ public Geary.RFC822.MailboxAddresses remove_addresses(Geary.RFC822.MailboxAddres
return new Geary.RFC822.MailboxAddresses(result);
}
public bool equal(Geary.RFC822.MailboxAddresses? first, Geary.RFC822.MailboxAddresses? second) {
bool first_empty = first == null || first.size == 0;
bool second_empty = second == null || second.size == 0;
if (first_empty && second_empty || first == second)
return true;
if (first_empty || second_empty || first.size != second.size)
return false;
Gee.HashSet<string> first_addresses = new Gee.HashSet<string>();
Gee.HashSet<string> second_addresses = new Gee.HashSet<string>();
foreach (Geary.RFC822.MailboxAddress a in first)
first_addresses.add(a.as_key());
foreach (Geary.RFC822.MailboxAddress a in second)
second_addresses.add(a.as_key());
return Geary.Collection.are_sets_equal<string>(first_addresses, second_addresses);
}
public string reply_references(Geary.Email source) {
// generate list for References
Gee.ArrayList<RFC822.MessageID> list = new Gee.ArrayList<RFC822.MessageID>();