[geary/wip/765516-gtk-widget-conversation-viewer: 40/174] Reenable displaying sub-messages.



commit 3c32a107ed4a2b10b49c3df0a15db1ade0fa66ab
Author: Michael James Gratton <mike vee net>
Date:   Tue Apr 19 16:52:34 2016 +1000

    Reenable displaying sub-messages.
    
    Geary currently displays RFC 822 attachments inline, below the email's
    primary message body, using the same HTML chrome for the headers and
    email body as for the primary body. Taking the same approach but using
    GTK+ widgets meant splitting ConversationMessage up into a
    ConversationEmail class that manages the UI for displaying an email in
    its entirety, and a ConversationMessage to manage the only header widgets
    and webview for displaying an individual RFC 822 message, usable for both
    the primary body and any sub-messages. Thus, this is a big change.
    
    One behavioural change is that each sub-message with remote images now
    requires individual approval, rather than being dependant on the
    containing message's sender and/or approval. This prevents some attacks
    e.g. a trusted sender forwarding a spam/malware message, but does not
    prevent it if the message is forwarded inline, obviosuly.
    
    * src/client/conversation-viewer/conversation-email.vala (ConversationEmail):
      New class for managing the UI for an overall email message. This
      replaces the old ConversationMessage and contains much of it's code and
      widgets - anything from that class which does not directly support
      displaying headers or a message body.
    
    * src/client/conversation-viewer/conversation-message.vala:
      (ConversationMessage): Same class as before, but now with its scope
      narrowed to only display message headers and body. The draft infobar
      remains here rather than being put ConversationEmail where it belongs
      since it's bit of a pain to insert in the right place and doesn't
      really hurt.
      (::email): Moved this property and any code that depends on it to
      ConversationEmail.
      (::always_load_remote_images): New property passed in via the ctor,
      allowing one dependency on the old ::email property to be removed.
      (::inlined_content_ids): Moved to ConversationEmail, since that is the
      class that keeps track of attachments to display. Add the signal
      attachment_displayed_inline to allow ConversationEmail to be notified
      of inlined attachments instead.
      (::flag_remote_images, ::remember_remote_images): New signals to notify
      ConversationEmail that the user has flagged this message or the
      message's sender for loading remote images. This is passed through
      since in the former's case we may need to set flags on the email
      itself, the latter because it is one less use of the contact_store
      property, which should be removed from this class at some point.
    
    * src/client/conversation-viewer/conversation-viewer.vala: Chase API
      changes from the above. In general, replace use of the term "message"
      with "email" since this class is now mostly dealing with
      ConversationEmail instances, rather than ConversationMessage instances.
      (ConversationViewer::check_mark_read): Only consider the
      ConversationEmail's primary message body when checking for visibility
      rather than that and any submessages to keep things simple.
      (ConversationViewer::show_message, ::hide_message): Renamed to
      expand_email/collapse_email respectively since we don't ever actually
      hide it. Carry that change on to same methods on ConversationEmail.
    
    * src/engine/rfc822/rfc822-message.vala (Geary.RFC822.Message): Add
      get_primary_originator(), almost vermatim from Geary.Email, to support
      determining the sender for remembering remote message loading for
      senders of sub-emails.
    
    * src/client/components/main-window.vala (MainWindow::set_styling): Fix
      background transition for collapsed emails.
    
    * src/client/application/geary-controller.vala: Chase API name changes.
    
    * src/CMakeLists.txt: Include new ConversationEmail source file.
    
    * ui/conversation-email.ui: New UI for ConversationEmail, move the email
      action box, attachments box amd sub-messages box here from
      conversation-message.ui.
    
    * ui/CMakeLists.txt: Include new UI in compiled resources.
    
    * po/POTFILES.in: Add new UI for transation.

 po/POTFILES.in                                     |    1 +
 src/CMakeLists.txt                                 |    1 +
 src/client/application/geary-controller.vala       |   26 +-
 .../conversation-viewer/conversation-email.vala    |  579 ++++++++++++++++++
 .../conversation-viewer/conversation-message.vala  |  635 ++------------------
 .../conversation-viewer/conversation-viewer.vala   |  173 +++---
 src/engine/rfc822/rfc822-message.vala              |   21 +-
 ui/CMakeLists.txt                                  |    1 +
 ui/conversation-email.ui                           |  174 ++++++
 ui/conversation-message.ui                         |  159 +-----
 ui/geary.css                                       |    5 +-
 11 files changed, 945 insertions(+), 830 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 717e8cf..d217ca5 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -384,6 +384,7 @@ src/mailer/main.vala
 [type: gettext/glade]ui/composer-headerbar.ui
 [type: gettext/glade]ui/composer-menus.ui
 [type: gettext/glade]ui/composer-widget.ui
+[type: gettext/glade]ui/conversation-email.ui
 [type: gettext/glade]ui/conversation-message.ui
 [type: gettext/glade]ui/conversation-message-menu.ui
 [type: gettext/glade]ui/conversation-viewer.ui
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 2c3a354..42e473d 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -360,6 +360,7 @@ client/conversation-list/conversation-list-store.vala
 client/conversation-list/conversation-list-view.vala
 client/conversation-list/formatted-conversation-data.vala
 
+client/conversation-viewer/conversation-email.vala
 client/conversation-viewer/conversation-message.vala
 client/conversation-viewer/conversation-viewer.vala
 client/conversation-viewer/conversation-web-view.vala
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 515f663..81660da 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -1,7 +1,9 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2016 Michael Gratton <mike vee net>
  *
  * This software is licensed under the GNU Lesser General Public License
- * (version 2.1 or later).  See the COPYING file in this distribution.
+ * (version 2.1 or later). See the COPYING file in this distribution.
  */
 
 // Required because Gcr's VAPI is behind-the-times
@@ -205,12 +207,12 @@ public class GearyController : Geary.BaseObject {
         main_window.main_toolbar.copy_folder_menu.folder_selected.connect(on_copy_conversation);
         main_window.main_toolbar.move_folder_menu.folder_selected.connect(on_move_conversation);
         main_window.search_bar.search_text_changed.connect(on_search_text_changed);
-        main_window.conversation_viewer.message_added.connect(on_message_added);
-        main_window.conversation_viewer.message_removed.connect(on_message_removed);
+        main_window.conversation_viewer.email_row_added.connect(on_email_row_added);
+        main_window.conversation_viewer.email_row_removed.connect(on_email_row_removed);
         main_window.conversation_viewer.reply_to_message.connect(on_reply_to_message);
         main_window.conversation_viewer.reply_all_message.connect(on_reply_all_message);
         main_window.conversation_viewer.forward_message.connect(on_forward_message);
-        main_window.conversation_viewer.mark_messages.connect(on_conversation_viewer_mark_messages);
+        main_window.conversation_viewer.mark_emails.connect(on_conversation_viewer_mark_emails);
         main_window.conversation_viewer.save_attachments.connect(on_save_attachments);
         main_window.conversation_viewer.save_buffer_to_file.connect(on_save_buffer_to_file);
         new_messages_monitor = new NewMessagesMonitor(should_notify_new_messages);
@@ -284,12 +286,12 @@ public class GearyController : Geary.BaseObject {
         main_window.main_toolbar.copy_folder_menu.folder_selected.disconnect(on_copy_conversation);
         main_window.main_toolbar.move_folder_menu.folder_selected.disconnect(on_move_conversation);
         main_window.search_bar.search_text_changed.disconnect(on_search_text_changed);
-        main_window.conversation_viewer.message_added.disconnect(on_message_added);
-        main_window.conversation_viewer.message_removed.disconnect(on_message_removed);
+        main_window.conversation_viewer.email_row_added.disconnect(on_email_row_added);
+        main_window.conversation_viewer.email_row_removed.disconnect(on_email_row_removed);
         main_window.conversation_viewer.reply_to_message.disconnect(on_reply_to_message);
         main_window.conversation_viewer.reply_all_message.disconnect(on_reply_all_message);
         main_window.conversation_viewer.forward_message.disconnect(on_forward_message);
-        main_window.conversation_viewer.mark_messages.disconnect(on_conversation_viewer_mark_messages);
+        main_window.conversation_viewer.mark_emails.disconnect(on_conversation_viewer_mark_emails);
         main_window.conversation_viewer.save_attachments.disconnect(on_save_attachments);
         main_window.conversation_viewer.save_buffer_to_file.disconnect(on_save_buffer_to_file);
         // hide window while shutting down, as this can take a few seconds under certain conditions
@@ -1777,7 +1779,7 @@ public class GearyController : Geary.BaseObject {
             flags_to_add, flags_to_remove);
     }
     
-    private void on_conversation_viewer_mark_messages(Gee.Collection<Geary.EmailIdentifier> emails,
+    private void on_conversation_viewer_mark_emails(Gee.Collection<Geary.EmailIdentifier> emails,
         Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove) {
         mark_email(emails, flags_to_add, flags_to_remove);
     }
@@ -2094,7 +2096,7 @@ public class GearyController : Geary.BaseObject {
     private void create_reply_forward_widget(ComposerWidget.ComposeType compose_type,
         Geary.Email? message) {
         string? quote;
-        Geary.Email? quote_message = main_window.conversation_viewer.get_selected_message(out quote);
+        Geary.Email? quote_message = main_window.conversation_viewer.get_selected_email(out quote);
         if (message == null)
             message = quote_message;
         if (quote_message != message)
@@ -2578,13 +2580,13 @@ public class GearyController : Geary.BaseObject {
         Libnotify.play_sound("message-sent-email");
     }
 
-    private void on_message_added(ConversationMessage message) {
+    private void on_email_row_added(ConversationEmail message) {
         message.link_activated.connect(on_link_activated);
         message.attachment_activated.connect(on_attachment_activated);
         message.edit_draft.connect(on_edit_draft);
     }
 
-    private void on_message_removed(ConversationMessage message) {
+    private void on_email_row_removed(ConversationEmail message) {
         message.link_activated.disconnect(on_link_activated);
         message.attachment_activated.disconnect(on_attachment_activated);
         message.edit_draft.disconnect(on_edit_draft);
diff --git a/src/client/conversation-viewer/conversation-email.vala 
b/src/client/conversation-viewer/conversation-email.vala
new file mode 100644
index 0000000..998ba66
--- /dev/null
+++ b/src/client/conversation-viewer/conversation-email.vala
@@ -0,0 +1,579 @@
+/*
+ * Copyright 2011-2015 Yorba Foundation
+ * Copyright 2016 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+/**
+ * A widget for displaying an email in a conversation.
+ *
+ * This widget corresponds to {@link Geary.Email}, displaying the
+ * email's primary message (a {@link Geary.RFC822.Message}), any
+ * sub-messages (also instances of {@link Geary.RFC822.Message}) and
+ * attachments. The RFC822 messages are themselves displayed by {@link
+ * ConversationMessage}.
+ */
+[GtkTemplate (ui = "/org/gnome/Geary/conversation-email.ui")]
+public class ConversationEmail : Gtk.Box {
+
+    private const int ATTACHMENT_ICON_SIZE = 32;
+    private const int ATTACHMENT_PREVIEW_SIZE = 64;
+
+
+    // The email message being displayed
+    public Geary.Email email { get; private set; }
+
+    // Is the message body shown or not?
+    public bool is_message_body_visible = false;
+
+    // Widget displaying the email's primary message
+    public ConversationMessage primary_message { get; private set; }
+
+    // Contacts for the email's account
+    private Geary.ContactStore contact_store;
+
+    // Messages that have been attached to this one
+    private Gee.List<ConversationMessage> conversation_messages =
+        new Gee.LinkedList<ConversationMessage>();
+
+    // Attachment ids that have been displayed inline
+    private Gee.HashSet<string> inlined_content_ids = new Gee.HashSet<string>();
+
+    [GtkChild]
+    private Gtk.Box action_box;
+
+    [GtkChild]
+    private Gtk.Image attachment_icon;
+
+    [GtkChild]
+    private Gtk.Button star_button;
+
+    [GtkChild]
+    private Gtk.Button unstar_button;
+
+    [GtkChild]
+    private Gtk.MenuButton email_menubutton;
+
+    [GtkChild]
+    private Gtk.Box sub_messages_box;
+
+    [GtkChild]
+    private Gtk.Box attachments_box;
+
+    [GtkChild]
+    private Gtk.ListStore attachments_model;
+
+    // Fired on link activation in the web_view
+    public signal void link_activated(string link);
+
+    // Fired on attachment activation
+    public signal void attachment_activated(Geary.Attachment attachment);
+
+    // Fired the edit draft button is clicked.
+    public signal void edit_draft(Geary.Email message);
+
+
+    public ConversationEmail(Geary.Email email,
+                             Geary.ContactStore contact_store,
+                             bool is_draft) {
+        this.email = email;
+        this.contact_store = contact_store;
+
+        Geary.RFC822.Message message;
+        try {
+            message = email.get_message();
+        } catch (Error error) {
+            debug("Error loading primary message: %s", error.message);
+            return;
+        }
+
+        primary_message = new ConversationMessage(
+            message,
+            contact_store,
+            email.load_remote_images().is_certain()
+        );
+        primary_message.flag_remote_images.connect(on_flag_remote_images);
+        primary_message.remember_remote_images.connect(on_remember_remote_images);
+        primary_message.attachment_displayed_inline.connect((id) => {
+                inlined_content_ids.add(id);
+            });
+        primary_message.web_view.link_selected.connect((link) => {
+                link_activated(link);
+            });
+        primary_message.draft_infobar.response.connect((infobar, response_id) => {
+                if (response_id == 1) { edit_draft(email); }
+            });
+        primary_message.summary_box.pack_start(action_box, false, false, 0);
+        if (is_draft) {
+            primary_message.draft_infobar.show();
+        }
+
+        email_menubutton.set_menu_model(build_message_menu(email));
+        email_menubutton.set_sensitive(false);
+
+        // if (email.from != null && email.from.contains_normalized(current_account_information.email)) {
+        //  // XXX set a RO property?
+        //  get_style_context().add_class("sent");
+        // }
+
+        // Add sub_messages container and message viewers if there are any
+        Gee.List<Geary.RFC822.Message> sub_messages = message.get_sub_messages();
+        if (sub_messages.size > 0) {
+            primary_message.body_box.pack_start(sub_messages_box, false, false, 0);
+        }
+        foreach (Geary.RFC822.Message sub_message in sub_messages) {
+            ConversationMessage conversation_message =
+                new ConversationMessage(sub_message, contact_store, false);
+            sub_messages_box.pack_start(conversation_message, false, false, 0);
+            this.conversation_messages.add(conversation_message);
+        }
+
+        pack_start(primary_message, true, true, 0);
+        update_email_state(false);
+    }
+
+    public async void start_loading(Cancellable load_cancelled) {
+        yield primary_message.load_message_body(load_cancelled);
+        foreach (ConversationMessage message in conversation_messages) {
+            yield message.load_message_body(load_cancelled);
+        }
+        yield load_attachments(load_cancelled);
+    }
+
+    public void expand_email(bool include_transitions=true) {
+        is_message_body_visible = true;
+        get_style_context().add_class("geary_show_body");
+        star_button.set_sensitive(true);
+        unstar_button.set_sensitive(true);
+        email_menubutton.set_sensitive(true);
+        primary_message.show_message_body(include_transitions);
+    }
+
+    public void collapse_email() {
+        is_message_body_visible = false;
+        get_style_context().remove_class("geary_show_body");
+        star_button.set_sensitive(false);
+        unstar_button.set_sensitive(false);
+        email_menubutton.set_sensitive(false);
+        primary_message.hide_message_body();
+    }
+
+    private MenuModel build_message_menu(Geary.Email email) {
+        Gtk.Builder builder = new Gtk.Builder.from_resource(
+            "/org/gnome/Geary/conversation-message-menu.ui"
+        );
+
+        MenuModel menu = (MenuModel) builder.get_object("conversation_message_menu");
+
+        // menu.selection_done.connect(on_message_menu_selection_done);
+
+        // int displayed = displayed_attachments(email);
+        // if (displayed > 0) {
+        //     string mnemonic = ngettext("Save A_ttachment...", "Save All A_ttachments...",
+        //         displayed);
+        //     Gtk.MenuItem save_all_item = new Gtk.MenuItem.with_mnemonic(mnemonic);
+        //     save_all_item.activate.connect(() => save_attachments(email.attachments));
+        //     menu.append(save_all_item);
+        //     menu.append(new Gtk.SeparatorMenuItem());
+        // }
+
+        // if (!in_drafts_folder()) {
+        //     // Reply to a message.
+        //     Gtk.MenuItem reply_item = new Gtk.MenuItem.with_mnemonic(_("_Reply"));
+        //     reply_item.activate.connect(() => reply_to_message(email));
+        //     menu.append(reply_item);
+
+        //     // Reply to all on a message.
+        //     Gtk.MenuItem reply_all_item = new Gtk.MenuItem.with_mnemonic(_("Reply to _All"));
+        //     reply_all_item.activate.connect(() => reply_all_message(email));
+        //     menu.append(reply_all_item);
+
+        //     // Forward a message.
+        //     Gtk.MenuItem forward_item = new Gtk.MenuItem.with_mnemonic(_("_Forward"));
+        //     forward_item.activate.connect(() => forward_message(email));
+        //     menu.append(forward_item);
+        // }
+
+        // if (menu.get_children().length() > 0) {
+        //     // Separator.
+        //     menu.append(new Gtk.SeparatorMenuItem());
+        // }
+
+        // // Mark as read/unread.
+        // if (email.is_unread().to_boolean(false)) {
+        //     Gtk.MenuItem mark_read_item = new Gtk.MenuItem.with_mnemonic(_("_Mark as Read"));
+        //     mark_read_item.activate.connect(() => on_mark_read_message(email));
+        //     menu.append(mark_read_item);
+        // } else {
+        //     Gtk.MenuItem mark_unread_item = new Gtk.MenuItem.with_mnemonic(_("_Mark as Unread"));
+        //     mark_unread_item.activate.connect(() => on_mark_unread_message(email));
+        //     menu.append(mark_unread_item);
+
+        //     if (messages.size > 1 && messages.last() != email) {
+        //         Gtk.MenuItem mark_unread_from_here_item = new Gtk.MenuItem.with_mnemonic(
+        //             _("Mark Unread From _Here"));
+        //         mark_unread_from_here_item.activate.connect(() => on_mark_unread_from_here(email));
+        //         menu.append(mark_unread_from_here_item);
+        //     }
+        // }
+
+        // // Print a message.
+        // Gtk.MenuItem print_item = new Gtk.MenuItem.with_mnemonic(Stock._PRINT_MENU);
+        // print_item.activate.connect(() => on_print_message(email));
+        // menu.append(print_item);
+
+        // // Separator.
+        // menu.append(new Gtk.SeparatorMenuItem());
+
+        // // View original message source.
+        // Gtk.MenuItem view_source_item = new Gtk.MenuItem.with_mnemonic(_("_View Source"));
+        // view_source_item.activate.connect(() => on_view_source(email));
+        // menu.append(view_source_item);
+
+        return menu;
+    }
+
+    public void update_flags(Geary.Email email) {
+        this.email.set_flags(email.email_flags);
+        update_email_state();
+    }
+
+    public bool is_manual_read() {
+        return get_style_context().has_class("geary_manual_read");
+    }
+
+    public void mark_manual_read() {
+        get_style_context().add_class("geary_manual_read");
+    }
+
+    private void update_email_state(bool include_transitions=true) {
+        Geary.EmailFlags flags = email.email_flags;
+        Gtk.StyleContext style = get_style_context();
+
+        if (flags.is_unread()) {
+            style.add_class("geary_unread");
+        } else {
+            style.remove_class("geary_unread");
+        }
+
+        if (flags.is_flagged()) {
+            style.add_class("geary_starred");
+            star_button.hide();
+            unstar_button.show();
+        } else {
+            style.remove_class("geary_starred");
+            star_button.show();
+            unstar_button.hide();
+        }
+
+        //if (email.email_flags.is_outbox_sent()) {
+        //  email_warning.set_inner_html(
+        //      _("This message was sent successfully, but could not be saved to %s.").printf(
+        //            Geary.SpecialFolderType.SENT.get_display_name()));
+    }
+
+    private void on_flag_remote_images(ConversationMessage view) {
+        // XXX check we aren't already auto loading the image
+        Geary.EmailFlags flags = new Geary.EmailFlags();
+        flags.add(Geary.EmailFlags.LOAD_REMOTE_IMAGES);
+        //get_viewer().mark_messages(Geary.iterate<Geary.EmailIdentifier>(email.id).to_array_list(),
+        //flags, null);
+    }
+
+
+    private void on_remember_remote_images(ConversationMessage view) {
+        Geary.RFC822.MailboxAddress? sender = view.message.get_primary_originator();
+        if (sender == null) {
+            debug("Couldn't find sender for message: %s", email.id.to_string());
+            return;
+        }
+
+        Geary.Contact? contact = contact_store.get_by_rfc822(
+            view.message.get_primary_originator()
+        );
+        if (contact == null) {
+            debug("Couldn't find contact for %s", sender.to_string());
+            return;
+        }
+
+        Geary.ContactFlags flags = new Geary.ContactFlags();
+        flags.add(Geary.ContactFlags.ALWAYS_LOAD_REMOTE_IMAGES);
+        Gee.ArrayList<Geary.Contact> contact_list = new Gee.ArrayList<Geary.Contact>();
+        contact_list.add(contact);
+        contact_store.mark_contacts_async.begin(contact_list, flags, null);
+    }
+
+    [GtkCallback]
+    private void on_attachments_view_activated(Gtk.IconView view, Gtk.TreePath path) {
+        Gtk.TreeIter iter;
+        Value attachment_id;
+
+        attachments_model.get_iter(out iter, path);
+        attachments_model.get_value(iter, 2, out attachment_id);
+
+        Geary.Attachment? attachment = null;
+        try {
+            attachment = email.get_attachment(attachment_id.get_string());
+        } catch (Error error) {
+            warning("Error getting attachment: %s", error.message);
+        }
+
+        if (attachment != null) {
+            attachment_activated(attachment);
+        }
+    }
+
+    // private void save_attachment(Geary.Attachment attachment) {
+    //     Gee.List<Geary.Attachment> attachments = new Gee.ArrayList<Geary.Attachment>();
+    //     attachments.add(attachment);
+    //     get_viewer().save_attachments(attachments);
+    // }
+
+    // private void on_mark_read_message(Geary.Email message) {
+    //     Geary.EmailFlags flags = new Geary.EmailFlags();
+    //     flags.add(Geary.EmailFlags.UNREAD);
+    //     get_viewer().mark_messages(Geary.iterate<Geary.EmailIdentifier>(message.id).to_array_list(), 
null, flags);
+    //     mark_manual_read(message.id);
+    // }
+
+    // private void on_mark_unread_message(Geary.Email message) {
+    //     Geary.EmailFlags flags = new Geary.EmailFlags();
+    //     flags.add(Geary.EmailFlags.UNREAD);
+    //     get_viewer().mark_messages(Geary.iterate<Geary.EmailIdentifier>(message.id).to_array_list(), 
flags, null);
+    //     mark_manual_read(message.id);
+    // }
+
+    // private void on_mark_unread_from_here(Geary.Email message) {
+    //     Geary.EmailFlags flags = new Geary.EmailFlags();
+    //     flags.add(Geary.EmailFlags.UNREAD);
+
+    //     Gee.Iterator<Geary.Email>? iter = messages.iterator_at(message);
+    //     if (iter == null) {
+    //         warning("Email not found in message list");
+
+    //         return;
+    //     }
+
+    //     // Build a list of IDs to mark.
+    //     Gee.ArrayList<Geary.EmailIdentifier> to_mark = new Gee.ArrayList<Geary.EmailIdentifier>();
+    //     to_mark.add(message.id);
+    //     while (iter.next())
+    //         to_mark.add(iter.get().id);
+
+    //     get_viewer().mark_messages(to_mark, flags, null);
+    //     foreach(Geary.EmailIdentifier id in to_mark)
+    //         mark_manual_read(id);
+    // }
+
+    // private void on_print_message(Geary.Email message) {
+    //     try {
+    //         email_to_element.get(message.id).get_class_list().add("print");
+    //         web_view.get_main_frame().print();
+    //         email_to_element.get(message.id).get_class_list().remove("print");
+    //     } catch (GLib.Error error) {
+    //         debug("Hiding elements for printing failed: %s", error.message);
+    //     }
+    // }
+
+    // private void flag_message() {
+    //     Geary.EmailFlags flags = new Geary.EmailFlags();
+    //     flags.add(Geary.EmailFlags.FLAGGED);
+    //     get_viewer().mark_messages(Geary.iterate<Geary.EmailIdentifier>(email.id).to_array_list(), flags, 
null);
+    // }
+
+    // private void unflag_message() {
+    //     Geary.EmailFlags flags = new Geary.EmailFlags();
+    //     flags.add(Geary.EmailFlags.FLAGGED);
+    //     get_viewer().mark_messages(Geary.iterate<Geary.EmailIdentifier>(email.id).to_array_list(), null, 
flags);
+    // }
+
+    // private void show_attachment_menu(Geary.Email email, Geary.Attachment attachment) {
+    //     attachment_menu = build_attachment_menu(email, attachment);
+    //     attachment_menu.show_all();
+    //     attachment_menu.popup(null, null, null, 0, Gtk.get_current_event_time());
+    // }
+
+    // private Gtk.Menu build_attachment_menu(Geary.Email email, Geary.Attachment attachment) {
+    //     Gtk.Menu menu = new Gtk.Menu();
+    //     menu.selection_done.connect(on_attachment_menu_selection_done);
+
+    //     Gtk.MenuItem save_attachment_item = new Gtk.MenuItem.with_mnemonic(_("_Save As..."));
+    //     save_attachment_item.activate.connect(() => save_attachment(attachment));
+    //     menu.append(save_attachment_item);
+
+    //     if (displayed_attachments(email) > 1) {
+    //         Gtk.MenuItem save_all_item = new Gtk.MenuItem.with_mnemonic(_("Save All A_ttachments..."));
+    //         save_all_item.activate.connect(() => save_attachments(email.attachments));
+    //         menu.append(save_all_item);
+    //     }
+
+    //     return menu;
+    // }
+
+    private async void load_attachments(Cancellable load_cancelled) {
+        Gee.List<Geary.Attachment> displayed_attachments =
+            new Gee.LinkedList<Geary.Attachment>();
+
+        // Do we have any attachments to display?
+        foreach (Geary.Attachment attachment in email.attachments) {
+            if (!(attachment.content_id in inlined_content_ids) &&
+                attachment.content_disposition.disposition_type ==
+                    Geary.Mime.DispositionType.ATTACHMENT) {
+                displayed_attachments.add(attachment);
+            }
+        }
+
+        if (displayed_attachments.is_empty) {
+            return;
+        }
+
+        // Show attachments container. Would like to do this in the
+        // ctor but we don't know at that point if any attachments
+        // will be displayed inline
+        attachment_icon.set_visible(true);
+        primary_message.body_box.pack_start(attachments_box, false, false, 0);
+
+        // Add each displayed attachment to the icon view
+        foreach (Geary.Attachment attachment in displayed_attachments) {
+            Gdk.Pixbuf? icon =
+                yield load_attachment_icon(attachment, load_cancelled);
+            string file_name = null;
+            if (attachment.has_supplied_filename) {
+                file_name = attachment.file.get_basename();
+            }
+            // XXX Geary.ImapDb.Attachment will use "none" when
+            // saving attachments with no filename to disk, this
+            // seems to be getting saved to be the filename and
+            // passed back, breaking the has_supplied_filename
+            // test - so check for it here.
+            if (file_name == null ||
+                file_name == "" ||
+                file_name == "none") {
+                // XXX Check for unknown types here and try to guess
+                // using attachment data.
+                file_name = ContentType.get_description(
+                    attachment.content_type.get_mime_type()
+                );
+            }
+            string file_size = Files.get_filesize_as_string(attachment.filesize);
+
+            Gtk.TreeIter iter;
+            attachments_model.append(out iter);
+            attachments_model.set(
+                iter,
+                0, icon,
+                1, Markup.printf_escaped("%s\n%s", file_name, file_size),
+                2, attachment.id,
+                -1
+            );
+        }
+    }
+
+    private async Gdk.Pixbuf? load_attachment_icon(Geary.Attachment attachment,
+                                                   Cancellable load_cancelled) {
+        Geary.Mime.ContentType content_type = attachment.content_type;
+        Gdk.Pixbuf? pixbuf = null;
+
+        // Due to Bug 65167, for retina/highdpi displays with
+        // window_scale == 2, GtkCellRendererPixbuf will draw the
+        // pixbuf twice as large and blurry, so clamp it to 1 for now
+        // - this at least gives is the correct size icons, but still
+        // blurry.
+        //int window_scale = get_window().get_scale_factor();
+        int window_scale = 1;
+        try {
+            // If the file is an image, use it. Otherwise get the icon
+            // for this mime_type.
+            if (content_type.has_media_type("image")) {
+                // Get a thumbnail for the image.
+                // TODO Generate and save the thumbnail when
+                // extracting the attachments rather than when showing
+                // them in the viewer.
+                int preview_size = ATTACHMENT_PREVIEW_SIZE * window_scale;
+                InputStream stream = yield attachment.file.read_async(
+                    Priority.DEFAULT,
+                    load_cancelled
+                );
+                pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async(
+                    stream, preview_size, preview_size, true, load_cancelled
+                );
+                pixbuf = pixbuf.apply_embedded_orientation();
+            } else {
+                // Load the icon for this mime type.
+                string gio_content_type =
+                   ContentType.from_mime_type(content_type.get_mime_type());
+                Icon icon = ContentType.get_icon(gio_content_type);
+                Gtk.IconTheme theme = Gtk.IconTheme.get_default();
+
+                // XXX GTK 3.14 We should be able to replace the
+                // ThemedIcon/LoadableIcon/other cases below with
+                // simply this:
+                // Gtk.IconInfo? icon_info = theme.lookup_by_gicon_for_scale(
+                //     icon, ATTACHMENT_ICON_SIZE, window_scale
+                // );
+                // pixbuf = yield icon_info.load_icon_async(load_cancelled);
+
+                if (icon is ThemedIcon) {
+                    Gtk.IconInfo? icon_info = null;
+                    foreach (string name in ((ThemedIcon) icon).names) {
+                        icon_info = theme.lookup_icon_for_scale(
+                            name, ATTACHMENT_ICON_SIZE, window_scale, 0
+                        );
+                        if (icon_info != null) {
+                            break;
+                        }
+                    }
+                    if (icon_info == null) {
+                        icon_info = theme.lookup_icon_for_scale(
+                            "x-office-document", ATTACHMENT_ICON_SIZE, window_scale, 0
+                        );
+                    }
+                    pixbuf = yield icon_info.load_icon_async(load_cancelled);
+                } else if (icon is LoadableIcon) {
+                    InputStream stream = yield ((LoadableIcon) icon).load_async(
+                        ATTACHMENT_ICON_SIZE, load_cancelled
+                    );
+                    int icon_size = ATTACHMENT_ICON_SIZE * window_scale;
+                    pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async(
+                        stream, icon_size, icon_size, true, load_cancelled
+                    );
+                } else {
+                    warning("Unsupported attachment icon type: %s\n",
+                            icon.get_type().name());
+                }
+            }
+        } catch (Error error) {
+            warning("Failed to load icon for attachment '%s': %s",
+                    attachment.id,
+                    error.message);
+        }
+
+        return pixbuf;
+    }
+
+    // private void on_view_source(Geary.Email message) {
+    //     string source = message.header.buffer.to_string() + message.body.buffer.to_string();
+
+    //     try {
+    //         string temporary_filename;
+    //         int temporary_handle = FileUtils.open_tmp("geary-message-XXXXXX.txt",
+    //             out temporary_filename);
+    //         FileUtils.set_contents(temporary_filename, source);
+    //         FileUtils.close(temporary_handle);
+
+    //         // ensure this file is only readable by the user ... this needs to be done after the
+    //         // file is closed
+    //         FileUtils.chmod(temporary_filename, (int) (Posix.S_IRUSR | Posix.S_IWUSR));
+
+    //         string temporary_uri = Filename.to_uri(temporary_filename, null);
+    //         Gtk.show_uri(web_view.get_screen(), temporary_uri, Gdk.CURRENT_TIME);
+    //     } catch (Error error) {
+    //         ErrorDialog dialog = new ErrorDialog(GearyApplication.instance.controller.main_window,
+    //             _("Failed to open default text editor."), error.message);
+    //         dialog.run();
+    //     }
+    // }
+
+}
diff --git a/src/client/conversation-viewer/conversation-message.vala 
b/src/client/conversation-viewer/conversation-message.vala
index f3ac510..b87a0ea 100644
--- a/src/client/conversation-viewer/conversation-message.vala
+++ b/src/client/conversation-viewer/conversation-message.vala
@@ -7,9 +7,13 @@
  */
 
 /**
- * A widget for displaying a message in a conversation.
+ * A widget for displaying an {@link Geary.RFC822.Message}.
+ *
+ * This widget corresponds to {@link Geary.RFC822.Message}, displaying
+ * both the message's headers and body. Any attachments and sub
+ * messages are handled by {@link ConversationEmail}, which typically
+ * embeds at least one instance of this class.
  */
-
 [GtkTemplate (ui = "/org/gnome/Geary/conversation-message.ui")]
 public class ConversationMessage : Gtk.Box {
     
@@ -38,16 +42,11 @@ public class ConversationMessage : Gtk.Box {
         "image/x-xbitmap",
         "image/x-xbm"
     };
-    private const int ATTACHMENT_ICON_SIZE = 32;
-    private const int ATTACHMENT_PREVIEW_SIZE = 64;
     private const string REPLACED_IMAGE_CLASS = "replaced_inline_image";
     private const string DATA_IMAGE_CLASS = "data_inline_image";
     private const int MAX_INLINE_IMAGE_MAJOR_DIM = 1024;
     private const int QUOTE_SIZE_THRESHOLD = 120;
-    
 
-    // The email message being displayed
-    public Geary.Email email { get; private set; }
 
     // The message being displayed
     public Geary.RFC822.Message message { get; private set; }
@@ -58,13 +57,16 @@ public class ConversationMessage : Gtk.Box {
     // The allocation for the web view
     public Gdk.Rectangle web_view_allocation { get; private set; }
 
-    // Is the message body shown or not?
-    public bool is_message_body_visible = false;
-
     // Has the message body been been fully loaded?
     public bool is_loading_complete = false;
 
     [GtkChild]
+    public Gtk.Box summary_box; // not yet supported: { get; private set; }
+
+    [GtkChild]
+    public Gtk.InfoBar draft_infobar; // not yet supported: { get; private set; }
+
+    [GtkChild]
     private Gtk.Image avatar_image;
 
     [GtkChild]
@@ -90,25 +92,11 @@ public class ConversationMessage : Gtk.Box {
     private Gtk.Box date_header;
 
     [GtkChild]
-    private Gtk.Image attachment_icon;
-    [GtkChild]
-    private Gtk.Button star_button;
-    [GtkChild]
-    private Gtk.Button unstar_button;
-    [GtkChild]
-    private Gtk.MenuButton message_menubutton;
-
-    [GtkChild]
     private Gtk.Revealer body_revealer;
     [GtkChild]
     public Gtk.Box body_box;
 
     [GtkChild]
-    private Gtk.Box attachments_box;
-    [GtkChild]
-    private Gtk.ListStore attachments_model;
-
-    [GtkChild]
     private Gtk.Popover link_popover;
     [GtkChild]
     private Gtk.Label good_link_label;
@@ -118,43 +106,35 @@ public class ConversationMessage : Gtk.Box {
     [GtkChild]
     private Gtk.InfoBar remote_images_infobar;
 
-    [GtkChild]
-    private Gtk.InfoBar draft_infobar;
-
     // The contacts for the message's account
     private Geary.ContactStore contact_store;
 
+    // Should any remote messages be always loaded and displayed?
+    private bool always_load_remote_images;
+
     // Contains the current mouse-over'ed link URL, if any
     private string? hover_url = null;
 
-    private Gee.HashSet<string> inlined_content_ids = new Gee.HashSet<string>();
     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>();
 
+    // Fired when an attachment is displayed inline
+    public signal void attachment_displayed_inline(string id);
 
-    // Fired on link activation in the web_view
-    public signal void link_activated(string link);
-
-    // Fired on attachment activation
-    public signal void attachment_activated(Geary.Attachment attachment);
+    // Fired when remote image load requested for sender
+    public signal void flag_remote_images();
 
-    // Fired the edit draft button is clicked.
-    public signal void edit_draft(Geary.Email message);
+    // Fired when remote image load requested for sender
+    public signal void remember_remote_images();
 
 
-    public ConversationMessage(Geary.Email email,
+    public ConversationMessage(Geary.RFC822.Message message,
                                Geary.ContactStore contact_store,
-                               bool is_draft) {
-        this.email = email;
+                               bool always_load_remote_images) {
+        this.message = message;
         this.contact_store = contact_store;
-
-        try {
-            message = email.get_message();
-        } catch (Error error) {
-            debug("Error loading  message: %s", error.message);
-            return;
-        }
+        this.always_load_remote_images = always_load_remote_images;
 
         // Preview headers
 
@@ -188,9 +168,6 @@ public class ConversationMessage : Gtk.Box {
             );
         }
 
-        message_menubutton.set_menu_model(build_message_menu(email));
-        message_menubutton.set_sensitive(false);
-
         web_view = new ConversationWebView();
         web_view.show();
         // web_view.context_menu.connect(() => { return true; }); // Suppress default context menu.
@@ -200,54 +177,12 @@ public class ConversationMessage : Gtk.Box {
                 web_view_allocation = allocation;
             });
         web_view.hovering_over_link.connect(on_hovering_over_link);
-        web_view.link_selected.connect((link) => { link_activated(link); });
 
         body_box.set_has_tooltip(true); // Used to show link URLs
         body_box.pack_start(web_view, true, true, 0);
-
-        // if (email.from != null && email.from.contains_normalized(current_account_information.email)) {
-        //  // XXX set a RO property?
-        //  get_style_context().add_class("sent");
-        // }
-
-        // Add the attachments container if there are displayed attachments.
-        int displayed = displayed_attachments(email);
-        if (displayed > 0) {
-            attachment_icon.set_visible(true);
-            body_box.pack_start(attachments_box, false, false, 0);
-        }
-
-        // // Look for any attached emails
-        // Gee.List<Geary.RFC822.Message> sub_messages = message.get_sub_messages();
-        // foreach (Geary.RFC822.Message sub_message in sub_messages) {
-        //     bool sub_remote_images = false;
-        //     try {
-        //         extra_part = set_message_html(
-        //             sub_message, part_div, out sub_remote_images
-        //         );
-        //         extra_part.get_class_list().add("read");
-        //         extra_part.get_class_list().add("hide");
-        //         remote_images = remote_images || sub_remote_images;
-        //     } catch (Error error) {
-        //         debug("Error adding attached message: %s", error.message);
-        //     }
-        // }
-
-        if (is_draft) {
-            draft_infobar.show();
-        }
-
-        update_message_state(false);
-    }
-
-    public async void start_loading(Cancellable load_cancelled) {
-        yield load_message_body(load_cancelled);
-        yield load_attachments(email.attachments, load_cancelled);
     }
 
     public void show_message_body(bool include_transitions=true) {
-        is_message_body_visible = true;
-        get_style_context().add_class("geary_show_body");
         avatar_image.set_pixel_size(32); // XXX constant
 
         Gtk.RevealerTransitionType revealer = preview_revealer.get_transition_type();
@@ -264,10 +199,6 @@ public class ConversationMessage : Gtk.Box {
         header_revealer.set_reveal_child(true);
         header_revealer.set_transition_type(revealer);
 
-        star_button.set_sensitive(true);
-        unstar_button.set_sensitive(true);
-        message_menubutton.set_sensitive(true);
-
         revealer = body_revealer.get_transition_type();
         if (!include_transitions) {
             body_revealer.set_transition_type(Gtk.RevealerTransitionType.NONE);
@@ -277,152 +208,14 @@ public class ConversationMessage : Gtk.Box {
     }
 
     public void hide_message_body() {
-        is_message_body_visible = false;
-        get_style_context().remove_class("geary_show_body");
         avatar_image.set_pixel_size(24); // XXX constant
         preview_revealer.set_reveal_child(true);
         header_revealer.set_reveal_child(false);
-        star_button.set_sensitive(false);
-        unstar_button.set_sensitive(false);
-        message_menubutton.set_sensitive(false);
         body_revealer.set_reveal_child(false);
     }
 
-    // Appends email address fields to the header.
-    private string format_addresses(Geary.RFC822.MailboxAddresses? addresses) {
-        int i = 0;
-        string value = "";
-        Gee.List<Geary.RFC822.MailboxAddress> list = addresses.get_all();
-        foreach (Geary.RFC822.MailboxAddress a in list) {
-            value += a.to_string();
-            
-            if (++i < list.size)
-                value += ", ";
-        }
-
-        return value;
-    }
-
-    private static void set_header_text(Gtk.Box header, string text) {
-        ((Gtk.Label) header.get_children().nth(1).data).set_text(text);
-        header.set_visible(true);
-    }
-
-    private MenuModel build_message_menu(Geary.Email email) {
-        Gtk.Builder builder = new Gtk.Builder.from_resource(
-            "/org/gnome/Geary/conversation-message-menu.ui"
-        );
-
-        MenuModel menu = (MenuModel) builder.get_object("conversation_message_menu");
-
-        // menu.selection_done.connect(on_message_menu_selection_done);
-        
-        // int displayed = displayed_attachments(email);
-        // if (displayed > 0) {
-        //     string mnemonic = ngettext("Save A_ttachment...", "Save All A_ttachments...",
-        //         displayed);
-        //     Gtk.MenuItem save_all_item = new Gtk.MenuItem.with_mnemonic(mnemonic);
-        //     save_all_item.activate.connect(() => save_attachments(email.attachments));
-        //     menu.append(save_all_item);
-        //     menu.append(new Gtk.SeparatorMenuItem());
-        // }
-        
-        // if (!in_drafts_folder()) {
-        //     // Reply to a message.
-        //     Gtk.MenuItem reply_item = new Gtk.MenuItem.with_mnemonic(_("_Reply"));
-        //     reply_item.activate.connect(() => reply_to_message(email));
-        //     menu.append(reply_item);
-
-        //     // Reply to all on a message.
-        //     Gtk.MenuItem reply_all_item = new Gtk.MenuItem.with_mnemonic(_("Reply to _All"));
-        //     reply_all_item.activate.connect(() => reply_all_message(email));
-        //     menu.append(reply_all_item);
-
-        //     // Forward a message.
-        //     Gtk.MenuItem forward_item = new Gtk.MenuItem.with_mnemonic(_("_Forward"));
-        //     forward_item.activate.connect(() => forward_message(email));
-        //     menu.append(forward_item);
-        // }
-        
-        // if (menu.get_children().length() > 0) {
-        //     // Separator.
-        //     menu.append(new Gtk.SeparatorMenuItem());
-        // }
-        
-        // // Mark as read/unread.
-        // if (email.is_unread().to_boolean(false)) {
-        //     Gtk.MenuItem mark_read_item = new Gtk.MenuItem.with_mnemonic(_("_Mark as Read"));
-        //     mark_read_item.activate.connect(() => on_mark_read_message(email));
-        //     menu.append(mark_read_item);
-        // } else {
-        //     Gtk.MenuItem mark_unread_item = new Gtk.MenuItem.with_mnemonic(_("_Mark as Unread"));
-        //     mark_unread_item.activate.connect(() => on_mark_unread_message(email));
-        //     menu.append(mark_unread_item);
-            
-        //     if (messages.size > 1 && messages.last() != email) {
-        //         Gtk.MenuItem mark_unread_from_here_item = new Gtk.MenuItem.with_mnemonic(
-        //             _("Mark Unread From _Here"));
-        //         mark_unread_from_here_item.activate.connect(() => on_mark_unread_from_here(email));
-        //         menu.append(mark_unread_from_here_item);
-        //     }
-        // }
-        
-        // // Print a message.
-        // Gtk.MenuItem print_item = new Gtk.MenuItem.with_mnemonic(Stock._PRINT_MENU);
-        // print_item.activate.connect(() => on_print_message(email));
-        // menu.append(print_item);
-
-        // // Separator.
-        // menu.append(new Gtk.SeparatorMenuItem());
-
-        // // View original message source.
-        // Gtk.MenuItem view_source_item = new Gtk.MenuItem.with_mnemonic(_("_View Source"));
-        // view_source_item.activate.connect(() => on_view_source(email));
-        // menu.append(view_source_item);
-
-        return menu;
-    }
-
-    public void update_flags(Geary.Email email) {
-        this.email.set_flags(email.email_flags);
-        update_message_state();
-    }
-
-    public bool is_manual_read() {
-        return get_style_context().has_class("geary_manual_read");
-    }
-
-    public void mark_manual_read() {
-        get_style_context().add_class("geary_manual_read");
-    }
-
-    private void update_message_state(bool include_transitions=true) {
-        Geary.EmailFlags flags = email.email_flags;
-        Gtk.StyleContext style = get_style_context();
-        
-        if (flags.is_unread()) {
-            style.add_class("geary_unread");
-        } else {
-            style.remove_class("geary_unread");
-        }
-        
-        if (flags.is_flagged()) {
-            style.add_class("geary_starred");
-            star_button.hide();
-            unstar_button.show();
-        } else {
-            style.remove_class("geary_starred");
-            star_button.show();
-            unstar_button.hide();
-        }
-        
-        //if (email.email_flags.is_outbox_sent()) {
-        //  email_warning.set_inner_html(
-        //      _("This message was sent successfully, but could not be saved to %s.").printf(
-        //            Geary.SpecialFolderType.SENT.get_display_name()));
-    }
-
-    private async void load_message_body(Cancellable load_cancelled) {
+    public async void load_message_body(Cancellable load_cancelled) {
+        bool remote_images = false;
         bool load_images = false;
         string? body_text = null;
         try {
@@ -438,9 +231,9 @@ public class ConversationMessage : Gtk.Box {
         body_text = clean_html_markup(body_text ?? "", message, out load_images);
         if (load_images) {
             Geary.Contact contact =
-                contact_store.get_by_rfc822(email.get_primary_originator());
-            bool always_load = contact != null && contact.always_load_remote_images();
-            if (always_load || email.load_remote_images().is_certain()) {
+                contact_store.get_by_rfc822(message.get_primary_originator());
+            bool contact_load = contact != null && contact.always_load_remote_images();
+            if (contact_load || always_load_remote_images) {
                 load_images = true;
             } else {
                 remote_images_infobar.show();
@@ -482,6 +275,26 @@ public class ConversationMessage : Gtk.Box {
         web_view.load_string(body_text, "text/html", "UTF8", "");
     }
 
+    // Appends email address fields to the header.
+    private string format_addresses(Geary.RFC822.MailboxAddresses? addresses) {
+        int i = 0;
+        string value = "";
+        Gee.List<Geary.RFC822.MailboxAddress> list = addresses.get_all();
+        foreach (Geary.RFC822.MailboxAddress a in list) {
+            value += a.to_string();
+
+            if (++i < list.size)
+                value += ", ";
+        }
+
+        return value;
+    }
+
+    private static void set_header_text(Gtk.Box header, string text) {
+        ((Gtk.Label) header.get_children().nth(1).data).set_text(text);
+        header.set_visible(true);
+    }
+
     // This delegate is called from within Geary.RFC822.Message.get_body while assembling the plain
     // or HTML document when a non-text MIME part is encountered within a multipart/mixed container.
     // If this returns null, the MIME part is dropped from the final returned document; otherwise,
@@ -634,48 +447,10 @@ public class ConversationMessage : Gtk.Box {
     //         inspect_item.activate.connect(() => {web_view.web_inspector.inspect_node(clicked_element);});
     //         menu.append(inspect_item);
     //     }
-        
-    //     return menu;
-    // }
-
-    // private void on_unstar_clicked() {
-    //  unflag_message();
-    // }
-
-    // private void on_star_clicked() {
-    //  flag_message();
-    // }
-
-    // private bool is_hidden() {
-    //  // XXX
-    //  return false;
-    // }
 
-    // private void on_toggle_hidden() {
-    //  // XXX
-    //     get_viewer().mark_read();
+    //     return menu;
     // }
 
-    [GtkCallback]
-    private void on_attachments_view_activated(Gtk.IconView view, Gtk.TreePath path) {
-        Gtk.TreeIter iter;
-        Value attachment_id;
-
-        attachments_model.get_iter(out iter, path);
-        attachments_model.get_value(iter, 2, out attachment_id);
-
-        Geary.Attachment? attachment = null;
-        try {
-            attachment = email.get_attachment(attachment_id.get_string());
-        } catch (Error error) {
-            warning("Error getting attachment: %s", error.message);
-        }
-
-        if (attachment != null) {
-            attachment_activated(attachment);
-        }
-    }
-
     // private void on_data_image_menu(WebKit.DOM.Element element, WebKit.DOM.Event event) {
     //     event.stop_propagation();
         
@@ -697,97 +472,10 @@ public class ConversationMessage : Gtk.Box {
     //         save_buffer_to_file(replaced_image.filename, replaced_image.buffer);
     //     });
     //     image_menu.append(save_image_item);
-        
-    //     image_menu.show_all();
-        
-    //     image_menu.popup(null, null, null, 0, Gtk.get_current_event_time());
-    // }
-    
-    // private void save_attachment(Geary.Attachment attachment) {
-    //     Gee.List<Geary.Attachment> attachments = new Gee.ArrayList<Geary.Attachment>();
-    //     attachments.add(attachment);
-    //     get_viewer().save_attachments(attachments);
-    // }
-    
-    // private void on_mark_read_message(Geary.Email message) {
-    //     Geary.EmailFlags flags = new Geary.EmailFlags();
-    //     flags.add(Geary.EmailFlags.UNREAD);
-    //     get_viewer().mark_messages(Geary.iterate<Geary.EmailIdentifier>(message.id).to_array_list(), 
null, flags);
-    //     mark_manual_read(message.id);
-    // }
 
-    // private void on_mark_unread_message(Geary.Email message) {
-    //     Geary.EmailFlags flags = new Geary.EmailFlags();
-    //     flags.add(Geary.EmailFlags.UNREAD);
-    //     get_viewer().mark_messages(Geary.iterate<Geary.EmailIdentifier>(message.id).to_array_list(), 
flags, null);
-    //     mark_manual_read(message.id);
-    // }
-    
-    // private void on_mark_unread_from_here(Geary.Email message) {
-    //     Geary.EmailFlags flags = new Geary.EmailFlags();
-    //     flags.add(Geary.EmailFlags.UNREAD);
-        
-    //     Gee.Iterator<Geary.Email>? iter = messages.iterator_at(message);
-    //     if (iter == null) {
-    //         warning("Email not found in message list");
-            
-    //         return;
-    //     }
-        
-    //     // Build a list of IDs to mark.
-    //     Gee.ArrayList<Geary.EmailIdentifier> to_mark = new Gee.ArrayList<Geary.EmailIdentifier>();
-    //     to_mark.add(message.id);
-    //     while (iter.next())
-    //         to_mark.add(iter.get().id);
-        
-    //     get_viewer().mark_messages(to_mark, flags, null);
-    //     foreach(Geary.EmailIdentifier id in to_mark)
-    //         mark_manual_read(id);
-    // }
-    
-    // private void on_print_message(Geary.Email message) {
-    //     try {
-    //         email_to_element.get(message.id).get_class_list().add("print");
-    //         web_view.get_main_frame().print();
-    //         email_to_element.get(message.id).get_class_list().remove("print");
-    //     } catch (GLib.Error error) {
-    //         debug("Hiding elements for printing failed: %s", error.message);
-    //     }
-    // }
-    
-    // private void flag_message() {
-    //     Geary.EmailFlags flags = new Geary.EmailFlags();
-    //     flags.add(Geary.EmailFlags.FLAGGED);
-    //     get_viewer().mark_messages(Geary.iterate<Geary.EmailIdentifier>(email.id).to_array_list(), flags, 
null);
-    // }
-
-    // private void unflag_message() {
-    //     Geary.EmailFlags flags = new Geary.EmailFlags();
-    //     flags.add(Geary.EmailFlags.FLAGGED);
-    //     get_viewer().mark_messages(Geary.iterate<Geary.EmailIdentifier>(email.id).to_array_list(), null, 
flags);
-    // }
+    //     image_menu.show_all();
 
-    // private void show_attachment_menu(Geary.Email email, Geary.Attachment attachment) {
-    //     attachment_menu = build_attachment_menu(email, attachment);
-    //     attachment_menu.show_all();
-    //     attachment_menu.popup(null, null, null, 0, Gtk.get_current_event_time());
-    // }
-    
-    // private Gtk.Menu build_attachment_menu(Geary.Email email, Geary.Attachment attachment) {
-    //     Gtk.Menu menu = new Gtk.Menu();
-    //     menu.selection_done.connect(on_attachment_menu_selection_done);
-        
-    //     Gtk.MenuItem save_attachment_item = new Gtk.MenuItem.with_mnemonic(_("_Save As..."));
-    //     save_attachment_item.activate.connect(() => save_attachment(attachment));
-    //     menu.append(save_attachment_item);
-        
-    //     if (displayed_attachments(email) > 1) {
-    //         Gtk.MenuItem save_all_item = new Gtk.MenuItem.with_mnemonic(_("Save All A_ttachments..."));
-    //         save_all_item.activate.connect(() => save_attachments(email.attachments));
-    //         menu.append(save_all_item);
-    //     }
-        
-    //     return menu;
+    //     image_menu.popup(null, null, null, 0, Gtk.get_current_event_time());
     // }
 
     private string clean_html_markup(string text, Geary.RFC822.Message message, out bool remote_images) {
@@ -842,6 +530,7 @@ public class ConversationMessage : Gtk.Box {
             // Then look for all <img> tags. Inline images are replaced with
             // data URLs.
             WebKit.DOM.NodeList inline_list = container.query_selector_all("img");
+            Gee.HashSet<string> inlined_content_ids = new Gee.HashSet<string>();
             for (ulong i = 0; i < inline_list.length; ++i) {
                 // Get the MIME content for the image.
                 WebKit.DOM.HTMLImageElement img = (WebKit.DOM.HTMLImageElement) inline_list.item(i);
@@ -886,6 +575,7 @@ public class ConversationMessage : Gtk.Box {
                     // stash here so inlined image isn't listed as attachment (esp. if it has no
                     // Content-Disposition)
                     inlined_content_ids.add(content_id);
+                    attachment_displayed_inline(content_id);
                 } else {
                     // replaced by data: URI, remove this tag and let the inserted one shine through
                     img.parent_element.remove_child(img);
@@ -1003,100 +693,7 @@ public class ConversationMessage : Gtk.Box {
         }
 
         if (remember) {
-            // only add flag to load remote images if not already present
-            if (email != null && !email.load_remote_images().is_certain()) {
-                Geary.EmailFlags flags = new Geary.EmailFlags();
-                flags.add(Geary.EmailFlags.LOAD_REMOTE_IMAGES);
-                // XXX reenable this
-                //get_viewer().mark_messages(Geary.iterate<Geary.EmailIdentifier>(email.id).to_array_list(), 
flags, null);
-            }
-        }
-    }
-
-    private void always_show_images() {
-        Geary.Contact? contact = contact_store.get_by_rfc822(
-            email.get_primary_originator()
-        );
-        if (contact == null) {
-            debug("Couldn't find contact for %s", email.from.to_string());
-            return;
-        }
-        
-        Geary.ContactFlags flags = new Geary.ContactFlags();
-        flags.add(Geary.ContactFlags.ALWAYS_LOAD_REMOTE_IMAGES);
-        Gee.ArrayList<Geary.Contact> contact_list = new Gee.ArrayList<Geary.Contact>();
-        contact_list.add(contact);
-        contact_store.mark_contacts_async.begin(contact_list, flags, null);
-        
-        show_images(false);
-
-        // XXX notify something to go load images for the rest of the
-        // messages in he current convo for this sender
-    }
-    
-    private int displayed_attachments(Geary.Email email) {
-        int ret = 0;
-        foreach (Geary.Attachment attachment in email.attachments) {
-            if (should_show_attachment(attachment)) {
-                ret++;
-            }
-        }
-        return ret;
-    }
-    
-    private bool should_show_attachment(Geary.Attachment attachment) {
-        // if displayed inline, don't include in attachment list
-        if (attachment.content_id in inlined_content_ids)
-            return false;
-        
-        switch (attachment.content_disposition.disposition_type) {
-            case Geary.Mime.DispositionType.ATTACHMENT:
-                return true;
-            
-            case Geary.Mime.DispositionType.INLINE:
-                return !is_content_type_supported_inline(attachment.content_type);
-            
-            default:
-                assert_not_reached();
-        }
-    }
-
-    private async void load_attachments(Gee.List<Geary.Attachment> attachments,
-                                        Cancellable load_cancelled) {
-        foreach (Geary.Attachment attachment in attachments) {
-            if (should_show_attachment(attachment)) {
-                Gdk.Pixbuf? icon =
-                    yield load_attachment_icon(attachment, load_cancelled);
-                string file_name = null;
-                if (attachment.has_supplied_filename) {
-                    file_name = attachment.file.get_basename();
-                }
-                // XXX Geary.ImapDb.Attachment will use "none" when
-                // saving attachments with no filename to disk, this
-                // seems to be getting saved to be the filename and
-                // passed back, breaking the has_supplied_filename
-                // test - so check for it here.
-                if (file_name == null ||
-                    file_name == "" ||
-                    file_name == "none") {
-                    // XXX Check for unknown types here and try to guess
-                    // using attachment data.
-                    file_name = ContentType.get_description(
-                        attachment.content_type.get_mime_type()
-                    );
-                }
-                string file_size = Files.get_filesize_as_string(attachment.filesize);
-
-                Gtk.TreeIter iter;
-                attachments_model.append(out iter);
-                attachments_model.set(
-                    iter,
-                    0, icon,
-                    1, Markup.printf_escaped("%s\n%s", file_name, file_size),
-                    2, attachment.id,
-                    -1
-                );
-            }
+            flag_remote_images();
         }
     }
 
@@ -1113,88 +710,6 @@ public class ConversationMessage : Gtk.Box {
         return false;
     }
 
-    private async Gdk.Pixbuf? load_attachment_icon(Geary.Attachment attachment,
-                                                   Cancellable load_cancelled) {
-        Geary.Mime.ContentType content_type = attachment.content_type;
-        Gdk.Pixbuf? pixbuf = null;
-
-        // Due to Bug 65167, for retina/highdpi displays with
-        // window_scale == 2, GtkCellRendererPixbuf will draw the
-        // pixbuf twice as large and blurry, so clamp it to 1 for now
-        // - this at least gives is the correct size icons, but still
-        // blurry.
-        //int window_scale = get_window().get_scale_factor();
-        int window_scale = 1;
-        try {
-            // If the file is an image, use it. Otherwise get the icon
-            // for this mime_type.
-            if (content_type.has_media_type("image")) {
-                // Get a thumbnail for the image.
-                // TODO Generate and save the thumbnail when
-                // extracting the attachments rather than when showing
-                // them in the viewer.
-                int preview_size = ATTACHMENT_PREVIEW_SIZE * window_scale;
-                InputStream stream = yield attachment.file.read_async(
-                    Priority.DEFAULT,
-                    load_cancelled
-                );
-                pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async(
-                    stream, preview_size, preview_size, true, load_cancelled
-                );
-                pixbuf = pixbuf.apply_embedded_orientation();
-            } else {
-                // Load the icon for this mime type.
-                string gio_content_type =
-                   ContentType.from_mime_type(content_type.get_mime_type());
-                Icon icon = ContentType.get_icon(gio_content_type);
-                Gtk.IconTheme theme = Gtk.IconTheme.get_default();
-
-                // XXX GTK 3.14 We should be able to replace the
-                // ThemedIcon/LoadableIcon/other cases below with
-                // simply this:
-                // Gtk.IconInfo? icon_info = theme.lookup_by_gicon_for_scale(
-                //     icon, ATTACHMENT_ICON_SIZE, window_scale
-                // );
-                // pixbuf = yield icon_info.load_icon_async(load_cancelled);
-
-                if (icon is ThemedIcon) {
-                    Gtk.IconInfo? icon_info = null;
-                    foreach (string name in ((ThemedIcon) icon).names) {
-                        icon_info = theme.lookup_icon_for_scale(
-                            name, ATTACHMENT_ICON_SIZE, window_scale, 0
-                        );
-                        if (icon_info != null) {
-                            break;
-                        }
-                    }
-                    if (icon_info == null) {
-                        icon_info = theme.lookup_icon_for_scale(
-                            "x-office-document", ATTACHMENT_ICON_SIZE, window_scale, 0
-                        );
-                    }
-                    pixbuf = yield icon_info.load_icon_async(load_cancelled);
-                } else if (icon is LoadableIcon) {
-                    InputStream stream = yield ((LoadableIcon) icon).load_async(
-                        ATTACHMENT_ICON_SIZE, load_cancelled
-                    );
-                    int icon_size = ATTACHMENT_ICON_SIZE * window_scale;
-                    pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async(
-                        stream, icon_size, icon_size, true, load_cancelled
-                    );
-                } else {
-                    warning("Unsupported attachment icon type: %s\n",
-                            icon.get_type().name());
-                }
-            }
-        } catch (Error error) {
-            warning("Failed to load icon for attachment '%s': %s",
-                    attachment.id,
-                    error.message);
-        }
-
-        return pixbuf;
-    }
-
     /*
      * Test whether text looks like a URI that leads somewhere other than href.  The text
      * will have a scheme prepended if it doesn't already have one, and the short versions
@@ -1344,25 +859,22 @@ public class ConversationMessage : Gtk.Box {
     private void on_remote_images_response(Gtk.InfoBar info_bar, int response_id) {
         switch (response_id) {
         case 1:
+            // Show images for the message
             show_images(true);
             break;
         case 2:
-            always_show_images();
+            // Show images for sender
+            show_images(false);
+            remember_remote_images();
             break;
         default:
-            break; // pass
+            // Pass
+            break;
         }
 
         remote_images_infobar.hide();
     }
 
-    [GtkCallback]
-    private void on_draft_response(Gtk.InfoBar info_bar, int response_id) {
-        if (response_id == 1) {
-            edit_draft(email);
-        }
-    }
-
     // private void on_copy_text() {
     //     web_view.copy_clipboard();
     // }
@@ -1395,28 +907,5 @@ public class ConversationMessage : Gtk.Box {
     //         warning("Could not make selection: %s", error.message);
     //     }
     // }
-    
-    // private void on_view_source(Geary.Email message) {
-    //     string source = message.header.buffer.to_string() + message.body.buffer.to_string();
-        
-    //     try {
-    //         string temporary_filename;
-    //         int temporary_handle = FileUtils.open_tmp("geary-message-XXXXXX.txt",
-    //             out temporary_filename);
-    //         FileUtils.set_contents(temporary_filename, source);
-    //         FileUtils.close(temporary_handle);
-            
-    //         // ensure this file is only readable by the user ... this needs to be done after the
-    //         // file is closed
-    //         FileUtils.chmod(temporary_filename, (int) (Posix.S_IRUSR | Posix.S_IWUSR));
-            
-    //         string temporary_uri = Filename.to_uri(temporary_filename, null);
-    //         Gtk.show_uri(web_view.get_screen(), temporary_uri, Gdk.CURRENT_TIME);
-    //     } catch (Error error) {
-    //         ErrorDialog dialog = new ErrorDialog(GearyApplication.instance.controller.main_window,
-    //             _("Failed to open default text editor."), error.message);
-    //         dialog.run();
-    //     }
-    // }
 
 }
diff --git a/src/client/conversation-viewer/conversation-viewer.vala 
b/src/client/conversation-viewer/conversation-viewer.vala
index e7208c5..ae3e3f5 100644
--- a/src/client/conversation-viewer/conversation-viewer.vala
+++ b/src/client/conversation-viewer/conversation-viewer.vala
@@ -51,13 +51,13 @@ public class ConversationViewer : Gtk.Stack {
         
         COUNT;
     }
- 
-    // Fired a message is added to the view
-    public signal void message_added(ConversationMessage message);
-    
-    // Fired a message is removed from the view
-    public signal void message_removed(ConversationMessage message);
-    
+
+    // Fired when an email is added to the view
+    public signal void email_row_added(ConversationEmail email);
+
+    // Fired when an email is removed from the view
+    public signal void email_row_removed(ConversationEmail email);
+
     // Fired when the user clicks "reply" in the message menu.
     public signal void reply_to_message(Geary.Email message);
 
@@ -68,7 +68,7 @@ public class ConversationViewer : Gtk.Stack {
     public signal void forward_message(Geary.Email message);
 
     // Fired when the user mark messages.
-    public signal void mark_messages(Gee.Collection<Geary.EmailIdentifier> emails,
+    public signal void mark_emails(Gee.Collection<Geary.EmailIdentifier> emails,
         Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove);
 
     // Fired when the user opens an attachment.
@@ -98,7 +98,7 @@ public class ConversationViewer : Gtk.Stack {
     [GtkChild]
     private Gtk.Box composer_page;
 
-    // Conversation messages list
+    // Conversation emails list
     [GtkChild]
     private Gtk.ListBox conversation_listbox;
     private Gtk.Widget? last_list_row;
@@ -108,13 +108,13 @@ public class ConversationViewer : Gtk.Stack {
     private Gtk.Label user_message_label;
 
     // Sorted set of emails being displayed
-    private Gee.TreeSet<Geary.Email> messages { get; private set; default = 
+    private Gee.TreeSet<Geary.Email> emails { get; private set; default =
         new Gee.TreeSet<Geary.Email>(Geary.Email.compare_sent_date_ascending); }
-    
+
     // Maps displayed emails to their corresponding ListBoxRow.
     private Gee.HashMap<Geary.EmailIdentifier, Gtk.ListBoxRow> email_to_row = new
         Gee.HashMap<Geary.EmailIdentifier, Gtk.ListBoxRow>();
-    
+
     // State machine setup for search/find modes.
     private Geary.State.MachineDescriptor search_machine_desc = new Geary.State.MachineDescriptor(
         "ConversationViewer search", SearchState.NONE, SearchState.COUNT, SearchEvent.COUNT, null, null); 
@@ -132,27 +132,27 @@ public class ConversationViewer : Gtk.Stack {
     public ConversationViewer() {
         // Setup the conversation list box
         conversation_listbox.set_sort_func((row1, row2) => {
-                // If not a ConversationMessage, will be an
+                // If not a ConversationEmail, will be an
                 // embedded composer and should always be last.
-                ConversationMessage? msg1 = row1.get_child() as ConversationMessage;
+                ConversationEmail? msg1 = row1.get_child() as ConversationEmail;
                 if (msg1 == null) {
                     return 1;
                 }
-                ConversationMessage? msg2 = row2.get_child() as ConversationMessage;
+                ConversationEmail? msg2 = row2.get_child() as ConversationEmail;
                 if (msg2 == null) {
                     return -1;
                 }
                 return Geary.Email.compare_sent_date_ascending(msg1.email, msg2.email);
             });
         conversation_listbox.row_activated.connect((box, row) => {
-                // If not a ConversationMessage, will be an
+                // If not a ConversationEmail, will be an
                 // embedded composer and should not be activated.
-                ConversationMessage? msg = row.get_child() as ConversationMessage;
+                ConversationEmail? msg = row.get_child() as ConversationEmail;
                 if (email_to_row.size > 1 && msg != null) {
                     if (msg.is_message_body_visible) {
-                        hide_message(row);
+                        collapse_email(row);
                     } else {
-                        show_message(row);
+                        expand_email(row);
                     }
                 }
             });
@@ -213,19 +213,19 @@ public class ConversationViewer : Gtk.Stack {
         do_conversation();
     }
     
-    public Geary.Email? get_last_message() {
-        return messages.is_empty ? null : messages.last();
+    public Geary.Email? get_last_email() {
+        return emails.is_empty ? null : emails.last();
     }
     
-    public Geary.Email? get_selected_message(out string? quote) {
-        // XXX check to see if there is a message with selected text,
+    public Geary.Email? get_selected_email(out string? quote) {
+        // XXX check to see if there is a email with selected text,
         // if so return that
         quote = null;
-        return messages.is_empty ? null : messages.last();
+        return emails.is_empty ? null : emails.last();
     }
-    
+
     public void check_mark_read() {
-        Gee.ArrayList<Geary.EmailIdentifier> emails =
+        Gee.ArrayList<Geary.EmailIdentifier> email_ids =
             new Gee.ArrayList<Geary.EmailIdentifier>();
 
         Gtk.Adjustment adj = conversation_page.vadjustment;
@@ -233,46 +233,49 @@ public class ConversationViewer : Gtk.Stack {
         int bottom_bound = top_bound + (int) adj.page_size;
 
         const int TEXT_PADDING = 50;
-        foreach (Geary.Email message in messages) {
-            ConversationMessage row = conversation_message_for_id(message.id);
-            // Don't bother with not-yet-loaded messages since the
+        foreach (Geary.Email email in emails) {
+            ConversationEmail conversation_email = conversation_email_for_id(email.id);
+            ConversationMessage conversation_message =
+                conversation_email.primary_message;
+            // Don't bother with not-yet-loaded emails since the
             // size of the body will be off, affecting the visibility
-            // of messages further down the conversation.
-            if (message.email_flags.is_unread() &&
-                row.is_loading_complete &&
-                !row.is_manual_read()) {
+            // of emails further down the conversation.
+            if (email.email_flags.is_unread() &&
+                conversation_message.is_loading_complete &&
+                !conversation_email.is_manual_read()) {
                  int body_top = 0;
                  int body_left = 0;
-                 row.web_view.translate_coordinates(
+                 conversation_message.web_view.translate_coordinates(
                      conversation_listbox,
                      0, 0,
                      out body_left, out body_top
                  );
-                 int body_bottom = body_top + row.web_view_allocation.height;
+                 int body_bottom = body_top +
+                     conversation_message.web_view_allocation.height;
 
-                 // Only mark the message as read if it's actually visible
+                 // Only mark the email as read if it's actually visible
                  if (body_bottom > top_bound &&
                      body_top + TEXT_PADDING < bottom_bound) {
-                     emails.add(message.id);
+                     email_ids.add(email.id);
 
                      // Since it can take some time for the new flags
                      // to round-trip back to ConversationViewer's
                      // signal handlers, mark as manually read here
-                     row.mark_manual_read();
+                     conversation_email.mark_manual_read();
                  }
              }
         }
 
-        if (emails.size > 0) {
+        if (email_ids.size > 0) {
             Geary.EmailFlags flags = new Geary.EmailFlags();
             flags.add(Geary.EmailFlags.UNREAD);
-            mark_messages(emails, null, flags);
+            mark_emails(email_ids, null, flags);
         }
     }
 
     // Use this when an email has been marked read through manual (user) intervention
     public void mark_manual_read(Geary.EmailIdentifier id) {
-        ConversationMessage? row = conversation_message_for_id(id);
+        ConversationEmail? row = conversation_email_for_id(id);
         if (row != null) {
             row.mark_manual_read();
         }
@@ -352,7 +355,7 @@ public class ConversationViewer : Gtk.Stack {
             conversation_listbox.remove(child);
         }
         email_to_row.clear();
-        messages.clear();
+        emails.clear();
         current_conversation = null;
         cleared();
     }
@@ -460,33 +463,33 @@ public class ConversationViewer : Gtk.Stack {
         // Load this once, so if it's cancelled, we cancel the WHOLE load.
         Cancellable cancellable = cancellable_fetch;
 
-        // Fetch full messages.
-        Gee.Collection<Geary.Email>? messages_to_add
-            = yield list_full_messages_async(conversation.get_emails(
+        // Fetch full emails.
+        Gee.Collection<Geary.Email>? emails_to_add
+            = yield list_full_emails_async(conversation.get_emails(
             Geary.App.Conversation.Ordering.SENT_DATE_ASCENDING), cancellable);
 
         if (cancellable.is_cancelled()) {
             return;
         }
-        
-        // Add messages.
-        if (messages_to_add != null) {
-            foreach (Geary.Email email in messages_to_add)
-                add_message(email, conversation.is_in_current_folder(email.id));
+
+        // Add emails.
+        if (emails_to_add != null) {
+            foreach (Geary.Email email in emails_to_add)
+                add_email(email, conversation.is_in_current_folder(email.id));
         }
 
         if (current_folder is Geary.SearchFolder) {
             yield highlight_search_terms();
         } else {
             compress_emails();
-            // Ensure the last message is always shown
-            Gtk.ListBoxRow last_row = conversation_listbox.get_row_at_index(messages.size - 1);
-            show_message(last_row, false);
+            // Ensure the last email is always shown
+            Gtk.ListBoxRow last_row = conversation_listbox.get_row_at_index(emails.size - 1);
+            expand_email(last_row, false);
         }
 
         loading_conversations = false;
         if (state == ViewState.CONVERSATION) {
-            debug("Messages loaded\n");
+            debug("Emails loaded\n");
             set_visible_child(conversation_page);
         }
     }
@@ -518,7 +521,7 @@ public class ConversationViewer : Gtk.Stack {
 
         // List all IDs of emails we're viewing.
         Gee.Collection<Geary.EmailIdentifier> ids = new Gee.ArrayList<Geary.EmailIdentifier>();
-        foreach (Geary.Email email in messages)
+        foreach (Geary.Email email in emails)
             ids.add(email.id);
 
         // Using the fetch cancellable here is appropriate since each
@@ -574,7 +577,7 @@ public class ConversationViewer : Gtk.Stack {
     }
 
     // Given some emails, fetch the full versions with all required fields.
-    private async Gee.Collection<Geary.Email>? list_full_messages_async(
+    private async Gee.Collection<Geary.Email>? list_full_emails_async(
         Gee.Collection<Geary.Email> emails, Cancellable? cancellable) throws Error {
         Geary.Email.Field required_fields = ConversationViewer.REQUIRED_FIELDS |
             Geary.ComposedEmail.REQUIRED_REPLY_FIELDS;
@@ -588,7 +591,7 @@ public class ConversationViewer : Gtk.Stack {
     }
     
     // Given an email, fetch the full version with all required fields.
-    private async Geary.Email fetch_full_message_async(Geary.Email email,
+    private async Geary.Email fetch_full_email_async(Geary.Email email,
         Cancellable? cancellable) throws Error {
         Geary.Email.Field required_fields = ConversationViewer.REQUIRED_FIELDS |
             Geary.ComposedEmail.REQUIRED_REPLY_FIELDS;
@@ -597,7 +600,7 @@ public class ConversationViewer : Gtk.Stack {
             Geary.Folder.ListFlags.NONE, cancellable);
     }
     
-    // Cancels the current message load, if in progress.
+    // Cancels the current email load, if in progress.
     private void cancel_load() {
         Cancellable old_cancellable = cancellable_fetch;
         cancellable_fetch = new Cancellable();
@@ -611,7 +614,7 @@ public class ConversationViewer : Gtk.Stack {
     
     private async void on_conversation_appended_async(Geary.App.Conversation conversation,
         Geary.Email email) throws Error {
-        add_message(yield fetch_full_message_async(email, cancellable_fetch),
+        add_email(yield fetch_full_email_async(email, cancellable_fetch),
             conversation.is_in_current_folder(email.id));
     }
     
@@ -624,16 +627,16 @@ public class ConversationViewer : Gtk.Stack {
     }
     
     private void on_conversation_trimmed(Geary.Email email) {
-        remove_message(email);
+        remove_email(email);
     }
     
-    private void add_message(Geary.Email email, bool is_in_folder) {
-        if (messages.contains(email)) {
+    private void add_email(Geary.Email email, bool is_in_folder) {
+        if (emails.contains(email)) {
             return;
         }
-        messages.add(email);
+        emails.add(email);
 
-        // XXX Should be able to edit draft messages from any
+        // XXX Should be able to edit draft emails from any
         // conversation. This test should be more like "is in drafts
         // folder"
         bool is_draft = (
@@ -641,53 +644,55 @@ public class ConversationViewer : Gtk.Stack {
             is_in_folder
         );
 
-        ConversationMessage message = new ConversationMessage(
+        ConversationEmail conversation_email = new ConversationEmail(
             email,
             current_folder.account.get_contact_store(),
             is_draft
         );
-        message.body_box.button_release_event.connect_after((event) => {
+
+        ConversationMessage conversation_message = conversation_email.primary_message;
+        conversation_message.body_box.button_release_event.connect_after((event) => {
                 // Consume all non-consumed clicks so the row is not
                 // inadvertently activated after clicking on the
-                // message body.
+                // email body.
                 return true;
             });
 
         Gtk.ListBoxRow row = new Gtk.ListBoxRow();
         row.show();
-        row.add(message);
+        row.add(conversation_email);
         email_to_row.set(email.id, row);
 
         conversation_listbox.add(row);
 
         if (email.is_unread() == Geary.Trillian.TRUE) {
-            show_message(row, false);
+            expand_email(row, false);
         }
 
-        message.start_loading.begin(cancellable_fetch);
-        message_added(message);
-        
+        conversation_email.start_loading.begin(cancellable_fetch);
+        email_row_added(conversation_email);
+
         // Update the search results
         //if (conversation_find_bar.visible)
         //    conversation_find_bar.commence_search();
     }
-    
-    private void remove_message(Geary.Email email) {
+
+    private void remove_email(Geary.Email email) {
         Gtk.ListBoxRow row = email_to_row.get(email.id);
-        message_removed((ConversationMessage) row.get_child());
+        email_row_removed((ConversationEmail) row.get_child());
         conversation_listbox.remove(row);
         email_to_row.get(email.id);
-        messages.remove(email);
+        emails.remove(email);
     }
 
-    private void show_message(Gtk.ListBoxRow row, bool include_transitions=true) {
+    private void expand_email(Gtk.ListBoxRow row, bool include_transitions=true) {
         row.get_style_context().add_class("geary_expand");
-        ((ConversationMessage) row.get_child()).show_message_body(include_transitions);
+        ((ConversationEmail) row.get_child()).expand_email(include_transitions);
     }
 
-    private void hide_message(Gtk.ListBoxRow row) {
+    private void collapse_email(Gtk.ListBoxRow row) {
         row.get_style_context().remove_class("geary_expand");
-        ((ConversationMessage) row.get_child()).hide_message_body();
+        ((ConversationEmail) row.get_child()).collapse_email();
     }
 
     private void compress_emails() {
@@ -716,9 +721,9 @@ public class ConversationViewer : Gtk.Stack {
             return;
         }
 
-        // Get the convo message and update its state.
+        // Get the convo email and update its state.
         Gtk.ListBoxRow row = email_to_row.get(email.id);
-        ((ConversationMessage) row.get_child()).update_flags(email);
+        ((ConversationEmail) row.get_child()).update_flags(email);
     }
 
     // State reset.
@@ -771,8 +776,8 @@ public class ConversationViewer : Gtk.Stack {
         return SearchState.SEARCH_FOLDER;
     }
 
-    private ConversationMessage? conversation_message_for_id(Geary.EmailIdentifier id) {
-        return (ConversationMessage) email_to_row.get(id).get_child();
+    private ConversationEmail? conversation_email_for_id(Geary.EmailIdentifier id) {
+        return (ConversationEmail) email_to_row.get(id).get_child();
     }
 
 }
diff --git a/src/engine/rfc822/rfc822-message.vala b/src/engine/rfc822/rfc822-message.vala
index b2f4923..2c9a645 100644
--- a/src/engine/rfc822/rfc822-message.vala
+++ b/src/engine/rfc822/rfc822-message.vala
@@ -317,7 +317,26 @@ public class Geary.RFC822.Message : BaseObject {
         return Geary.String.safe_byte_substring((preview ?? "").chug(),
             Geary.Email.MAX_PREVIEW_BYTES);
     }
-    
+
+    /**
+     * Returns the primary originator of an email, which is defined as the first mailbox address
+     * in From:, Sender:, or Reply-To:, in that order, depending on availability.
+     *
+     * Returns null if no originators are present.
+     */
+    public RFC822.MailboxAddress? get_primary_originator() {
+        if (from != null && from.size > 0)
+            return from[0];
+
+        if (sender != null)
+            return sender;
+
+        if (reply_to != null && reply_to.size > 0)
+            return reply_to[0];
+
+        return null;
+    }
+
     private void stock_from_gmime() {
         // GMime calls the From address the "sender"
         string? message_sender = message.get_sender();
diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt
index 4017c07..7bc8e7e 100644
--- a/ui/CMakeLists.txt
+++ b/ui/CMakeLists.txt
@@ -8,6 +8,7 @@ set(RESOURCE_LIST
   STRIPBLANKS "composer-headerbar.ui"
   STRIPBLANKS "composer-menus.ui"
   STRIPBLANKS "composer-widget.ui"
+  STRIPBLANKS "conversation-email.ui"
   STRIPBLANKS "conversation-message.ui"
   STRIPBLANKS "conversation-message-menu.ui"
   STRIPBLANKS "conversation-viewer.ui"
diff --git a/ui/conversation-email.ui b/ui/conversation-email.ui
new file mode 100644
index 0000000..50e80d6
--- /dev/null
+++ b/ui/conversation-email.ui
@@ -0,0 +1,174 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="ConversationEmail" parent="GtkBox">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="orientation">vertical</property>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <style>
+      <class name="geary_email"/>
+    </style>
+  </template>
+  <object class="GtkBox" id="action_box">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="halign">end</property>
+    <property name="valign">start</property>
+    <property name="homogeneous">True</property>
+    <child>
+      <object class="GtkImage" id="attachment_icon">
+        <property name="can_focus">False</property>
+        <property name="tooltip_text" translatable="yes">This message has attachments</property>
+        <property name="icon_name">mail-attachment-symbolic</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">False</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton" id="star_button">
+        <property name="visible">True</property>
+        <property name="sensitive">False</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="tooltip_text" translatable="yes">Mark this message as starred</property>
+        <property name="valign">start</property>
+        <property name="action_name">win.message_star</property>
+        <property name="relief">none</property>
+        <child>
+          <object class="GtkImage">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="icon_name">non-starred-symbolic</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">False</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton" id="unstar_button">
+        <property name="visible">True</property>
+        <property name="sensitive">False</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="tooltip_text" translatable="yes">Mark this message as not starred</property>
+        <property name="valign">start</property>
+        <property name="action_name">win.message_unstar</property>
+        <property name="relief">none</property>
+        <child>
+          <object class="GtkImage">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="icon_name">starred-symbolic</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">False</property>
+        <property name="position">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkMenuButton" id="email_menubutton">
+        <property name="visible">True</property>
+        <property name="sensitive">False</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="tooltip_text" translatable="yes">Display the message menu</property>
+        <property name="valign">start</property>
+        <property name="relief">none</property>
+        <child>
+          <object class="GtkImage">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="icon_name">open-menu-symbolic</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">False</property>
+        <property name="position">3</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkListStore" id="attachments_model">
+    <columns>
+      <!-- column-name icon -->
+      <column type="GdkPixbuf"/>
+      <!-- column-name label -->
+      <column type="gchararray"/>
+      <!-- column-name attachment_id -->
+      <column type="gchararray"/>
+    </columns>
+  </object>
+  <object class="GtkBox" id="attachments_box">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="orientation">vertical</property>
+    <child>
+      <object class="GtkSeparator">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">False</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkIconView" id="attachments_view">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="margin">0</property>
+        <property name="selection_mode">multiple</property>
+        <property name="item_orientation">horizontal</property>
+        <property name="model">attachments_model</property>
+        <property name="spacing">6</property>
+        <signal name="item-activated" handler="on_attachments_view_activated" swapped="no"/>
+        <child>
+          <object class="GtkCellRendererPixbuf" id="icon"/>
+          <attributes>
+            <attribute name="pixbuf">0</attribute>
+          </attributes>
+        </child>
+        <child>
+          <object class="GtkCellRendererText" id="file_name">
+            <property name="xpad">6</property>
+          </object>
+          <attributes>
+            <attribute name="text">1</attribute>
+          </attributes>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">True</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkBox" id="sub_messages_box">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="orientation">vertical</property>
+    <child>
+      <placeholder/>
+    </child>
+  </object>
+</interface>
diff --git a/ui/conversation-message.ui b/ui/conversation-message.ui
index 69890c2..876318e 100644
--- a/ui/conversation-message.ui
+++ b/ui/conversation-message.ui
@@ -8,7 +8,7 @@
     <property name="can_focus">False</property>
     <property name="orientation">vertical</property>
     <child>
-      <object class="GtkBox">
+      <object class="GtkBox" id="summary_box">
         <property name="visible">True</property>
         <property name="can_focus">False</property>
         <property name="spacing">8</property>
@@ -384,105 +384,6 @@
             <property name="position">1</property>
           </packing>
         </child>
-        <child>
-          <object class="GtkBox">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="valign">start</property>
-            <property name="vexpand">False</property>
-            <property name="spacing">1</property>
-            <property name="homogeneous">True</property>
-            <child>
-              <object class="GtkImage" id="attachment_icon">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="tooltip_text" translatable="yes">This message has attachments</property>
-                <property name="icon_name">mail-attachment-symbolic</property>
-              </object>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">True</property>
-                <property name="position">0</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkButton" id="star_button">
-                <property name="visible">True</property>
-                <property name="sensitive">False</property>
-                <property name="can_focus">True</property>
-                <property name="receives_default">True</property>
-                <property name="tooltip_text" translatable="yes">Mark this message as starred</property>
-                <property name="valign">start</property>
-                <property name="action_name">win.message_star</property>
-                <property name="relief">none</property>
-                <child>
-                  <object class="GtkImage">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="icon_name">non-starred-symbolic</property>
-                  </object>
-                </child>
-              </object>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">True</property>
-                <property name="position">1</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkButton" id="unstar_button">
-                <property name="visible">True</property>
-                <property name="sensitive">False</property>
-                <property name="can_focus">True</property>
-                <property name="receives_default">True</property>
-                <property name="tooltip_text" translatable="yes">Mark this message as not starred</property>
-                <property name="valign">start</property>
-                <property name="action_name">win.message_unstar</property>
-                <property name="relief">none</property>
-                <child>
-                  <object class="GtkImage">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="icon_name">starred-symbolic</property>
-                  </object>
-                </child>
-              </object>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">True</property>
-                <property name="position">2</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkMenuButton" id="message_menubutton">
-                <property name="visible">True</property>
-                <property name="sensitive">False</property>
-                <property name="can_focus">True</property>
-                <property name="receives_default">True</property>
-                <property name="tooltip_text" translatable="yes">Display the message menu</property>
-                <property name="valign">start</property>
-                <property name="relief">none</property>
-                <child>
-                  <object class="GtkImage">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="icon_name">open-menu-symbolic</property>
-                  </object>
-                </child>
-              </object>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">False</property>
-                <property name="position">3</property>
-              </packing>
-            </child>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">False</property>
-            <property name="position">2</property>
-          </packing>
-        </child>
       </object>
       <packing>
         <property name="expand">False</property>
@@ -615,7 +516,6 @@
                 <property name="can_focus">False</property>
                 <property name="no_show_all">True</property>
                 <property name="message_type">warning</property>
-                <signal name="response" handler="on_draft_response" swapped="no"/>
                 <child internal-child="action_area">
                   <object class="GtkButtonBox">
                     <property name="can_focus">False</property>
@@ -783,61 +683,4 @@
       <class name="tooltip"/>
     </style>
   </object>
-  <object class="GtkListStore" id="attachments_model">
-    <columns>
-      <!-- column-name icon -->
-      <column type="GdkPixbuf"/>
-      <!-- column-name label -->
-      <column type="gchararray"/>
-      <!-- column-name attachment_id -->
-      <column type="gchararray"/>
-    </columns>
-  </object>
-  <object class="GtkBox" id="attachments_box">
-    <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <property name="orientation">vertical</property>
-    <child>
-      <object class="GtkSeparator">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-      </object>
-      <packing>
-        <property name="expand">False</property>
-        <property name="fill">False</property>
-        <property name="position">0</property>
-      </packing>
-    </child>
-    <child>
-      <object class="GtkIconView" id="attachments_view">
-        <property name="visible">True</property>
-        <property name="can_focus">True</property>
-        <property name="margin">0</property>
-        <property name="selection_mode">multiple</property>
-        <property name="item_orientation">horizontal</property>
-        <property name="model">attachments_model</property>
-        <property name="spacing">6</property>
-        <signal name="item-activated" handler="on_attachments_view_activated" swapped="no"/>
-        <child>
-          <object class="GtkCellRendererPixbuf" id="icon"/>
-          <attributes>
-            <attribute name="pixbuf">0</attribute>
-          </attributes>
-        </child>
-        <child>
-          <object class="GtkCellRendererText" id="file_name">
-            <property name="xpad">6</property>
-          </object>
-          <attributes>
-            <attribute name="text">1</attribute>
-          </attributes>
-        </child>
-      </object>
-      <packing>
-        <property name="expand">True</property>
-        <property name="fill">True</property>
-        <property name="position">1</property>
-      </packing>
-    </child>
-  </object>
 </interface>
diff --git a/ui/geary.css b/ui/geary.css
index fa79d12..2482509 100644
--- a/ui/geary.css
+++ b/ui/geary.css
@@ -66,12 +66,13 @@ row.geary-folder-popover-list-row > label {
   border-bottom-width: 0;
   padding: 0;
   box-shadow: 0 4px 8px 1px rgba(0,0,0,0.4);
-  transition: margin 0.1s, background 0.15s;
+  transition: margin 0.1s;
 }
 #conversation_listbox > row > box {
   background: shade(@theme_base_color, 0.96);
+  transition: background 0.25s;
 }
-#conversation_listbox > row:hover,
+#conversation_listbox > row:hover > box,
 #conversation_listbox > row > box.geary_show_body,
 #conversation_listbox > row:hover > box.geary_show_body {
   background: @theme_base_color;


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