[geary] Properly restore state of drafts



commit 09582736b8620bc1088ca8503cb87fdef24ceeaf
Author: Robert Schroll <rschroll gmail com>
Date:   Fri Jan 16 14:35:19 2015 -0800

    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

 src/client/application/geary-controller.vala       |   23 ++++-
 src/client/composer/composer-embed.vala            |    9 ++-
 src/client/composer/composer-widget.vala           |  103 ++++++++++++++++++-
 .../conversation-viewer/conversation-viewer.vala   |   45 +++++++++
 src/engine/app/app-draft-manager.vala              |    1 +
 src/engine/rfc822/rfc822-utils.vala                |   17 +++
 theming/message-viewer.html                        |    1 +
 7 files changed, 186 insertions(+), 13 deletions(-)
---
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 7b1ac8b..27affab 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -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() {
diff --git a/src/client/composer/composer-embed.vala b/src/client/composer/composer-embed.vala
index 6629ab9..7bf5d07 100644
--- a/src/client/composer/composer-embed.vala
+++ b/src/client/composer/composer-embed.vala
@@ -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;
     }
     
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index 8129f2a..a2b4de6 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -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();
     }
diff --git a/src/client/conversation-viewer/conversation-viewer.vala 
b/src/client/conversation-viewer/conversation-viewer.vala
index c899e90..2704986 100644
--- a/src/client/conversation-viewer/conversation-viewer.vala
+++ b/src/client/conversation-viewer/conversation-viewer.vala
@@ -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);
diff --git a/src/engine/app/app-draft-manager.vala b/src/engine/app/app-draft-manager.vala
index 985f700..860248f 100644
--- a/src/engine/app/app-draft-manager.vala
+++ b/src/engine/app/app-draft-manager.vala
@@ -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";
diff --git a/src/engine/rfc822/rfc822-utils.vala b/src/engine/rfc822/rfc822-utils.vala
index 7837e57..a76373b 100644
--- a/src/engine/rfc822/rfc822-utils.vala
+++ b/src/engine/rfc822/rfc822-utils.vala
@@ -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>();
diff --git a/theming/message-viewer.html b/theming/message-viewer.html
index b928029..a7deabc 100644
--- a/theming/message-viewer.html
+++ b/theming/message-viewer.html
@@ -2,6 +2,7 @@
 <head>
     <title>Geary</title>
     <style id="default_fonts"></style>
+    <style id="blacklist_ids"></style>
 </head>
 <body>
 <div id="message_container"><span id="placeholder"></span></div>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]