~ubuntu-branches/ubuntu/saucy/geary/saucy-updates

« back to all changes in this revision

Viewing changes to src/client/views/conversation-viewer.vala

  • Committer: Package Import Robot
  • Author(s): Sebastien Bacher
  • Date: 2013-03-14 13:48:23 UTC
  • mfrom: (1.1.3)
  • Revision ID: package-import@ubuntu.com-20130314134823-gyk5av1g508zyj8a
Tags: 0.3.0~pr1-0ubuntu1
New upstream version (FFE lp: #1154316), supports multiple account as
well as full conversation views with inline replies

Show diffs side-by-side

added added

removed removed

Lines of Context:
4
4
 * (version 2.1 or later).  See the COPYING file in this distribution. 
5
5
 */
6
6
 
7
 
public class ConversationViewer : Object {
 
7
public class ConversationViewer : Gtk.Box {
8
8
    public const Geary.Email.Field REQUIRED_FIELDS =
9
9
        Geary.Email.Field.HEADER
10
10
        | Geary.Email.Field.BODY
15
15
        | Geary.Email.Field.FLAGS
16
16
        | Geary.Email.Field.PREVIEW;
17
17
    
18
 
    public const string USER_CSS = "user-message.css";
19
 
    
20
18
    private const int ATTACHMENT_PREVIEW_SIZE = 50;
21
19
    private const string MESSAGE_CONTAINER_ID = "message_container";
22
20
    private const string SELECTION_COUNTER_ID = "multiple_messages";
23
 
    private const string STYLE_NAME = "STYLE";
24
 
    
25
 
    private const string[] always_loaded_prefixes = {
26
 
        "http://www.gravatar.com/avatar/",
27
 
        "data:"
28
 
    };
29
21
    
30
22
    // Fired when the user clicks a link.
31
23
    public signal void link_selected(string link);
52
44
    public Gee.TreeSet<Geary.Email> messages { get; private set; default = 
53
45
        new Gee.TreeSet<Geary.Email>((CompareFunc<Geary.Email>) Geary.Email.compare_date_ascending); }
54
46
    
55
 
    // The content area contains all of the message-viewer's widgets.
56
 
    public Gtk.Widget content_area { get; private set; }
57
 
    
58
47
    // The HTML viewer to view the emails.
59
 
    public WebKit.WebView web_view { get; private set; }
 
48
    public ConversationWebView web_view { get; private set; }
60
49
    
61
50
    // The Info Bar to be shown when an external image is blocked.
62
51
    public Gtk.InfoBar external_images_info_bar { get; private set; }
63
52
    
64
 
    // HTML element that contains message DIVs.
65
 
    private WebKit.DOM.HTMLDivElement container;
66
 
    
67
53
    // Label for displaying overlay messages.
68
54
    private Gtk.Label message_overlay_label;
69
55
    
76
62
    private Gtk.Menu? context_menu = null;
77
63
    private Gtk.Menu? message_menu = null;
78
64
    private Gtk.Menu? attachment_menu = null;
79
 
    private FileMonitor? user_style_monitor = null;
80
65
    private weak Geary.Folder? current_folder = null;
81
 
    private Geary.AccountSettings? current_settings = null;
82
 
    private bool load_external_images = false;
 
66
    private Geary.AccountInformation? current_account_information = null;
83
67
    
84
68
    public ConversationViewer() {
85
 
        Gtk.Box box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
 
69
        Object(orientation: Gtk.Orientation.VERTICAL, spacing: 0);
86
70
        
87
71
        external_images_info_bar = new Gtk.InfoBar.with_buttons(
88
72
            _("_Show Images"), Gtk.ResponseType.OK, _("_Cancel"), Gtk.ResponseType.CANCEL);
93
77
            external_images_info_bar.get_content_area() as Gtk.Box;
94
78
        if (external_images_info_bar_content_area != null) {
95
79
            Gtk.Label label = new Gtk.Label(_("This message contains images. Do you want to show them?"));
 
80
            label.set_line_wrap(true);
96
81
            external_images_info_bar_content_area.add(label);
97
82
            label.show_all();
98
83
        }
99
 
        box.pack_start(external_images_info_bar, false, false);
100
 
    
 
84
        pack_start(external_images_info_bar, false, false);
 
85
        
101
86
        web_view = new ConversationWebView();
102
87
        
103
 
        web_view.set_border_width(0);
104
 
        
105
 
        web_view.navigation_policy_decision_requested.connect(on_navigation_policy_decision_requested);
106
 
        web_view.new_window_policy_decision_requested.connect(on_navigation_policy_decision_requested);
107
88
        web_view.hovering_over_link.connect(on_hovering_over_link);
108
 
        web_view.resource_request_starting.connect(on_resource_request_starting);
109
 
        
110
 
        WebKit.WebSettings settings = new WebKit.WebSettings();
111
 
        settings.enable_default_context_menu = false;
112
 
        settings.enable_scripts = false;
113
 
        settings.enable_java_applet = false;
114
 
        settings.enable_plugins = false;
115
 
        web_view.settings = settings;
116
 
        
117
 
        // Load the HTML into WebKit.
118
 
        web_view.load_finished.connect(on_load_finished);
119
 
        string html_text = GearyApplication.instance.read_theme_file("message-viewer.html") ?? "";
120
 
        web_view.load_string(html_text, "text/html", "UTF8", "");
 
89
        web_view.realize.connect( () => { web_view.get_vadjustment().value_changed.connect(mark_read); });
 
90
        web_view.size_allocate.connect(mark_read);
 
91
 
 
92
        web_view.image_load_requested.connect(on_image_load_requested);
 
93
        web_view.link_selected.connect((link) => { link_selected(link); });
121
94
        
122
95
        Gtk.ScrolledWindow conversation_viewer_scrolled = new Gtk.ScrolledWindow(null, null);
123
96
        conversation_viewer_scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
132
105
        message_overlay_label.valign = Gtk.Align.END;
133
106
        message_overlay.add_overlay(message_overlay_label);
134
107
        
135
 
        box.pack_start(message_overlay);
136
 
        content_area = box;
 
108
        pack_start(message_overlay);
 
109
    }
 
110
    
 
111
    private void on_image_load_requested() {
 
112
        external_images_info_bar.show();
137
113
    }
138
114
    
139
115
    private void on_external_images_info_bar_response(Gtk.InfoBar sender, int response_id) {
140
 
        set_load_external_images(response_id == Gtk.ResponseType.OK);
 
116
        web_view.apply_load_external_images(response_id == Gtk.ResponseType.OK);
141
117
        sender.hide();
142
118
    }
143
119
    
144
 
    private void on_load_finished(WebKit.WebFrame frame) {
145
 
        // Load the style.
146
 
        try {
147
 
            WebKit.DOM.Document document = web_view.get_dom_document();
148
 
            WebKit.DOM.Element style_element = document.create_element(STYLE_NAME);
149
 
 
150
 
            string css_text = GearyApplication.instance.read_theme_file("message-viewer.css") ?? "";
151
 
            WebKit.DOM.Text text_node = document.create_text_node(css_text);
152
 
            style_element.append_child(text_node);
153
 
 
154
 
            WebKit.DOM.HTMLHeadElement head_element = document.get_head();
155
 
            head_element.append_child(style_element);
156
 
        } catch (Error error) {
157
 
            debug("Unable to load message-viewer document from files: %s", error.message);
158
 
        }
159
 
 
160
 
        load_user_style();
161
 
 
162
 
        // Grab the HTML container.
163
 
        WebKit.DOM.Element? _container = web_view.get_dom_document().get_element_by_id("message_container");
164
 
        assert(_container != null);
165
 
        container = _container as WebKit.DOM.HTMLDivElement;
166
 
        assert(container != null);
167
 
 
168
 
        // Load the icons.
169
 
        set_icon_src("#email_template .menu .icon", "go-down");
170
 
        set_icon_src("#email_template .starred .icon", "starred");
171
 
        set_icon_src("#email_template .unstarred .icon", "non-starred-grey");
172
 
        set_icon_src("#email_template .attachment.icon", "mail-attachment");
173
 
    }
174
 
    
175
 
    private void on_resource_request_starting(WebKit.WebFrame web_frame,
176
 
        WebKit.WebResource web_resource, WebKit.NetworkRequest request,
177
 
        WebKit.NetworkResponse? response) {
178
 
        
179
 
        string? uri = request.get_uri();
180
 
        bool uri_is_image = is_image(uri);
181
 
        if (uri_is_image && !load_external_images)
182
 
            external_images_info_bar.show();
183
 
        if (!is_always_loaded(uri) && !(uri_is_image && load_external_images))
184
 
            request.set_uri("about:blank");
185
 
    }
186
 
    
187
 
    private bool is_always_loaded(string? uri) {
188
 
        if (uri == null)
189
 
            return false;
190
 
        
191
 
        foreach (string prefix in always_loaded_prefixes) {
192
 
            if (uri.has_prefix(prefix))
193
 
                return true;
194
 
        }
195
 
        
196
 
        return false;
197
 
    }
198
 
    
199
 
    private bool is_image(string? uri) {
200
 
        if (uri == null)
201
 
            return false;
202
 
        
203
 
        try {
204
 
            Regex regex = new Regex("(?:jpe?g|gif|png)$", RegexCompileFlags.CASELESS);
205
 
            return regex.match(uri);
206
 
        } catch (RegexError err) {
207
 
            debug("Error creating image-matching regex: %s", err.message);
208
 
            return false;
209
 
        }
210
 
    }
211
 
    
212
 
    private void set_load_external_images(bool load_external_images) {
213
 
        this.load_external_images = load_external_images;
214
 
        
215
 
        // Refreshing the images would do nothing in this case--the resource has already been
216
 
        // loaded, so no additional resource request will be sent.
217
 
        if (load_external_images == false)
218
 
            return;
219
 
        
220
 
        // We can't simply set load_external_images to true before refreshing, then set it back to
221
 
        // false afterwards. If one of the images' sources is redirected, an additional resource
222
 
        // request will come after we reset load_external_images to false.
223
 
        try {
224
 
            WebKit.DOM.Document document = web_view.get_dom_document();
225
 
            WebKit.DOM.NodeList nodes = document.query_selector_all("img");
226
 
            for (ulong i = 0; i < nodes.length; i++) {
227
 
                WebKit.DOM.Element? element = nodes.item(i) as WebKit.DOM.Element;
228
 
                if (element == null)
229
 
                    continue;
230
 
                
231
 
                if (!element.has_attribute("src"))
232
 
                    continue;
233
 
                
234
 
                string src = element.get_attribute("src");
235
 
                if (Geary.String.is_empty_or_whitespace(src) || is_always_loaded(src))
236
 
                    continue;
237
 
                
238
 
                // Refresh the image source. Requests are denied when load_external_images
239
 
                // is false, so we need to force webkit to send the request again.
240
 
                element.set_attribute("src", src);
241
 
            }
242
 
        } catch (Error err) {
243
 
            debug("Error refreshing images: %s", err.message);
244
 
        }
245
 
    }
246
 
 
247
 
    private void load_user_style() {
248
 
        try {
249
 
            WebKit.DOM.Document document = web_view.get_dom_document();
250
 
            WebKit.DOM.Element style_element = document.create_element(STYLE_NAME);
251
 
            style_element.set_attribute("id", "user_style");
252
 
            WebKit.DOM.HTMLHeadElement head_element = document.get_head();
253
 
            head_element.append_child(style_element);
254
 
            
255
 
            File user_style = GearyApplication.instance.get_user_config_directory().get_child(USER_CSS);
256
 
            user_style_monitor = user_style.monitor_file(FileMonitorFlags.NONE, null);
257
 
            user_style_monitor.changed.connect(on_user_style_changed);
258
 
            
259
 
            // And call it once to load the initial user style
260
 
            on_user_style_changed(user_style, null, FileMonitorEvent.CREATED);
261
 
        } catch (Error error) {
262
 
            debug("Error setting up user style: %s", error.message);
263
 
        }
264
 
    }
265
 
 
266
 
    private void on_user_style_changed(File user_style, File? other_file, FileMonitorEvent event_type) {
267
 
        // Changing a file produces 1 created signal, 3 changes done hints, and 0 changed
268
 
        if (event_type != FileMonitorEvent.CHANGED && event_type != FileMonitorEvent.CREATED
269
 
            && event_type != FileMonitorEvent.DELETED) {
270
 
            return;
271
 
        }
272
 
        
273
 
        debug("Loading new message viewer style from %s...", user_style.get_path());
274
 
        
275
 
        WebKit.DOM.Document document = web_view.get_dom_document();
276
 
        WebKit.DOM.Element style_element = document.get_element_by_id("user_style");
277
 
        ulong n = style_element.child_nodes.length;
278
 
        try {
279
 
            for (int i = 0; i < n; i++)
280
 
                style_element.remove_child(style_element.first_child);
281
 
        } catch (Error error) {
282
 
            debug("Error removing old user style: %s", error.message);
283
 
        }
284
 
        
285
 
        try {
286
 
            DataInputStream data_input_stream = new DataInputStream(user_style.read());
287
 
            size_t length;
288
 
            string user_css = data_input_stream.read_upto("\0", 1, out length);
289
 
            WebKit.DOM.Text text_node = document.create_text_node(user_css);
290
 
            style_element.append_child(text_node);
291
 
        } catch (Error error) {
292
 
            // Expected if file was deleted.
293
 
        }
294
 
    }
295
 
 
296
 
    private void set_icon_src(string selector, string icon_name) {
297
 
        try {
298
 
            // Load the icon.
299
 
            string icon_filename = IconFactory.instance.lookup_icon(icon_name, 16).get_filename();
300
 
            uint8[] icon_content;
301
 
            FileUtils.get_data(icon_filename, out icon_content);
302
 
 
303
 
            // Fetch its mime type.
304
 
            bool uncertain_content_type;
305
 
            string icon_mimetype = ContentType.get_mime_type(ContentType.guess(icon_filename,
306
 
                icon_content, out uncertain_content_type));
307
 
 
308
 
            // Then set the source to a data url.
309
 
            WebKit.DOM.HTMLImageElement img = Util.DOM.select(web_view.get_dom_document(), selector)
310
 
                as WebKit.DOM.HTMLImageElement;
311
 
            set_data_url(img, icon_mimetype, icon_content);
312
 
        } catch (Error error) {
313
 
            warning("Failed to load icon '%s': %s", icon_name, error.message);
314
 
        }
315
 
    }
316
 
 
317
 
    private void set_image_src(WebKit.DOM.HTMLImageElement img, string mime_type, string filename,
318
 
        int maxwidth, int maxheight = -1) {
319
 
        if( maxheight == -1 ){
320
 
            maxheight = maxwidth;
321
 
        }
322
 
 
323
 
        try {
324
 
            // If the file is an image, use it. Otherwise get the icon for this mime_type.
325
 
            uint8[] content;
326
 
            string content_type = ContentType.from_mime_type(mime_type);
327
 
            string icon_mime_type = mime_type;
328
 
            if (mime_type.has_prefix("image/")) {
329
 
                // Get a thumbnail for the image.
330
 
                // TODO Generate and save the thumbnail when extracting the attachments rather than
331
 
                // when showing them in the viewer.
332
 
                img.get_class_list().add("thumbnail");
333
 
                Gdk.Pixbuf image = new Gdk.Pixbuf.from_file_at_scale(filename, maxwidth, maxheight,
334
 
                    true);
335
 
                image.save_to_buffer(out content, "png");
336
 
                icon_mime_type = "image/png";
337
 
            } else {
338
 
                // Load the icon for this mime type.
339
 
                ThemedIcon icon = ContentType.get_icon(content_type) as ThemedIcon;
340
 
                string icon_filename = IconFactory.instance.lookup_icon(icon.names[0], maxwidth)
341
 
                    .get_filename();
342
 
                FileUtils.get_data(icon_filename, out content);
343
 
                icon_mime_type = ContentType.get_mime_type(ContentType.guess(icon_filename, content,
344
 
                    null));
345
 
            }
346
 
 
347
 
            // Then set the source to a data url.
348
 
            set_data_url(img, icon_mime_type, content);
349
 
        } catch (Error error) {
350
 
            warning("Failed to load image '%s': %s", filename, error.message);
351
 
        }
352
 
    }
353
 
 
354
 
    private void set_data_url(WebKit.DOM.HTMLImageElement img, string mime_type, uint8[] content)
355
 
        throws Error {
356
 
        img.set_attribute("src", "data:%s;base64,%s".printf(mime_type, Base64.encode(content)));
357
 
    }
358
 
    
359
120
    public Geary.Email? get_last_message() {
360
121
        return messages.is_empty ? null : messages.last();
361
122
    }
362
123
    
363
124
    // Removes all displayed e-mails from the view.
364
 
    public void clear(Geary.Folder? new_folder, Geary.AccountSettings? settings) {
 
125
    public void clear(Geary.Folder? new_folder, Geary.AccountInformation? account_information) {
365
126
        // Remove all messages from DOM.
366
127
        try {
367
128
            foreach (WebKit.DOM.HTMLElement element in email_to_element.values) {
375
136
        messages.clear();
376
137
        
377
138
        current_folder = new_folder;
378
 
        current_settings = settings;
 
139
        current_account_information = account_information;
379
140
    }
380
141
    
381
142
    // Converts an email ID into HTML ID used by the <div> for the email.
383
144
        return "message_%s".printf(id.to_string());
384
145
    }
385
146
    
386
 
    private void hide_element_by_id(string element_id) throws Error {
387
 
        web_view.get_dom_document().get_element_by_id(element_id).set_attribute("style", "display:none");
388
 
    }
389
 
 
390
 
    private void show_element_by_id(string element_id) throws Error {
391
 
        web_view.get_dom_document().get_element_by_id(element_id).set_attribute("style", "display:block");
392
 
    }
393
 
    
394
147
    public void show_multiple_selected(uint selected_count) {
395
148
        // Remove any messages and hide the message container, then show the counter.
396
 
        clear(current_folder, current_settings);
 
149
        clear(current_folder, current_account_information);
397
150
        try {
398
 
            hide_element_by_id(MESSAGE_CONTAINER_ID);
399
 
            show_element_by_id(SELECTION_COUNTER_ID);
 
151
            web_view.hide_element_by_id(MESSAGE_CONTAINER_ID);
 
152
            web_view.show_element_by_id(SELECTION_COUNTER_ID);
400
153
            
401
154
            // Update the counter's count.
402
155
            WebKit.DOM.HTMLElement counter =
412
165
    }
413
166
    
414
167
    public void add_message(Geary.Email email) {
415
 
        set_load_external_images(false);
 
168
        web_view.apply_load_external_images(false);
416
169
        
417
170
        // Make sure the message container is showing and the multi-message counter hidden.
418
171
        try {
419
 
            show_element_by_id(MESSAGE_CONTAINER_ID);
420
 
            hide_element_by_id(SELECTION_COUNTER_ID);
 
172
            web_view.show_element_by_id(MESSAGE_CONTAINER_ID);
 
173
            web_view.hide_element_by_id(SELECTION_COUNTER_ID);
421
174
        } catch (Error e) {
422
175
            debug("Error showing/hiding containers: %s", e.message);
423
176
        }
428
181
        string message_id = get_div_id(email.id);
429
182
        string header = "";
430
183
        
431
 
        WebKit.DOM.Node insert_before = container.get_last_child();
 
184
        WebKit.DOM.Node insert_before = web_view.container.get_last_child();
432
185
        
433
186
        messages.add(email);
434
187
        Geary.Email? higher = messages.higher(email);
463
216
            // </div>
464
217
            div_message = Util.DOM.clone_select(web_view.get_dom_document(), "#email_template");
465
218
            div_message.set_attribute("id", message_id);
466
 
            container.insert_before(div_message, insert_before);
 
219
            web_view.container.insert_before(div_message, insert_before);
467
220
            div_email_container = Util.DOM.select(div_message, "div.email_container");
468
221
            if (email.is_unread() == Geary.Trillian.FALSE) {
469
222
                div_message.get_class_list().add("hide");
479
232
        insert_header_address(ref header, _("From:"), email.from != null ? email.from : email.sender,
480
233
            true);
481
234
        
482
 
        // Only include to string if it's not just this account.
483
 
        // TODO: multiple accounts.
484
 
        if (email.to != null && current_settings != null) {
485
 
            if (!(email.to.get_all().size == 1 && email.to.get_all().get(0).address == current_settings.email.address))
486
 
                 insert_header_address(ref header, _("To:"), email.to);
487
 
        }
488
 
 
 
235
        if (email.to != null)
 
236
             insert_header_address(ref header, _("To:"), email.to);
 
237
        
489
238
        if (email.cc != null) {
490
239
            insert_header_address(ref header, _("Cc:"), email.cc);
491
240
        }
509
258
                icon.set_attribute("src",
510
259
                    Gravatar.get_image_uri(primary, Gravatar.Default.MYSTERY_MAN, 48));
511
260
            } catch (Error error) {
512
 
                warning("Failed to load avatar: %s", error.message);
 
261
                debug("Failed to inject avatar URL: %s", error.message);
513
262
            }
514
263
        }
515
264
        
553
302
            warning("Error setting HTML for message: %s", html_error.message);
554
303
        }
555
304
 
556
 
        // Add the attachments container if we have any attachments.
 
305
        // Set attachment icon and add the attachments container if we have any attachments.
 
306
        set_attachment_icon(div_message, email.attachments.size > 0);
557
307
        if (email.attachments.size > 0) {
558
308
            insert_attachments(div_message, email.attachments);
559
309
        }
560
310
 
561
311
        // Add classes according to the state of the email.
562
312
        update_flags(email);
563
 
 
 
313
        
 
314
        // Add animation class after other classes set, to avoid initial animation.
 
315
        Idle.add(() => {
 
316
            try {
 
317
                div_message.get_class_list().add("animate");
 
318
            } catch (Error error) {
 
319
                debug("Could not enable animation class: %s", error.message);
 
320
            }
 
321
            return false;
 
322
        });
 
323
        
564
324
        // Attach to the click events for hiding/showing quotes, opening the menu, and so forth.
565
325
        bind_event(web_view, ".email", "contextmenu", (Callback) on_context_menu, this);
566
326
        bind_event(web_view, ".quote_container > .hider", "click", (Callback) on_hide_quote_clicked);
570
330
        bind_event(web_view, ".email_container .unstarred", "click", (Callback) on_star_clicked, this);
571
331
        bind_event(web_view, ".header .field .value", "click", (Callback) on_value_clicked, this);
572
332
        bind_event(web_view, ".email .header_container", "click", (Callback) on_body_toggle_clicked, this);
 
333
        bind_event(web_view, ".email .compressed_note", "click", (Callback) on_body_toggle_clicked, this);
573
334
        bind_event(web_view, ".attachment_container .attachment", "click", (Callback) on_attachment_clicked, this);
574
335
        bind_event(web_view, ".attachment_container .attachment", "contextmenu", (Callback) on_attachment_menu, this);
575
336
    }
576
 
        
 
337
    
577
338
    public void unhide_last_email() {
578
 
        WebKit.DOM.HTMLElement last_email = (WebKit.DOM.HTMLElement) container.get_last_child().previous_sibling;
 
339
        WebKit.DOM.HTMLElement last_email = (WebKit.DOM.HTMLElement) web_view.container.get_last_child().previous_sibling;
579
340
        if (last_email != null) {
580
341
            WebKit.DOM.DOMTokenList class_list = last_email.get_class_list();
581
342
            try {
586
347
        }
587
348
    }
588
349
    
589
 
    private WebKit.DOM.HTMLElement? closest_ancestor(WebKit.DOM.Element element, string selector) {
590
 
        try {
591
 
            WebKit.DOM.Element? parent = element.get_parent_element();
592
 
            while (parent != null && !parent.webkit_matches_selector(selector)) {
593
 
                parent = parent.get_parent_element();
594
 
            }
595
 
            return parent as WebKit.DOM.HTMLElement;
596
 
        } catch (Error error) {
597
 
            warning("Failed to find ancestor: %s", error.message);
598
 
            return null;
599
 
        }
600
 
    }
601
 
 
 
350
    public void compress_emails() {
 
351
        if (messages.size == 0)
 
352
            return;
 
353
        
 
354
        WebKit.DOM.Document document = web_view.get_dom_document();
 
355
        WebKit.DOM.Element first_compressed = null;
 
356
        int compress_count = 0;
 
357
        bool prev_hidden = false, curr_hidden = false, next_hidden = false;
 
358
        try {
 
359
            next_hidden = document.get_element_by_id(get_div_id(messages.first().id)).get_class_list().contains("hide");
 
360
        } catch (Error error) {
 
361
            debug("Error checking hidden status: %s", error.message);
 
362
        }
 
363
        
 
364
        foreach (Geary.Email message in messages) {
 
365
            try {
 
366
                WebKit.DOM.Element message_element = document.get_element_by_id(get_div_id(message.id));
 
367
                prev_hidden = curr_hidden;
 
368
                curr_hidden = next_hidden;
 
369
                next_hidden = (message_element.next_element_sibling != null)
 
370
                        && message_element.next_element_sibling.get_class_list().contains("hide");
 
371
                if (curr_hidden && prev_hidden && next_hidden) {
 
372
                    message_element.get_class_list().add("compressed");
 
373
                    compress_count += 1;
 
374
                    if (first_compressed == null)
 
375
                        first_compressed = message_element;
 
376
                } else if (compress_count > 0) {
 
377
                    if (compress_count == 1) {
 
378
                        message_element.previous_element_sibling.get_class_list().remove("compressed");
 
379
                    } else {
 
380
                        WebKit.DOM.HTMLElement span =
 
381
                            first_compressed.first_element_child.first_element_child
 
382
                            as WebKit.DOM.HTMLElement;
 
383
                        span.set_inner_html(_("%u read messages").printf(compress_count));
 
384
                        // We need to set the display to get an accurate offset_height
 
385
                        span.set_attribute("style", "display:inline-block;");
 
386
                        span.set_attribute("style", "display:inline-block; top:%ipx".printf(
 
387
                            (int) (message_element.offset_top - first_compressed.offset_top
 
388
                            - span.offset_height) / 2));
 
389
                    }
 
390
                    compress_count = 0;
 
391
                    first_compressed = null;
 
392
                }
 
393
            } catch (Error error) {
 
394
                debug("Error compressing emails: %s", error.message);
 
395
            }
 
396
        }
 
397
    }
 
398
    
 
399
    public void decompress_emails(WebKit.DOM.Element email_element) {
 
400
        WebKit.DOM.Element iter_element = email_element;
 
401
        try {
 
402
            while ((iter_element != null) && iter_element.get_class_list().contains("compressed")) {
 
403
                iter_element.get_class_list().remove("compressed");
 
404
                iter_element.first_element_child.first_element_child.set_attribute("style", "display:none");
 
405
                iter_element = iter_element.previous_element_sibling;
 
406
            }
 
407
        } catch (Error error) {
 
408
            debug("Error decompressing emails: %s", error.message);
 
409
        }
 
410
        iter_element = email_element.next_element_sibling;
 
411
        try {
 
412
            while ((iter_element != null) && iter_element.get_class_list().contains("compressed")) {
 
413
                iter_element.get_class_list().remove("compressed");
 
414
                iter_element.first_element_child.first_element_child.set_attribute("style", "display:none");
 
415
                iter_element = iter_element.next_element_sibling;
 
416
            }
 
417
        } catch (Error error) {
 
418
            debug("Error decompressing emails: %s", error.message);
 
419
        }
 
420
    }
 
421
    
602
422
    private Geary.Email? get_email_from_element(WebKit.DOM.Element element) {
603
423
        // First get the email container.
604
424
        WebKit.DOM.Element? email_element = null;
649
469
        }
650
470
    }
651
471
 
 
472
    private void set_attachment_icon(WebKit.DOM.HTMLElement container, bool show) {
 
473
        try {
 
474
            WebKit.DOM.DOMTokenList class_list = container.get_class_list();
 
475
            Util.DOM.toggle_class(class_list, "attachment", show);
 
476
        } catch (Error e) {
 
477
            warning("Failed to set attachment icon: %s", e.message);
 
478
        }
 
479
    }
 
480
 
652
481
    public void update_flags(Geary.Email email) {
653
482
        // Nothing to do if we aren't displaying this email.
654
483
        if (!email_to_element.has_key(email.id)) {
671
500
            WebKit.DOM.DOMTokenList class_list = container.get_class_list();
672
501
            Util.DOM.toggle_class(class_list, "read", !flags.is_unread());
673
502
            Util.DOM.toggle_class(class_list, "starred", flags.is_flagged());
674
 
            Util.DOM.toggle_class(class_list, "attachment", email.attachments.size > 0);
675
503
        } catch (Error e) {
676
504
            warning("Failed to set classes on .email: %s", e.message);
677
505
        }
681
509
        ConversationViewer conversation_viewer) {
682
510
        Geary.Email email = conversation_viewer.get_email_from_element(clicked_element);
683
511
        if (email != null)
684
 
            conversation_viewer.show_context_menu(email);
 
512
            conversation_viewer.show_context_menu(email, clicked_element);
685
513
    }
686
514
    
687
 
    private void show_context_menu(Geary.Email email) {
688
 
        context_menu = build_context_menu(email);
 
515
    private void show_context_menu(Geary.Email email, WebKit.DOM.Element clicked_element) {
 
516
        context_menu = build_context_menu(email, clicked_element);
689
517
        context_menu.show_all();
690
518
        context_menu.popup(null, null, null, 0, 0);
691
519
    }
692
520
    
693
 
    private Gtk.Menu build_context_menu(Geary.Email email) {
 
521
    private Gtk.Menu build_context_menu(Geary.Email email, WebKit.DOM.Element clicked_element) {
694
522
        Gtk.Menu menu = new Gtk.Menu();
695
523
        
696
524
        if (web_view.can_copy_clipboard()) {
719
547
        select_all_item.activate.connect(on_select_all);
720
548
        menu.append(select_all_item);
721
549
        
 
550
        // Inspect.
 
551
        if (Args.inspector) {
 
552
            Gtk.MenuItem inspect_item = new Gtk.MenuItem.with_mnemonic(_("_Inspect"));
 
553
            inspect_item.activate.connect(() => {web_view.web_inspector.inspect_node(clicked_element);});
 
554
            menu.append(inspect_item);
 
555
        }
 
556
        
722
557
        return menu;
723
558
    }
724
559
 
796
631
                return;
797
632
            
798
633
            WebKit.DOM.DOMTokenList class_list = email_element.get_class_list();
799
 
            if (class_list.contains("hide"))
 
634
            if (class_list.contains("compressed"))
 
635
                decompress_emails(email_element);
 
636
            else if (class_list.contains("hide"))
800
637
                class_list.remove("hide");
801
638
            else
802
639
                class_list.add("hide");
803
640
        } catch (Error error) {
804
641
            warning("Error toggling message: %s", error.message);
805
642
        }
 
643
 
 
644
        mark_read();
806
645
    }
807
646
 
808
647
    private static void on_attachment_clicked(WebKit.DOM.Element element, WebKit.DOM.Event event,
854
693
        Geary.EmailFlags flags = new Geary.EmailFlags();
855
694
        flags.add(Geary.EmailFlags.UNREAD);
856
695
        mark_message(message, null, flags);
 
696
        mark_manual_read(message.id);
857
697
    }
858
698
 
859
699
    private void on_mark_unread_message(Geary.Email message) {
860
700
        Geary.EmailFlags flags = new Geary.EmailFlags();
861
701
        flags.add(Geary.EmailFlags.UNREAD);
862
702
        mark_message(message, flags, null);
 
703
        mark_manual_read(message.id);
 
704
    }
 
705
 
 
706
    // Use this when an email has been marked read through manual (user) intervention
 
707
    public void mark_manual_read(Geary.EmailIdentifier id) {
 
708
        if (email_to_element.has_key(id)) {
 
709
            try {
 
710
                email_to_element.get(id).get_class_list().add("manual_read");
 
711
            } catch (Error error) {
 
712
                debug("Adding manual_read class failed: %s", error.message);
 
713
            }
 
714
        }
863
715
    }
864
716
 
865
717
    private void on_print_message(Geary.Email message) {
974
826
    }
975
827
 
976
828
    private WebKit.DOM.HTMLDivElement create_quote_container() throws Error {
977
 
        WebKit.DOM.HTMLDivElement quote_container = web_view.get_dom_document().create_element("div")
978
 
            as WebKit.DOM.HTMLDivElement;
 
829
        WebKit.DOM.HTMLDivElement quote_container = web_view.create_div();
979
830
        quote_container.set_attribute("class", "quote_container");
980
831
        quote_container.set_inner_html("%s%s%s".printf("<div class=\"shower\">[show]</div>",
981
832
            "<div class=\"hider\">[hide]</div>", "<div class=\"quote\"></div>"));
995
846
    private string set_up_quotes(string text) {
996
847
        try {
997
848
            // Extract any quote containers from the signature block and make them controllable.
998
 
            WebKit.DOM.HTMLElement container = web_view.get_dom_document().create_element("div")
999
 
                as WebKit.DOM.HTMLElement;
 
849
            WebKit.DOM.HTMLElement container = web_view.create_div();
1000
850
            container.set_inner_html(text);
1001
851
            WebKit.DOM.NodeList quote_list = container.query_selector_all(".signature .quote_container");
1002
852
            for (int i = 0; i < quote_list.length; ++i) {
1030
880
        // Wrap all quotes in hide/show controllers.
1031
881
        string message = "";
1032
882
        try {
1033
 
            WebKit.DOM.HTMLElement container = web_view.get_dom_document().create_element("div")
1034
 
                as WebKit.DOM.HTMLElement;
 
883
            WebKit.DOM.HTMLElement container = web_view.create_div();
1035
884
            int offset = 0;
1036
885
            while (offset < text.length) {
1037
886
                // Find the beginning of a quote block.
1085
934
        }
1086
935
        return "<pre>" + set_up_quotes(message + signature) + "</pre>";
1087
936
    }
1088
 
 
1089
 
    private string decorate_quotes(string text) throws Error {
1090
 
        int level = 0;
1091
 
        string outtext = "";
1092
 
        Regex quote_leader = new Regex("^(&gt;)* ?");  // Some &gt; followed by optional space
1093
 
 
1094
 
        foreach (string line in text.split("\n")) {
1095
 
            MatchInfo match_info;
1096
 
            if (quote_leader.match_all(line, 0, out match_info)) {
1097
 
                int start, end, new_level;
1098
 
                match_info.fetch_pos(0, out start, out end);
1099
 
                new_level = end / 4;  // Cast to int removes 0.25 from space at end, if present
1100
 
                while (new_level > level) {
1101
 
                    outtext += "<blockquote>";
1102
 
                    level += 1;
1103
 
                }
1104
 
                while (new_level < level) {
1105
 
                    outtext += "</blockquote>";
1106
 
                    level -= 1;
1107
 
                }
1108
 
                outtext += line.substring(end);
1109
 
            } else {
1110
 
                debug("This line didn't match the quote regex: %s", line);
1111
 
                outtext += line;
1112
 
            }
1113
 
        }
1114
 
        // Close any remaining blockquotes.
1115
 
        while (level > 0) {
1116
 
            outtext += "</blockquote>";
1117
 
            level -= 1;
1118
 
        }
1119
 
        return outtext;
1120
 
    }
1121
 
 
 
937
    
1122
938
    private string insert_html_markup(string text, Geary.Email email) {
1123
939
        try {
1124
940
            // Create a workspace for manipulating the HTML.
1125
 
            WebKit.DOM.Document document = web_view.get_dom_document();
1126
 
            WebKit.DOM.HTMLElement container = document.create_element("div") as WebKit.DOM.HTMLElement;
 
941
            WebKit.DOM.HTMLElement container = web_view.create_div();
1127
942
            container.set_inner_html(text);
1128
943
            
1129
944
            // Some HTML messages like to wrap themselves in full, proper html, head, and body tags.
1162
977
            // Now look for the signature.
1163
978
            wrap_html_signature(ref container);
1164
979
 
1165
 
            // Then get all inline images and replace them with data URLs.
1166
 
            WebKit.DOM.NodeList inline_list = container.query_selector_all("img[src^=\"cid:\"]");
1167
 
            for (int i = 0; i < inline_list.length; ++i) {
 
980
            // Then look for all <img> tags. Inline images are replaced with
 
981
            // data URLs, while external images are added to
 
982
            // external_images_uri (to be used later by is_image()).
 
983
            Gee.ArrayList<string> external_images_uri = new Gee.ArrayList<string>();
 
984
            WebKit.DOM.NodeList inline_list = container.query_selector_all("img");
 
985
            for (ulong i = 0; i < inline_list.length; ++i) {
1168
986
                // Get the MIME content for the image.
1169
987
                WebKit.DOM.HTMLImageElement img = (WebKit.DOM.HTMLImageElement) inline_list.item(i);
1170
 
                string mime_id = img.get_attribute("src").substring(4);
1171
 
                Geary.Memory.AbstractBuffer image_content =
1172
 
                    email.get_message().get_content_by_mime_id(mime_id);
1173
 
                uint8[] image_data = image_content.get_array();
1174
 
 
1175
 
                // Get the content type.
1176
 
                bool uncertain_content_type;
1177
 
                string mimetype = ContentType.get_mime_type(ContentType.guess(null, image_data,
1178
 
                    out uncertain_content_type));
1179
 
 
1180
 
                // Then set the source to a data url.
1181
 
                set_data_url(img, mimetype, image_data);
 
988
                string? src = img.get_attribute("src");
 
989
                if (Geary.String.is_empty(src)) {
 
990
                    continue;
 
991
                } else if (src.has_prefix("cid:")) {
 
992
                    string mime_id = src.substring(4);
 
993
                    Geary.Memory.AbstractBuffer image_content =
 
994
                        email.get_message().get_content_by_mime_id(mime_id);
 
995
                    uint8[] image_data = image_content.get_array();
 
996
 
 
997
                    // Get the content type.
 
998
                    bool uncertain_content_type;
 
999
                    string mimetype = ContentType.get_mime_type(ContentType.guess(null, image_data,
 
1000
                        out uncertain_content_type));
 
1001
 
 
1002
                    // Then set the source to a data url.
 
1003
                    web_view.set_data_url(img, mimetype, image_data);
 
1004
                } else if (!src.has_prefix("data:")) {
 
1005
                    external_images_uri.add(src);
 
1006
                    if (!web_view.load_external_images)
 
1007
                        external_images_info_bar.show();
 
1008
                }
1182
1009
            }
1183
1010
 
 
1011
            web_view.set_external_images_uris(external_images_uri);
 
1012
 
1184
1013
            // Now return the whole message.
1185
1014
            return set_up_quotes(container.get_inner_html());
1186
1015
        } catch (Error e) {
1216
1045
            return;
1217
1046
        }
1218
1047
        WebKit.DOM.Element elem = div_list.item(i) as WebKit.DOM.Element;
1219
 
        WebKit.DOM.HTMLElement signature_container = web_view.get_dom_document().create_element("div")
1220
 
            as WebKit.DOM.HTMLElement;
 
1048
        WebKit.DOM.HTMLElement signature_container = web_view.create_div();
1221
1049
        signature_container.set_attribute("class", "signature");
1222
1050
        do {
1223
1051
            // Get its sibling _before_ we move it into the signature div.
1230
1058
        container.append_child(signature_container);
1231
1059
    }
1232
1060
    
1233
 
    private bool node_is_child_of(WebKit.DOM.Node node, string ancestor_tag) {
1234
 
        WebKit.DOM.Element? ancestor = node.get_parent_element();
1235
 
        for (; ancestor != null; ancestor = ancestor.get_parent_element()) {
1236
 
            if (ancestor.get_tag_name() == ancestor_tag) {
1237
 
                return true;
1238
 
            }
1239
 
        }
1240
 
        return false;
1241
 
    }
1242
 
 
1243
1061
    public void remove_message(Geary.Email email) {
1244
1062
        if (!messages.contains(email))
1245
1063
            return;
1269
1087
        if (Geary.String.is_empty(_value))
1270
1088
            return;
1271
1089
        
1272
 
        string title = Geary.HTML.escape_markup(_title);
1273
 
        string value = Geary.HTML.escape_markup(_value);
1274
 
        
1275
 
        header_text += create_header_row(title, value, important);
 
1090
        header_text += create_header_row(Geary.HTML.escape_markup(_title),
 
1091
            Geary.HTML.escape_markup(_value), important);
1276
1092
    }
1277
1093
 
1278
1094
    private void insert_header_date(ref string header_text, string _title, DateTime _value,
1314
1130
        header_text += create_header_row(Geary.HTML.escape_markup(title), value, important);
1315
1131
    }
1316
1132
    
1317
 
    private string linkify_and_escape_plain_text(string input) throws Error {
1318
 
        // Convert < and > into non-printable characters, and change & to &amp;.
1319
 
        string output = input.replace("<", " \01 ").replace(">", " \02 ").replace("&", "&amp;");
1320
 
        
1321
 
        // Converts text links into HTML hyperlinks.
1322
 
        Regex r = new Regex(URL_REGEX, RegexCompileFlags.CASELESS);
1323
 
        
1324
 
        output = r.replace_eval(output, -1, 0, 0, is_valid_url);
1325
 
        return output.replace(" \01 ", "&lt;").replace(" \02 ", "&gt;");
1326
 
    }
1327
 
 
1328
1133
    private void insert_attachments(WebKit.DOM.HTMLElement email_container,
1329
1134
        Gee.List<Geary.Attachment> attachments) {
1330
1135
 
1368
1173
                // Set the image preview and insert it into the container.
1369
1174
                WebKit.DOM.HTMLImageElement img =
1370
1175
                    Util.DOM.select(attachment_table, ".preview img") as WebKit.DOM.HTMLImageElement;
1371
 
                set_image_src(img, attachment.mime_type, attachment.filepath, ATTACHMENT_PREVIEW_SIZE);
 
1176
                web_view.set_image_src(img, attachment.mime_type, attachment.filepath, ATTACHMENT_PREVIEW_SIZE);
1372
1177
                attachment_container.append_child(attachment_table);
1373
1178
            }
1374
1179
 
1378
1183
            debug("Failed to insert attachments: %s", error.message);
1379
1184
        }
1380
1185
    }
1381
 
 
1382
 
    // Validates a URL.
1383
 
    // Ensures the URL begins with a valid protocol specifier.  (If not, we don't
1384
 
    // want to linkify it.)
1385
 
    private bool is_valid_url(MatchInfo match_info, StringBuilder result) {
1386
 
        try {
1387
 
            string? url = match_info.fetch(0);
1388
 
            Regex r = new Regex(PROTOCOL_REGEX, RegexCompileFlags.CASELESS);
1389
 
            
1390
 
            result.append(r.match(url) ? "<a href=\"%s\">%s</a>".printf(url, url) : url);
1391
 
        } catch (Error e) {
1392
 
            debug("URL parsing error: %s\n", e.message);
1393
 
        }
1394
 
        return false; // False to continue processing.
1395
 
    }
1396
1186
    
1397
 
    // Scrolls back up to the top.
1398
1187
    public void scroll_reset() {
1399
 
        web_view.get_dom_document().get_default_view().scroll(0, 0);
1400
 
    }
1401
 
    
1402
 
    private bool on_navigation_policy_decision_requested(WebKit.WebFrame frame,
1403
 
        WebKit.NetworkRequest request, WebKit.WebNavigationAction navigation_action,
1404
 
        WebKit.WebPolicyDecision policy_decision) {
1405
 
        policy_decision.ignore();
1406
 
        
1407
 
        // Other policy-decisions may be requested for various reasons. The existence of an iframe,
1408
 
        // for example, causes a policy-decision request with an "OTHER" reason. We don't want to
1409
 
        // open a webpage in the browser just because an email contains an iframe.
1410
 
        if (navigation_action.reason == WebKit.WebNavigationReason.LINK_CLICKED)
1411
 
            link_selected(request.uri);
1412
 
        return true;
 
1188
        web_view.scroll_reset();
1413
1189
    }
1414
1190
    
1415
1191
    private void on_hovering_over_link(string? title, string? url) {
1461
1237
            dialog.run();
1462
1238
        }
1463
1239
    }
 
1240
 
 
1241
    public void mark_read() {
 
1242
        Gee.List<Geary.EmailIdentifier> ids = new Gee.ArrayList<Geary.EmailIdentifier>();
 
1243
        WebKit.DOM.Document document = web_view.get_dom_document();
 
1244
        long scroll_top = document.body.scroll_top;
 
1245
        long scroll_height = document.document_element.scroll_height;
 
1246
 
 
1247
        foreach (Geary.Email message in messages) {
 
1248
            try {
 
1249
                if (message.email_flags.is_unread()) {
 
1250
                    WebKit.DOM.HTMLElement element = email_to_element.get(message.id);
 
1251
                    WebKit.DOM.HTMLElement body = (WebKit.DOM.HTMLElement) element.get_elements_by_class_name("body").item(0);
 
1252
                    if (!element.get_class_list().contains("manual_read") &&
 
1253
                            body.offset_top + body.offset_height > scroll_top &&
 
1254
                            body.offset_top + 28 < scroll_top + scroll_height) {  // 28 = 15 padding + 13 first line of text
 
1255
                        ids.add(message.id);
 
1256
                    }
 
1257
                }
 
1258
            } catch (Error error) {
 
1259
                debug("Problem checking email class: %s", error.message);
 
1260
            }
 
1261
        }
 
1262
 
 
1263
        Geary.FolderSupportsMark? supports_mark = current_folder as Geary.FolderSupportsMark;
 
1264
        if (supports_mark != null & ids.size > 0) {
 
1265
            Geary.EmailFlags flags = new Geary.EmailFlags();
 
1266
            flags.add(Geary.EmailFlags.UNREAD);
 
1267
            supports_mark.mark_email_async.begin(ids, null, flags, null);
 
1268
        }
 
1269
    }
1464
1270
}
1465
1271