[geary/wip/765516-gtk-widget-conversation-viewer: 2/58] Add new ConversationMessage widget to display a single message with a WebView.
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/wip/765516-gtk-widget-conversation-viewer: 2/58] Add new ConversationMessage widget to display a single message with a WebView.
- Date: Fri, 13 May 2016 02:16:32 +0000 (UTC)
commit 7de1c583921088e3dfe7a85c6bd1e7f6acdc55a8
Author: Michael James Gratton <mike vee net>
Date: Sat Apr 9 17:32:29 2016 +1000
Add new ConversationMessage widget to display a single message with a WebView.
The new widget is designed to be added to a ListBox like container, and
can display both a summary and the complete message, a'la the traditional
Geary ConversationView.
Most features are currently disabled, but it does handle hiding/showing
the message body using a single WebKit.WebView. All code from
ConversationViewer relating to DOM manipulation as been copied over, all
but that which was needed to display the message has been commentd out.
* src/client/conversation-viewer/conversation-message.vala: Source code
for new widget.
* src/client/components/main-window.vala: Add CSS theme code for
ConversationMessage.
* ui/conversation-message.ui: GtkBuilder template for new widget.
* ui/conversation-message-menu.ui: GtkBuilder for the message menu. This
is a separate file since GTK+ 3.10 doesn't support GtkPopoverMenu and I
can't build it using Glade otherwise.
* src/CMakeLists.txt: Added new source file.
* po/POTFILES.in, ui/CMakeLists.txt: Added new UI files.
po/POTFILES.in | 2 +
src/CMakeLists.txt | 1 +
src/client/components/main-window.vala | 15 +-
.../conversation-viewer/conversation-message.vala | 1342 ++++++++++++++++++++
ui/CMakeLists.txt | 2 +
ui/conversation-message-menu.ui | 50 +
ui/conversation-message.ui | 439 +++++++
7 files changed, 1849 insertions(+), 2 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 8f0beae..59a9e3f 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -383,6 +383,8 @@ src/mailer/main.vala
[type: gettext/glade]ui/certificate_warning_dialog.glade
[type: gettext/glade]ui/composer_accelerators.ui
[type: gettext/glade]ui/composer.glade
+[type: gettext/glade]ui/conversation-message.ui
+[type: gettext/glade]ui/conversation-message-menu.ui
[type: gettext/glade]ui/edit_alternate_emails.glade
[type: gettext/glade]ui/find_bar.glade
[type: gettext/glade]ui/login.glade
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 124cd18..bdbcedd 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -362,6 +362,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-message.vala
client/conversation-viewer/conversation-viewer.vala
client/conversation-viewer/conversation-web-view.vala
diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala
index 5a42312..07e5c7d 100644
--- a/src/client/components/main-window.vala
+++ b/src/client/components/main-window.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.
*/
public class MainWindow : Gtk.ApplicationWindow {
@@ -185,6 +187,15 @@ public class MainWindow : Gtk.ApplicationWindow {
.geary-titlebar-left:dir(rtl) {
border-top-left-radius: 0px;
}
+ #ConversationMessage {
+ padding: 12px;
+ }
+ #ConversationMessage .header-label {
+ margin-right: 6px;
+ }
+ #ConversationMessage separator {
+ margin: 12px 0;
+ }
""";
if(Gtk.MAJOR_VERSION > 3 || Gtk.MAJOR_VERSION == 3 && Gtk.MINOR_VERSION >= 14) {
diff --git a/src/client/conversation-viewer/conversation-message.vala
b/src/client/conversation-viewer/conversation-message.vala
new file mode 100644
index 0000000..a84c19d
--- /dev/null
+++ b/src/client/conversation-viewer/conversation-message.vala
@@ -0,0 +1,1342 @@
+/*
+ * 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 a message in a conversation.
+ */
+
+[GtkTemplate (ui = "/org/gnome/Geary/conversation-message.ui")]
+public class ConversationMessage : Gtk.Box {
+
+
+ // Internal class to associate inline image buffers (replaced by rotated scaled versions of
+ // them) so they can be saved intact if the user requires it
+ private class ReplacedImage : Geary.BaseObject {
+ public string id;
+ public string filename;
+ public Geary.Memory.Buffer buffer;
+
+ public ReplacedImage(int replaced_number, string filename, Geary.Memory.Buffer buffer) {
+ id = "%X".printf(replaced_number);
+ this.filename = filename;
+ this.buffer = buffer;
+ }
+ }
+
+ private const string[] INLINE_MIME_TYPES = {
+ "image/png",
+ "image/gif",
+ "image/jpeg",
+ "image/pjpeg",
+ "image/bmp",
+ "image/x-icon",
+ "image/x-xbitmap",
+ "image/x-xbm"
+ };
+ private const int ATTACHMENT_PREVIEW_SIZE = 50;
+ 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;
+
+ [GtkChild]
+ private Gtk.Image avatar_image;
+
+ [GtkChild]
+ private Gtk.Revealer from_revealer;
+
+ [GtkChild]
+ private Gtk.Box from_header;
+
+ [GtkChild]
+ private Gtk.Box to_header;
+
+ [GtkChild]
+ private Gtk.Box cc_header;
+
+ [GtkChild]
+ private Gtk.Box bcc_header;
+
+ [GtkChild]
+ private Gtk.Box subject_header;
+
+ [GtkChild]
+ private Gtk.Box date_header;
+
+ [GtkChild]
+ private Gtk.Label preview_label;
+
+ [GtkChild]
+ private Gtk.Button flag_button;
+
+ [GtkChild]
+ private Gtk.MenuButton message_menubutton;
+
+ [GtkChild]
+ private Gtk.Revealer body_revealer;
+
+ [GtkChild]
+ private Gtk.Box body_box;
+
+ // The email message being displayed
+ public Geary.Email email { get; private set; }
+
+ // The message being displayed
+ public Geary.RFC822.Message message { get; private set; }
+
+ // The folder containing the message
+ private Geary.Folder containing_folder = null; // XXX weak??
+
+ // The HTML viewer to view the emails.
+ private ConversationWebView web_view { get; private set; }
+
+ // Overlay consisting of a label in front of a webpage
+ private Gtk.Overlay message_overlay;
+
+ // Label for displaying overlay messages.
+ //private Gtk.Label message_overlay_label;
+
+ //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>();
+
+ public ConversationMessage(Geary.Email email, Geary.Folder containing_folder) {
+ this.email = email;
+ this.containing_folder = containing_folder;
+
+ try {
+ message = email.get_message();
+ } catch (Error error) {
+ debug("Error loading message: %s", error.message);
+ return;
+ }
+
+ set_header_text(from_header, format_addresses(message.from));
+
+ if (message.to != null) {
+ set_header_text(to_header, format_addresses(message.to));
+ to_header.get_style_context().remove_class("empty");
+ }
+
+ if (message.cc != null) {
+ set_header_text(cc_header, format_addresses(message.cc));
+ cc_header.get_style_context().remove_class("empty");
+ }
+
+ if (message.bcc != null) {
+ set_header_text(bcc_header, format_addresses(message.bcc));
+ bcc_header.get_style_context().remove_class("empty");
+ }
+
+ if (message.subject != null) {
+ set_header_text(subject_header, message.subject.value);
+ subject_header.get_style_context().remove_class("empty");
+ }
+
+ if (message.date != null) {
+ Date.ClockFormat clock_format =
+ GearyApplication.instance.config.clock_format;
+ set_header_text(
+ date_header,
+ Date.pretty_print_verbose(message.date.value, clock_format)
+ );
+ date_header.get_style_context().remove_class("empty");
+ }
+
+ string preview_str = message.get_preview();
+ preview_str = Geary.String.reduce_whitespace(preview_str);
+ preview_label.set_text(preview_str);
+
+ message_menubutton.set_menu_model(build_message_menu(email));
+ message_menubutton.set_sensitive(false);
+
+ web_view = new ConversationWebView();
+ web_view.show();
+ body_box.pack_end(web_view, true, true, 0);
+
+ load_message_body();
+
+ //Gtk.ScrolledWindow web_scroller = new Gtk.ScrolledWindow(null, null);
+ //web_scroller.show();
+ //web_scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.NEVER);
+ //web_scroller.add(web_view);
+ //body_box.pack_end(web_scroller, true, true, 0);
+
+ // web_view.hovering_over_link.connect(on_hovering_over_link);
+ // web_view.context_menu.connect(() => { return true; }); // Suppress default context menu.
+ // web_view.realize.connect( () => { web_view.get_vadjustment().value_changed.connect(mark_read); });
+ // web_view.size_allocate.connect(mark_read);
+ web_view.realize.connect(() => { debug("web_view: realised"); });
+ web_view.size_allocate.connect(() => { debug("web_view: allocated"); });
+
+ // web_view.link_selected.connect((link) => { link_selected(link); });
+
+ // if (email.from != null && email.from.contains_normalized(current_account_information.email)) {
+ // // XXX set a RO property?
+ // get_style_context().add_class("sent");
+ // }
+
+ // // Set attachment icon and add the attachments container if there are displayed attachments.
+ // int displayed = displayed_attachments(email);
+ // set_attachment_icon(div_message, displayed > 0);
+ // if (displayed > 0) {
+ // insert_attachments(div_message, email.attachments);
+ // }
+
+ // // 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);
+ // }
+ // }
+
+ // // Edit draft button for drafts folder.
+ // if (in_drafts_folder() && is_in_folder) {
+ // WebKit.DOM.HTMLElement draft_edit_container = Util.DOM.select(div_message, ".draft_edit");
+ // WebKit.DOM.HTMLElement draft_edit_button =
+ // Util.DOM.select(div_message, ".draft_edit_button");
+ // try {
+ // draft_edit_container.set_attribute("style", "display:block");
+ // draft_edit_button.set_inner_html(_("Edit Draft"));
+ // } catch (Error e) {
+ // warning("Error setting draft button: %s", e.message);
+ // }
+ // }
+
+ update_flags(email);
+
+ message_overlay = new Gtk.Overlay();
+ //message_overlay.add(conversation_viewer_scrolled);
+ // composer_paned.pack1(message_overlay, true, false);
+ }
+
+ public bool is_message_visible() {
+ return get_style_context().has_class("show-message");
+ }
+
+ public void show_message(bool include_transitions=true) {
+ get_style_context().add_class("show-message");
+ avatar_image.set_pixel_size(32); // XXX constant
+
+ // XXX this is pretty gross
+ Gtk.RevealerTransitionType revealer = from_revealer.get_transition_type();
+ if (!include_transitions) {
+ from_revealer.set_transition_type(Gtk.RevealerTransitionType.NONE);
+ }
+ from_revealer.set_reveal_child(true);
+ from_revealer.set_transition_type(revealer);
+
+ if (!to_header.get_style_context().has_class("empty")) {
+ to_header.show();
+ }
+ if (!cc_header.get_style_context().has_class("empty")) {
+ cc_header.show();
+ }
+ if (!bcc_header.get_style_context().has_class("empty")) {
+ bcc_header.show();
+ }
+ if (!subject_header.get_style_context().has_class("empty")) {
+ subject_header.show();
+ }
+ if (!date_header.get_style_context().has_class("empty")) {
+ date_header.show();
+ }
+ preview_label.hide();
+ flag_button.set_sensitive(true);
+ message_menubutton.set_sensitive(true);
+
+ // XXX this is pretty gross
+ revealer = body_revealer.get_transition_type();
+ if (!include_transitions) {
+ body_revealer.set_transition_type(Gtk.RevealerTransitionType.NONE);
+ }
+ body_revealer.set_reveal_child(true);
+ body_revealer.set_transition_type(revealer);
+ }
+
+ public void hide_message() {
+ get_style_context().remove_class("show-message");
+ avatar_image.set_pixel_size(24); // XXX constant
+ from_revealer.set_reveal_child(false);
+ to_header.hide();
+ cc_header.hide();
+ bcc_header.hide();
+ subject_header.hide();
+ date_header.hide();
+ preview_label.show();
+ flag_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);
+ }
+
+ 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) {
+ toggle_class("read");
+ toggle_class("starred");
+
+ //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()));
+ }
+
+ public void mark_manual_read() {
+ get_style_context().add_class("manual_read");
+ }
+
+ // private void build_message_overlay_label(string? url) {
+ // message_overlay_label = new Gtk.Label(url);
+ // message_overlay_label.ellipsize = Pango.EllipsizeMode.MIDDLE;
+ // message_overlay_label.halign = Gtk.Align.START;
+ // message_overlay_label.valign = Gtk.Align.END;
+ // //message_overlay_label.realize.connect(on_message_overlay_label_realize);
+ // message_overlay.add_overlay(message_overlay_label);
+ // }
+
+ private void load_message_body() {
+ bool remote_images = false;
+ string body_text = "";
+ try {
+ body_text = message.get_body(Geary.RFC822.TextFormat.HTML, inline_image_replacer) ?? "";
+ } catch (Error err) {
+ debug("Could not get message text. %s", err.message);
+ }
+
+ body_text = clean_html_markup(body_text, message, out remote_images);
+ web_view.load_string(body_text, "text/html", "UTF8", "");
+
+ // XXX The following will probably need to happen after the
+ // message has been loaded.
+
+ // if (remote_images) {
+ // Geary.Contact contact = containing_folder.account.get_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()) {
+ // show_images_email(div_message, false);
+ // } else {
+ // WebKit.DOM.HTMLElement remote_images_bar =
+ // Util.DOM.select(div_message, ".remote_images");
+ // try {
+ // ((WebKit.DOM.Element) remote_images_bar).get_class_list().add("show");
+ // remote_images_bar.set_inner_html("""%s %s
+ // <input type="button" value="%s" class="show_images" />
+ // <input type="button" value="%s" class="show_from" />""".printf(
+ // remote_images_bar.get_inner_html(),
+ // _("This message contains remote images."), _("Show Images"),
+ // _("Always Show From Sender")));
+ // } catch (Error error) {
+ // warning("Error showing remote images bar: %s", error.message);
+ // }
+ // }
+ // }
+ }
+
+ // 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,
+ // this returns HTML that is placed into the document in the position where the MIME part was
+ // found
+ private string? inline_image_replacer(string filename, Geary.Mime.ContentType? content_type,
+ Geary.Mime.ContentDisposition? disposition, string? content_id, Geary.Memory.Buffer buffer) {
+ if (content_type == null) {
+ debug("Not displaying inline: no Content-Type");
+
+ return null;
+ }
+
+ if (!is_content_type_supported_inline(content_type)) {
+ debug("Not displaying %s inline: unsupported Content-Type", content_type.to_string());
+
+ return null;
+ }
+
+ // Even if the image doesn't need to be rotated, there's a win here: by reducing the size
+ // of the image at load time, it reduces the amount of work that has to be done to insert
+ // it into the HTML and then decoded and displayed for the user ... note that we currently
+ // have the doucment set up to reduce the size of the image to fit in the viewport, and a
+ // scaled load-and-deode is always faster than load followed by scale.
+ Geary.Memory.Buffer rotated_image = buffer;
+ string mime_type = content_type.get_mime_type();
+ try {
+ Gdk.PixbufLoader loader = new Gdk.PixbufLoader();
+ loader.size_prepared.connect(on_inline_image_size_prepared);
+
+ Geary.Memory.UnownedBytesBuffer? unowned_buffer = buffer as Geary.Memory.UnownedBytesBuffer;
+ if (unowned_buffer != null)
+ loader.write(unowned_buffer.to_unowned_uint8_array());
+ else
+ loader.write(buffer.get_uint8_array());
+ loader.close();
+
+ Gdk.Pixbuf? pixbuf = loader.get_pixbuf();
+ if (pixbuf != null) {
+ pixbuf = pixbuf.apply_embedded_orientation();
+
+ // trade-off here between how long it takes to compress the data and how long it
+ // takes to turn it into Base-64 (coupled with how long it takes WebKit to then
+ // Base-64 decode and uncompress it)
+ uint8[] image_data;
+ pixbuf.save_to_buffer(out image_data, "png", "compression", "5");
+
+ // Save length before transferring ownership (which frees the array)
+ int image_length = image_data.length;
+ rotated_image = new Geary.Memory.ByteBuffer.take((owned) image_data, image_length);
+ mime_type = "image/png";
+ }
+ } catch (Error err) {
+ debug("Unable to load and rotate image %s for display: %s", filename, err.message);
+ }
+
+ // store so later processing of the message doesn't replace this element with the original
+ // MIME part
+ string? escaped_content_id = null;
+ if (!Geary.String.is_empty(content_id)) {
+ replaced_content_ids.add(content_id);
+ escaped_content_id = Geary.HTML.escape_markup(content_id);
+ }
+
+ // Store the original buffer and its filename in a local map so they can be recalled later
+ // (if the user wants to save it) ... note that Content-ID is optional and there's no
+ // guarantee that filename will be unique, even in the same message, so need to generate
+ // a unique identifier for each object
+ ReplacedImage replaced_image = new ReplacedImage(next_replaced_buffer_number++, filename,
+ buffer);
+ replaced_images.set(replaced_image.id, replaced_image);
+
+ return "<img alt=\"%s\" class=\"%s %s\" src=\"%s\" replaced-id=\"%s\" %s />".printf(
+ Geary.HTML.escape_markup(filename),
+ DATA_IMAGE_CLASS, REPLACED_IMAGE_CLASS,
+ assemble_data_uri(mime_type, rotated_image),
+ Geary.HTML.escape_markup(replaced_image.id),
+ escaped_content_id != null ? @"cid=\"$escaped_content_id\"" : "");
+ }
+
+ // Called by Gdk.PixbufLoader when the image's size has been determined but not loaded yet ...
+ // this allows us to load the image scaled down, for better performance when manipulating and
+ // writing the data URI for WebKit
+ private static void on_inline_image_size_prepared(Gdk.PixbufLoader loader, int width, int height) {
+ // easier to use as local variable than have the const listed everywhere in the code
+ // IN ALL SCREAMING CAPS
+ int scale = MAX_INLINE_IMAGE_MAJOR_DIM;
+
+ // Borrowed liberally from Shotwell's Dimensions.get_scaled() method
+
+ // check for existing fit
+ if (width <= scale && height <= scale)
+ return;
+
+ int adj_width, adj_height;
+ if ((width - scale) > (height - scale)) {
+ double aspect = (double) scale / (double) width;
+
+ adj_width = scale;
+ adj_height = (int) Math.round((double) height * aspect);
+ } else {
+ double aspect = (double) scale / (double) height;
+
+ adj_width = (int) Math.round((double) width * aspect);
+ adj_height = scale;
+ }
+
+ loader.set_size(adj_width, adj_height);
+ }
+
+ // private Gtk.Menu build_context_menu(Geary.Email email, WebKit.DOM.Element clicked_element) {
+ // Gtk.Menu menu = new Gtk.Menu();
+
+ // if (web_view.can_copy_clipboard()) {
+ // // Add a menu item for copying the current selection.
+ // Gtk.MenuItem item = new Gtk.MenuItem.with_mnemonic(_("_Copy"));
+ // item.activate.connect(on_copy_text);
+ // menu.append(item);
+ // }
+
+ // if (hover_url != null) {
+ // if (hover_url.has_prefix(Geary.ComposedEmail.MAILTO_SCHEME)) {
+ // // Add a menu item for copying the address.
+ // Gtk.MenuItem item = new Gtk.MenuItem.with_mnemonic(_("Copy _Email Address"));
+ // item.activate.connect(on_copy_email_address);
+ // menu.append(item);
+ // } else {
+ // // Add a menu item for copying the link.
+ // Gtk.MenuItem item = new Gtk.MenuItem.with_mnemonic(_("Copy _Link"));
+ // item.activate.connect(on_copy_link);
+ // menu.append(item);
+ // }
+ // }
+
+ // // Select message.
+ // if (!is_hidden()) {
+ // Gtk.MenuItem select_message_item = new Gtk.MenuItem.with_mnemonic(_("Select _Message"));
+ // select_message_item.activate.connect(() => {on_select_message(clicked_element);});
+ // menu.append(select_message_item);
+ // }
+
+ // // Select all.
+ // Gtk.MenuItem select_all_item = new Gtk.MenuItem.with_mnemonic(_("Select _All"));
+ // select_all_item.activate.connect(on_select_all);
+ // menu.append(select_all_item);
+
+ // // Inspect.
+ // if (Args.inspector) {
+ // Gtk.MenuItem inspect_item = new Gtk.MenuItem.with_mnemonic(_("_Inspect"));
+ // inspect_item.activate.connect(() => {web_view.web_inspector.inspect_node(clicked_element);});
+ // menu.append(inspect_item);
+ // }
+
+ // return menu;
+ // }
+
+ // private static void on_hide_quote_clicked(WebKit.DOM.Element element) {
+ // try {
+ // WebKit.DOM.Element parent = element.get_parent_element();
+ // parent.set_attribute("class", "quote_container controllable hide");
+ // } catch (Error error) {
+ // warning("Error hiding quote: %s", error.message);
+ // }
+ // }
+
+ // private static void on_show_quote_clicked(WebKit.DOM.Element element) {
+ // try {
+ // WebKit.DOM.Element parent = element.get_parent_element();
+ // parent.set_attribute("class", "quote_container controllable show");
+ // } catch (Error error) {
+ // warning("Error hiding quote: %s", error.message);
+ // }
+ // }
+
+ // 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();
+ // }
+
+ // private void on_show_images() {
+ // show_images(true);
+ // }
+
+ // private void on_show_images_from() {
+ // Geary.ContactStore contact_store =
+ // containing_folder.account.get_contact_store();
+ // 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);
+
+ // WebKit.DOM.Document document = web_view.get_dom_document();
+ // try {
+ // WebKit.DOM.NodeList nodes = document.query_selector_all(".email");
+ // for (ulong i = 0; i < nodes.length; i ++) {
+ // WebKit.DOM.Element? email_element = nodes.item(i) as WebKit.DOM.Element;
+ // if (email_element != null) {
+ // string? address = null;
+ // WebKit.DOM.Element? address_el = email_element.query_selector(".address_value");
+ // if (address_el != null) {
+ // address = ((WebKit.DOM.HTMLElement) address_el).get_inner_text();
+ // } else {
+ // address_el = email_element.query_selector(".address_name");
+ // if (address_el != null)
+ // address = ((WebKit.DOM.HTMLElement) address_el).get_inner_text();
+ // }
+ // if (address != null && address.normalize().casefold() == contact.normalized_email)
+ // show_images(false);
+ // }
+ // }
+ // } catch (Error error) {
+ // debug("Error showing images: %s", error.message);
+ // }
+ // }
+
+ // private void show_images(bool remember) {
+ // WebKit.DOM.Element email_element = get_email_element();
+ // try {
+ // WebKit.DOM.NodeList body_nodes = email_element.query_selector_all(".body");
+ // for (ulong j = 0; j < body_nodes.length; j++) {
+ // WebKit.DOM.Element? body = body_nodes.item(j) as WebKit.DOM.Element;
+ // if (body == null)
+ // continue;
+
+ // WebKit.DOM.NodeList nodes = body.query_selector_all("img");
+ // for (ulong i = 0; i < nodes.length; i++) {
+ // WebKit.DOM.Element? element = nodes.item(i) as WebKit.DOM.Element;
+ // if (element == null || !element.has_attribute("src"))
+ // continue;
+
+ // string src = element.get_attribute("src");
+ // if (!web_view.is_always_loaded(src)) {
+ // // Workaround a WebKitGTK+ 2.4.10 crash. See Bug 763933
+ // element.remove_attribute("src");
+ // element.set_attribute("src", web_view.allow_prefix + src);
+ // }
+ // }
+ // }
+
+ // WebKit.DOM.Element? remote_images = email_element.query_selector(".remote_images");
+ // if (remote_images != null)
+ // remote_images.get_class_list().remove("show");
+ // } catch (Error error) {
+ // warning("Error showing images: %s", error.message);
+ // }
+
+ // 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);
+ //
get_viewer().mark_messages(Geary.iterate<Geary.EmailIdentifier>(email.id).to_array_list(), flags, null);
+ // }
+ // }
+ // }
+
+ // private bool on_link_clicked_self(WebKit.DOM.Element element) {
+ // if (!Geary.String.is_empty(element.get_attribute("warning"))) {
+ // // A warning is open, so ignore clicks.
+ // return true;
+ // }
+
+ // string? href = element.get_attribute("href");
+ // if (Geary.String.is_empty(href))
+ // return false;
+ // string text = ((WebKit.DOM.HTMLElement) element).get_inner_text();
+ // string href_short, text_short;
+ // if (!deceptive_text(href, ref text, out href_short, out text_short))
+ // return false;
+
+ // WebKit.DOM.HTMLElement div = Util.DOM.clone_select(web_view.get_dom_document(),
+ // "#link_warning_template");
+ // try {
+ // div.set_inner_html("""%s %s <span><a href="%s">%s</a></span> %s
+ // <span><a href="%s">%s</a></span>""".printf(div.get_inner_html(),
+ // _("This link appears to go to"), text, text_short,
+ // _("but actually goes to"), href, href_short));
+ // div.remove_attribute("id");
+ // element.parent_node.insert_before(div, element);
+ // element.set_attribute("warning", "open");
+
+ // long overhang = div.get_offset_left() + div.get_offset_width() -
+ // web_view.get_dom_document().get_body().get_offset_width();
+ // if (overhang > 0)
+ // div.set_attribute("style", @"margin-left: -$(overhang)px;");
+ // } catch (Error error) {
+ // warning("Error showing link warning dialog: %s", error.message);
+ // }
+ // bind_event(web_view, ".link_warning .close_link_warning, .link_warning a", "click",
+ // (Callback) on_close_link_warning, this);
+ // return true;
+ // }
+
+ // private void on_close_link_warning(WebKit.DOM.Element element, WebKit.DOM.Event event,
+ // ConversationMessage conversation_message) {
+ // try {
+ // WebKit.DOM.Element warning_div = closest_ancestor(element, ".link_warning");
+ // WebKit.DOM.Element link = (WebKit.DOM.Element) warning_div.get_next_sibling();
+ // link.remove_attribute("warning");
+ // warning_div.parent_node.remove_child(warning_div);
+ // } catch (Error error) {
+ // warning("Error removing link warning dialog: %s", error.message);
+ // }
+ // }
+
+ // private void on_draft_edit_menu() {
+ // get_viewer().edit_draft(email);
+ // }
+
+ // /*
+ // * 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
+ // * have the scheme skipped and long paths truncated.
+ // */
+ // private bool deceptive_text(string href, ref string text, out string href_short,
+ // out string text_short) {
+ // href_short = "";
+ // text_short = "";
+ // // mailto URLs have a different form, and the worst they can do is pop up a composer,
+ // // so we don't trigger on them.
+ // if (href.has_prefix("mailto:"))
+ // return false;
+
+ // // First, does text look like a URI? Right now, just test whether it has
+ // // <string>.<string> in it. More sophisticated tests are possible.
+ // GLib.MatchInfo text_match, href_match;
+ // try {
+ // GLib.Regex domain = new GLib.Regex(
+ // "([a-z]*://)?" // Optional scheme
+ // + "([^\\s:/]+\\.[^\\s:/\\.]+)" // Domain
+ // + "(/[^\\s]*)?" // Optional path
+ // );
+ // if (!domain.match(text, 0, out text_match))
+ // return false;
+ // if (!domain.match(href, 0, out href_match)) {
+ // // If href doesn't look like a URL, something is fishy, so warn the user
+ // href_short = href + _(" (Invalid?)");
+ // text_short = text;
+ // return true;
+ // }
+ // } catch (Error error) {
+ // warning("Error in Regex text for deceptive urls: %s", error.message);
+ // return false;
+ // }
+
+ // // Second, do the top levels of the two domains match? We compare the top n levels,
+ // // where n is the minimum of the number of levels of the two domains.
+ // string[] href_parts = href_match.fetch_all();
+ // string[] text_parts = text_match.fetch_all();
+ // string[] text_domain = text_parts[2].down().reverse().split(".");
+ // string[] href_domain = href_parts[2].down().reverse().split(".");
+ // for (int i = 0; i < text_domain.length && i < href_domain.length; i++) {
+ // if (text_domain[i] != href_domain[i]) {
+ // if (href_parts[1] == "")
+ // href_parts[1] = "http://";
+ // if (text_parts[1] == "")
+ // text_parts[1] = href_parts[1];
+ // string temp;
+ // assemble_uris(href_parts, out temp, out href_short);
+ // assemble_uris(text_parts, out text, out text_short);
+ // return true;
+ // }
+ // }
+ // return false;
+ // }
+
+ // private void assemble_uris(string[] parts, out string full, out string short_) {
+ // full = parts[1] + parts[2];
+ // short_ = parts[2];
+ // if (parts.length == 4 && parts[3] != "/") {
+ // full += parts[3];
+ // if (parts[3].length > 20)
+ // short_ += parts[3].substring(0, 20) + "…";
+ // else
+ // short_ += parts[3];
+ // }
+ // }
+
+ // private void on_attachment_clicked(string attachment_id) {
+ // Geary.Attachment? attachment = null;
+ // try {
+ // attachment = email.get_attachment(attachment_id);
+ // } catch (Error error) {
+ // warning("Error opening attachment: %s", error.message);
+ // }
+
+ // if (attachment != null) {
+ // get_viewer().open_attachment(attachment);
+ // }
+ // }
+
+ // private void on_data_image_menu(WebKit.DOM.Element element, WebKit.DOM.Event event) {
+ // event.stop_propagation();
+
+ // string? replaced_id = element.get_attribute("replaced-id");
+ // if (Geary.String.is_empty(replaced_id))
+ // return;
+
+ // ReplacedImage? replaced_image = replaced_images.get(replaced_id);
+ // if (replaced_image == null)
+ // return;
+
+ // image_menu = new Gtk.Menu();
+ // image_menu.selection_done.connect(() => {
+ // image_menu = null;
+ // });
+
+ // Gtk.MenuItem save_image_item = new Gtk.MenuItem.with_mnemonic(_("_Save Image As..."));
+ // save_image_item.activate.connect(() => {
+ // 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);
+ // }
+
+ // 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 WebKit.DOM.HTMLDivElement create_quote_container() throws Error {
+ WebKit.DOM.HTMLDivElement quote_container = web_view.create_div();
+ quote_container.set_attribute("class", "quote_container controllable hide");
+ quote_container.set_inner_html(
+ """<div class="shower"><input type="button" value="▼ ▼ ▼" /></div>""" +
+ """<div class="hider"><input type="button" value="▲ ▲ ▲" /></div>""" +
+ """<div class="quote"></div>""");
+ return quote_container;
+ }
+
+ // private void unset_controllable_quotes(WebKit.DOM.HTMLElement element) throws GLib.Error {
+ // WebKit.DOM.NodeList quote_list = element.query_selector_all(".quote_container.controllable");
+ // for (int i = 0; i < quote_list.length; ++i) {
+ // WebKit.DOM.Element quote_container = quote_list.item(i) as WebKit.DOM.Element;
+ // long scroll_height = quote_container.query_selector(".quote").scroll_height;
+ // // If the message is hidden, scroll_height will be 0.
+ // if (scroll_height > 0 && scroll_height < QUOTE_SIZE_THRESHOLD) {
+ // quote_container.set_attribute("class", "quote_container");
+ // }
+ // }
+ // }
+
+ private string clean_html_markup(string text, Geary.RFC822.Message message, out bool remote_images) {
+ remote_images = false;
+ try {
+ string inner_text = text;
+
+ // If email HTML has a BODY, use only that
+ GLib.Regex body_regex = new GLib.Regex("<body([^>]*)>(.*)</body>",
+ GLib.RegexCompileFlags.DOTALL);
+ GLib.MatchInfo matches;
+ if (body_regex.match(text, 0, out matches)) {
+ inner_text = matches.fetch(2);
+ string attrs = matches.fetch(1);
+ if (attrs != "")
+ inner_text = @"<div$attrs>$inner_text</div>";
+ }
+
+ // Create a workspace for manipulating the HTML.
+ WebKit.DOM.HTMLElement container = web_view.create_div();
+ container.set_inner_html(inner_text);
+
+ // Get all the top level block quotes and stick them into a hide/show controller.
+ WebKit.DOM.NodeList blockquote_list = container.query_selector_all("blockquote");
+ for (int i = 0; i < blockquote_list.length; ++i) {
+ // Get the nodes we need.
+ WebKit.DOM.Node blockquote_node = blockquote_list.item(i);
+ WebKit.DOM.Node? next_sibling = blockquote_node.get_next_sibling();
+ WebKit.DOM.Node parent = blockquote_node.get_parent_node();
+
+ // Make sure this is a top level blockquote.
+ if (node_is_child_of(blockquote_node, "BLOCKQUOTE")) {
+ continue;
+ }
+
+ // parent
+ // quote_container
+ // blockquote
+ // sibling
+ WebKit.DOM.Element quote_container = create_quote_container();
+ Util.DOM.select(quote_container, ".quote").append_child(blockquote_node);
+ if (next_sibling == null) {
+ parent.append_child(quote_container);
+ } else {
+ parent.insert_before(quote_container, next_sibling);
+ }
+ }
+
+ // Now look for the signature.
+ wrap_html_signature(ref container);
+
+ // Then look for all <img> tags. Inline images are replaced with
+ // data URLs.
+ WebKit.DOM.NodeList inline_list = container.query_selector_all("img");
+ 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);
+ string? src = img.get_attribute("src");
+ if (Geary.String.is_empty(src))
+ continue;
+
+ // if no Content-ID, then leave as-is, but note if a non-data: URI is being used for
+ // purposes of detecting remote images
+ string? content_id = src.has_prefix("cid:") ? src.substring(4) : null;
+ if (Geary.String.is_empty(content_id)) {
+ remote_images = remote_images || !src.has_prefix("data:");
+
+ continue;
+ }
+
+ // if image has a Content-ID and it's already been replaced by the image replacer,
+ // drop this tag, otherwise fix up this one with the Base-64 data URI of the image
+ if (!replaced_content_ids.contains(content_id)) {
+ string? filename = message.get_content_filename_by_mime_id(content_id);
+ Geary.Memory.Buffer image_content = message.get_content_by_mime_id(content_id);
+ Geary.Memory.UnownedBytesBuffer? unowned_buffer =
+ image_content as Geary.Memory.UnownedBytesBuffer;
+
+ // Get the content type.
+ string guess;
+ if (unowned_buffer != null)
+ guess = ContentType.guess(null, unowned_buffer.to_unowned_uint8_array(), null);
+ else
+ guess = ContentType.guess(null, image_content.get_uint8_array(), null);
+
+ string mimetype = ContentType.get_mime_type(guess);
+
+ // Replace the SRC to a data URI, the class to a known label for the popup menu,
+ // and the ALT to its filename, if supplied
+ img.remove_attribute("src"); // Work around a WebKitGTK+ crash. Bug 764152
+ img.set_attribute("src", assemble_data_uri(mimetype, image_content));
+ img.set_attribute("class", DATA_IMAGE_CLASS);
+ if (!Geary.String.is_empty(filename))
+ img.set_attribute("alt", filename);
+
+ // stash here so inlined image isn't listed as attachment (esp. if it has no
+ // Content-Disposition)
+ inlined_content_ids.add(content_id);
+ } else {
+ // replaced by data: URI, remove this tag and let the inserted one shine through
+ img.parent_element.remove_child(img);
+ }
+ }
+
+ // Remove any inline images that were referenced through Content-ID
+ foreach (string cid in inlined_content_ids) {
+ try {
+ string escaped_cid = Geary.HTML.escape_markup(cid);
+ WebKit.DOM.Element? img = container.query_selector(@"[cid='$escaped_cid']");
+ if (img != null)
+ img.parent_element.remove_child(img);
+ } catch (Error error) {
+ debug("Error removing inlined image: %s", error.message);
+ }
+ }
+
+ // Now return the whole message.
+ return container.get_inner_html();
+ } catch (Error e) {
+ debug("Error modifying HTML message: %s", e.message);
+ return text;
+ }
+ }
+
+ private void wrap_html_signature(ref WebKit.DOM.HTMLElement container) throws Error {
+ // Most HTML signatures fall into one of these designs which are handled by this method:
+ //
+ // 1. GMail: <div>-- </div>$SIGNATURE
+ // 2. GMail Alternate: <div><span>-- </span></div>$SIGNATURE
+ // 3. Thunderbird: <div>-- <br>$SIGNATURE</div>
+ //
+ WebKit.DOM.NodeList div_list = container.query_selector_all("div,span,p");
+ int i = 0;
+ Regex sig_regex = new Regex("^--\\s*$");
+ Regex alternate_sig_regex = new Regex("^--\\s*(?:<br|\\R)");
+ for (; i < div_list.length; ++i) {
+ // Get the div and check that it starts a signature block and is not inside a quote.
+ WebKit.DOM.HTMLElement div = div_list.item(i) as WebKit.DOM.HTMLElement;
+ string inner_html = div.get_inner_html();
+ if ((sig_regex.match(inner_html) || alternate_sig_regex.match(inner_html)) &&
+ !node_is_child_of(div, "BLOCKQUOTE")) {
+ break;
+ }
+ }
+
+ // If we have a signature, move it and all of its following siblings that are not quotes
+ // inside a signature div.
+ if (i == div_list.length) {
+ return;
+ }
+ WebKit.DOM.Node elem = div_list.item(i) as WebKit.DOM.Node;
+ WebKit.DOM.Element parent = elem.get_parent_element();
+ WebKit.DOM.HTMLElement signature_container = web_view.create_div();
+ signature_container.set_attribute("class", "signature");
+ do {
+ // Get its sibling _before_ we move it into the signature div.
+ WebKit.DOM.Node? sibling = elem.get_next_sibling();
+ signature_container.append_child(elem);
+ elem = sibling;
+ } while (elem != null);
+ parent.append_child(signature_container);
+ }
+
+ // 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 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 void insert_attachments(WebKit.DOM.HTMLElement email_container,
+ // Gee.List<Geary.Attachment> attachments) {
+
+ // // <div class="attachment_container">
+ // // <div class="top_border"></div>
+ // // <table class="attachment" data-attachment-id="">
+ // // <tr>
+ // // <td class="preview">
+ // // <img src="" />
+ // // </td>
+ // // <td class="info">
+ // // <div class="filename"></div>
+ // // <div class="filesize"></div>
+ // // </td>
+ // // </tr>
+ // // </table>
+ // // </div>
+
+ // try {
+ // // Prepare the dom for our attachments.
+ // WebKit.DOM.Document document = web_view.get_dom_document();
+ // WebKit.DOM.HTMLElement attachment_container =
+ // Util.DOM.clone_select(document, "#attachment_template");
+ // WebKit.DOM.HTMLElement attachment_template =
+ // Util.DOM.select(attachment_container, ".attachment");
+ // attachment_container.remove_attribute("id");
+ // attachment_container.remove_child(attachment_template);
+
+ // // Create an attachment table for each attachment.
+ // foreach (Geary.Attachment attachment in attachments) {
+ // if (!should_show_attachment(attachment)) {
+ // continue;
+ // }
+ // // Generate the attachment table.
+ // WebKit.DOM.HTMLElement attachment_table = Util.DOM.clone_node(attachment_template);
+ // string filename = !attachment.has_supplied_filename ? _("none") :
attachment.file.get_basename();
+ // Util.DOM.select(attachment_table, ".info .filename")
+ // .set_inner_text(filename);
+ // Util.DOM.select(attachment_table, ".info .filesize")
+ // .set_inner_text(Files.get_filesize_as_string(attachment.filesize));
+ // attachment_table.set_attribute("data-attachment-id", attachment.id);
+
+ // // Set the image preview and insert it into the container.
+ // WebKit.DOM.HTMLImageElement img =
+ // Util.DOM.select(attachment_table, ".preview img") as WebKit.DOM.HTMLImageElement;
+ // web_view.set_attachment_src(img, attachment.content_type, attachment.file.get_path(),
+ // ATTACHMENT_PREVIEW_SIZE);
+ // attachment_container.append_child(attachment_table);
+ // }
+
+ // // Append the attachments to the email.
+ // email_container.append_child(attachment_container);
+ // } catch (Error error) {
+ // debug("Failed to insert attachments: %s", error.message);
+ // }
+ // }
+
+ // private bool in_drafts_folder() {
+ // return containing_folder.special_folder_type == Geary.SpecialFolderType.DRAFTS;
+ // }
+
+ private void toggle_class(string cls) {
+ Gtk.StyleContext context = get_style_context();
+ if (context.has_class(cls)) {
+ context.add_class(cls);
+ } else {
+ context.remove_class(cls);
+ }
+
+ }
+
+ private static bool is_content_type_supported_inline(Geary.Mime.ContentType content_type) {
+ foreach (string mime_type in INLINE_MIME_TYPES) {
+ try {
+ if (content_type.is_mime_type(mime_type))
+ return true;
+ } catch (Error err) {
+ debug("Unable to compare MIME type %s: %s", mime_type, err.message);
+ }
+ }
+
+ return false;
+ }
+
+ // private void on_hovering_over_link(string? title, string? url) {
+ // // Copy the link the user is hovering over. Note that when the user mouses-out,
+ // // this signal is called again with null for both parameters.
+ // hover_url = url != null ? Uri.unescape_string(url) : null;
+
+ // if (message_overlay_label == null) {
+ // if (url == null)
+ // return;
+ // build_message_overlay_label(Uri.unescape_string(url));
+ // message_overlay_label.show();
+ // return;
+ // }
+
+ // if (url == null) {
+ // message_overlay_label.hide();
+ // message_overlay_label.label = null;
+ // } else {
+ // message_overlay_label.show();
+ // message_overlay_label.label = Uri.unescape_string(url);
+ // }
+ // }
+
+ // private void on_copy_text() {
+ // web_view.copy_clipboard();
+ // }
+
+ // private void on_copy_link() {
+ // // Put the current link in clipboard.
+ // Gtk.Clipboard clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD);
+ // clipboard.set_text(hover_url, -1);
+ // clipboard.store();
+ // }
+
+ // private void on_copy_email_address() {
+ // // Put the current email address in clipboard.
+ // Gtk.Clipboard clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD);
+ // if (hover_url.has_prefix(Geary.ComposedEmail.MAILTO_SCHEME))
+ // clipboard.set_text(hover_url.substring(Geary.ComposedEmail.MAILTO_SCHEME.length, -1), -1);
+ // else
+ // clipboard.set_text(hover_url, -1);
+ // clipboard.store();
+ // }
+
+ // private void on_select_all() {
+ // web_view.select_all();
+ // }
+
+ // private void on_select_message(WebKit.DOM.Element email_element) {
+ // try {
+ //
web_view.get_dom_document().get_default_view().get_selection().select_all_children(email_element);
+ // } catch (Error error) {
+ // 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/ui/CMakeLists.txt b/ui/CMakeLists.txt
index 7133c51..7015d3b 100644
--- a/ui/CMakeLists.txt
+++ b/ui/CMakeLists.txt
@@ -8,6 +8,8 @@ set(RESOURCE_LIST
STRIPBLANKS "certificate_warning_dialog.glade"
STRIPBLANKS "composer.glade"
STRIPBLANKS "composer_accelerators.ui"
+ STRIPBLANKS "conversation-message.ui"
+ STRIPBLANKS "conversation-message-menu.ui"
STRIPBLANKS "edit_alternate_emails.glade"
STRIPBLANKS "find_bar.glade"
STRIPBLANKS "login.glade"
diff --git a/ui/conversation-message-menu.ui b/ui/conversation-message-menu.ui
new file mode 100644
index 0000000..022af36
--- /dev/null
+++ b/ui/conversation-message-menu.ui
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<interface>
+ <menu id="conversation_message_menu">
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">_Save Attachments</attribute>
+ <attribute name="action">msg.selected</attribute>
+ </item>
+ </section>
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">_Reply</attribute>
+ <attribute name="action">msg.reply_sender</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">Reply to _All</attribute>
+ <attribute name="action">msg.reply_all</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">_Forward</attribute>
+ <attribute name="action">msg.forward</attribute>
+ </item>
+ </section>
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">_Mark Read</attribute>
+ <attribute name="action">msg.mark_read</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">_Mark Unread</attribute>
+ <attribute name="action">msg.mark_unread</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">Mark Unread From _Here</attribute>
+ <attribute name="action">msg.mark_unread_down</attribute>
+ </item>
+ </section>
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">_Print...</attribute>
+ <attribute name="action">msg.print</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">_View Source</attribute>
+ <attribute name="action">msg.view_source</attribute>
+ </item>
+ </section>
+ </menu>
+</interface>
diff --git a/ui/conversation-message.ui b/ui/conversation-message.ui
new file mode 100644
index 0000000..0b1eb16
--- /dev/null
+++ b/ui/conversation-message.ui
@@ -0,0 +1,439 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface>
+ <requires lib="gtk+" version="3.10"/>
+ <template class="ConversationMessage" parent="GtkBox">
+ <property name="name">ConversationMessage</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">8</property>
+ <child>
+ <object class="GtkImage" id="avatar_image">
+ <property name="width_request">18</property>
+ <property name="height_request">18</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="pixel_size">24</property>
+ <property name="icon_name">avatar-default-symbolic</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="header_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="from_header">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkRevealer" id="from_revealer">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="transition_type">slide-right</property>
+ <child>
+ <object class="GtkLabel" id="from_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">From:</property>
+ <property name="xalign">1</property>
+ <style>
+ <class name="header-label"/>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="from_text">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">From <email></property>
+ <property name="ellipsize">end</property>
+ <style>
+ <class name="header-value"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="to_header">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkLabel" id="to_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">To:</property>
+ <property name="xalign">1</property>
+ <style>
+ <class name="header-label"/>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="to_text">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">To <email></property>
+ <property name="ellipsize">end</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <style>
+ <class name="empty"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="cc_header">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkLabel" id="cc_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Cc:</property>
+ <property name="xalign">1</property>
+ <style>
+ <class name="header-label"/>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="cc_text">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">CC <email></property>
+ <property name="ellipsize">end</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <style>
+ <class name="empty"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="bcc_header">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkLabel" id="bcc_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Bcc:</property>
+ <property name="xalign">1</property>
+ <style>
+ <class name="header-label"/>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="bcc_text">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">BCC <email></property>
+ <property name="ellipsize">end</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <style>
+ <class name="empty"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="subject_header">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkLabel" id="subject_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Subject:</property>
+ <property name="xalign">1</property>
+ <style>
+ <class name="header-label"/>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="subject_text">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Subject</property>
+ <property name="ellipsize">end</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <style>
+ <class name="empty"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="date_header">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkLabel" id="date_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Date:</property>
+ <property name="xalign">1</property>
+ <style>
+ <class name="header-label"/>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="date_text">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">1/1/1970 </property>
+ <property name="ellipsize">end</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <style>
+ <class name="empty"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="preview_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Preview body text.</property>
+ <property name="ellipsize">end</property>
+ <style>
+ <class name="preview-value"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">6</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">1</property>
+ <child>
+ <object class="GtkButton" id="flag_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="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">non-starred-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</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="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">1</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>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="body_revealer">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkBox" id="body_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">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <style>
+ <class name="view"/>
+ </style>
+ </template>
+ <object class="GtkSizeGroup" id="header_labels_sizegroup">
+ <property name="ignore_hidden">True</property>
+ <widgets>
+ <widget name="from_label"/>
+ <widget name="to_label"/>
+ <widget name="cc_label"/>
+ <widget name="bcc_label"/>
+ <widget name="subject_label"/>
+ <widget name="date_label"/>
+ </widgets>
+ </object>
+</interface>
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]