iMerge branch 'wip/90-304-image-dnd-paste' into 'mainline'

Inline image drag and drop #90 and image paste from clipboard #304

See merge request GNOME/geary!343
This commit is contained in:
Michael James Gratton 2019-11-17 16:42:12 +11:00
commit 8f2563bc09
12 changed files with 433 additions and 41 deletions

View file

@ -14,7 +14,7 @@ public class ComposerWebView : ClientWebView {
// WebKit message handler names
private const string CURSOR_CONTEXT_CHANGED = "cursorContextChanged";
private const string DRAG_DROP_RECEIVED = "dragDropReceived";
/**
* Encapsulates editing-related state for a specific DOM node.
@ -108,6 +108,9 @@ public class ComposerWebView : ClientWebView {
/** Emitted when the cursor's edit context has changed. */
public signal void cursor_context_changed(EditContext cursor_context);
/** Emitted when an image file has been dropped on the composer */
public signal void image_file_dropped(string filename, string type, uint8[] contents);
/** Workaround for WebView eating the button event */
internal signal bool button_release_event_done(Gdk.Event event);
@ -121,6 +124,7 @@ public class ComposerWebView : ClientWebView {
this.user_content_manager.add_script(ComposerWebView.app_script);
register_message_handler(CURSOR_CONTEXT_CHANGED, on_cursor_context_changed);
register_message_handler(DRAG_DROP_RECEIVED, on_drag_drop_received);
// XXX this is a bit of a hack given the docs for is_empty,
// above
@ -521,4 +525,40 @@ public class ComposerWebView : ClientWebView {
}
}
/**
* Handle a dropped image
*/
private void on_drag_drop_received(WebKit.JavascriptResult result) {
try {
JSC.Value object = result.get_js_value();
string filename = Util.JS.to_string(
Util.JS.get_property(object, "fileName")
);
string filename_unescaped = GLib.Uri.unescape_string(filename);
string file_type = Util.JS.to_string(
Util.JS.get_property(object, "fileType")
);
string content_base64 = Util.JS.to_string(
Util.JS.get_property(object, "content")
);
uint8[] image = GLib.Base64.decode(content_base64);
if (image.length == 0) {
warning("%s is empty", filename);
return;
}
// A simple check to see if the file looks like an image. A problem here
// will be this accepting types which won't be supported by WebKit
// or recipients.
if (file_type.index_of("image/") == 0) {
image_file_dropped(filename_unescaped, file_type, image);
}
} catch (Util.JS.Error err) {
debug("Could not get deceptive link param: %s", err.message);
}
}
}

View file

@ -179,6 +179,8 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
_("attach|attaching|attaches|attachment|attachments|attached|enclose|enclosed|enclosing|encloses|enclosure|enclosures");
private const string PASTED_IMAGE_FILENAME_TEMPLATE = "geary-pasted-image-%u.png";
public Geary.Account account { get; private set; }
private Gee.Map<string, Geary.AccountInformation> accounts;
@ -367,8 +369,8 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
private AttachPending pending_include = AttachPending.INLINE_ONLY;
private Gee.Set<File> attached_files = new Gee.HashSet<File>(Geary.Files.nullable_hash,
Geary.Files.nullable_equal);
private Gee.Map<string,File> inline_files = new Gee.HashMap<string,File>();
private Gee.Map<string,File> cid_files = new Gee.HashMap<string,File>();
private Gee.Map<string,Geary.Memory.Buffer> inline_files = new Gee.HashMap<string,Geary.Memory.Buffer>();
private Gee.Map<string,Geary.Memory.Buffer> cid_files = new Gee.HashMap<string,Geary.Memory.Buffer>();
private Geary.App.DraftManager? draft_manager = null;
private GLib.Cancellable? draft_manager_opening = null;
@ -524,6 +526,12 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
this.application.engine.account_unavailable.connect(
on_account_unavailable
);
// Listen for drag and dropped image file
this.editor.image_file_dropped.connect(
on_image_file_dropped
);
// TODO: also listen for account updates to allow adding identities while writing an email
this.from = new Geary.RFC822.MailboxAddresses.single(account.information.primary_mailbox);
@ -1623,9 +1631,10 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
// using a cid: URL anyway, so treat it as an
// attachment instead.
if (content_id != null) {
this.cid_files[content_id] = file;
Geary.Memory.FileBuffer file_buffer = new Geary.Memory.FileBuffer(file, true);
this.cid_files[content_id] = file_buffer;
this.editor.add_internal_resource(
content_id, new Geary.Memory.FileBuffer(file, true)
content_id, file_buffer
);
} else {
type = Geary.Mime.DispositionType.ATTACHMENT;
@ -1641,7 +1650,10 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
!this.attached_files.contains(file) &&
!this.inline_files.has_key(content_id)) {
if (type == Geary.Mime.DispositionType.INLINE) {
add_inline_part(file, content_id);
check_attachment_file(file);
Geary.Memory.FileBuffer file_buffer = new Geary.Memory.FileBuffer(file, true);
string unused;
add_inline_part(file_buffer, content_id, out unused);
} else {
add_attachment_part(file);
}
@ -1690,18 +1702,41 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
update_attachments_view();
}
private void add_inline_part(File target, string content_id)
private void add_inline_part(Geary.Memory.Buffer target, string content_id, out string unique_contentid)
throws AttachmentError {
check_attachment_file(target);
this.inline_files[content_id] = target;
try {
this.editor.add_internal_resource(
content_id, new Geary.Memory.FileBuffer(target, true)
const string UNIQUE_RENAME_TEMPLATE = "%s_%02u";
if (target.size == 0)
throw new AttachmentError.FILE(
_("“%s” is an empty file.").printf(content_id)
);
} catch (Error err) {
// unlikely
debug("Failed to re-open file for attachment: %s", err.message);
// Avoid filename conflicts
unique_contentid = content_id;
int suffix_index = 0;
string unsuffixed_filename = "";
while (this.inline_files.has_key(unique_contentid)) {
string[] filename_parts = unique_contentid.split(".");
// Handle no file extension
int partindex;
if (filename_parts.length > 1) {
partindex = filename_parts.length-2;
} else {
partindex = 0;
}
if (unsuffixed_filename == "")
unsuffixed_filename = filename_parts[partindex];
filename_parts[partindex] = UNIQUE_RENAME_TEMPLATE.printf(unsuffixed_filename, suffix_index++);
unique_contentid = string.joinv(".", filename_parts);
}
this.inline_files[unique_contentid] = target;
this.editor.add_internal_resource(
unique_contentid, target
);
}
private FileInfo check_attachment_file(File target)
@ -1873,7 +1908,14 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
private void on_paste(SimpleAction action, Variant? param) {
if (this.container.get_focus() == this.editor) {
if (this.editor.is_rich_text) {
this.editor.paste_rich_text();
// Check for pasted image in clipboard
Gtk.Clipboard clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD);
bool has_image = clipboard.wait_is_image_available();
if (has_image) {
paste_image();
} else {
this.editor.paste_rich_text();
}
} else {
this.editor.paste_plain_text();
}
@ -1882,6 +1924,39 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
}
}
/**
* Handle a pasted image, adding it as an inline attachment
*/
private void paste_image() {
// The slow operations here are creating the PNG and, to a lesser extent,
// requesting the image from the clipboard
this.container.top_window.application.mark_busy();
get_clipboard(Gdk.SELECTION_CLIPBOARD).request_image((clipboard, pixbuf) => {
if (pixbuf != null) {
try {
uint8[] buffer;
pixbuf.save_to_buffer(out buffer, "png");
Geary.Memory.ByteBuffer byte_buffer = new Geary.Memory.ByteBuffer(buffer, buffer.length);
GLib.DateTime time_now = new GLib.DateTime.now();
string filename = PASTED_IMAGE_FILENAME_TEMPLATE.printf(time_now.hash());
string unique_filename;
add_inline_part(byte_buffer, filename, out unique_filename);
this.editor.insert_image(
ClientWebView.INTERNAL_URL_PREFIX + unique_filename
);
} catch (Error error) {
warning("Failed to paste image %s", error.message);
}
} else {
warning("Failed to get image from clipboard");
}
this.container.top_window.application.unmark_busy();
});
}
private void on_paste_without_formatting(SimpleAction action, Variant? param) {
if (this.container.get_focus() == this.editor)
this.editor.paste_plain_text();
@ -2496,10 +2571,13 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
dialog.hide();
foreach (File file in dialog.get_files()) {
try {
check_attachment_file(file);
Geary.Memory.FileBuffer file_buffer = new Geary.Memory.FileBuffer(file, true);
string path = file.get_path();
add_inline_part(file, path);
string unique_filename;
add_inline_part(file_buffer, path, out unique_filename);
this.editor.insert_image(
ClientWebView.INTERNAL_URL_PREFIX + path
ClientWebView.INTERNAL_URL_PREFIX + unique_filename
);
} catch (Error err) {
attachment_failed(err.message);
@ -2555,4 +2633,21 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
}
}
/**
* Handle a dropped image file, adding it as an inline attachment
*/
private void on_image_file_dropped(string filename, string file_type, uint8[] contents) {
Geary.Memory.ByteBuffer buffer = new Geary.Memory.ByteBuffer(contents, contents.length);
string unique_filename;
try {
add_inline_part(buffer, filename, out unique_filename);
} catch (AttachmentError err) {
warning("Couldn't attach dropped empty file %s", filename);
return;
}
this.editor.insert_image(
ClientWebView.INTERNAL_URL_PREFIX + unique_filename
);
}
}

View file

@ -30,8 +30,7 @@ public void webkit_web_extension_initialize_with_user_data(WebKit.WebExtension e
*/
public class GearyWebExtension : Object {
private const string[] ALLOWED_SCHEMES = { "cid", "geary", "data" };
private const string[] ALLOWED_SCHEMES = { "cid", "geary", "data", "blob" };
private WebKit.WebExtension extension;

View file

@ -38,10 +38,10 @@ public class Geary.ComposedEmail : BaseObject {
public Gee.Set<File> attached_files { get; private set;
default = new Gee.HashSet<File>(Geary.Files.nullable_hash, Geary.Files.nullable_equal); }
public Gee.Map<string,File> inline_files { get; private set;
default = new Gee.HashMap<string,File>(); }
public Gee.Map<string,File> cid_files { get; private set;
default = new Gee.HashMap<string,File>(); }
public Gee.Map<string,Memory.Buffer> inline_files { get; private set;
default = new Gee.HashMap<string,Memory.Buffer>(); }
public Gee.Map<string,Memory.Buffer> cid_files { get; private set;
default = new Gee.HashMap<string,Memory.Buffer>(); }
public string img_src_prefix { get; set; default = ""; }
@ -88,18 +88,17 @@ public class Geary.ComposedEmail : BaseObject {
public bool replace_inline_img_src(string orig, string replacement) {
// XXX This and contains_inline_img_src are pretty
// hacky. Should probably be working with a DOM tree.
bool ret = false;
int index = -1;
if (this.body_html != null) {
string old_body = this.body_html;
this.body_html = old_body.replace(
IMG_SRC_TEMPLATE.printf(this.img_src_prefix + orig),
IMG_SRC_TEMPLATE.printf(replacement)
);
// Avoid doing a proper comparison so we don't need to scan
// the whole string again.
ret = this.body_html.length != old_body.length;
string prefixed_orig = IMG_SRC_TEMPLATE.printf(this.img_src_prefix + orig);
index = this.body_html.index_of(prefixed_orig);
if (index != -1) {
this.body_html = this.body_html.substring(0, index) +
IMG_SRC_TEMPLATE.printf(replacement) +
this.body_html.substring(index + prefixed_orig.length);
}
}
return ret;
return index != -1;
}
}

View file

@ -236,7 +236,7 @@ public class Geary.RFC822.Message : BaseObject, EmailHeaderSet {
new Gee.LinkedList<GMime.Object>();
// The files that need to have Content IDs assigned
Gee.Map<string,File> inline_files = new Gee.HashMap<string,File>();
Gee.Map<string,Memory.Buffer> inline_files = new Gee.HashMap<string,Memory.Buffer>();
inline_files.set_all(email.inline_files);
// Create parts for inline images, if any, and updating
@ -248,18 +248,18 @@ public class Geary.RFC822.Message : BaseObject, EmailHeaderSet {
// assigned
foreach (string cid in email.cid_files.keys) {
if (email.contains_inline_img_src(CID_URL_PREFIX + cid)) {
File file = email.cid_files[cid];
GMime.Object? inline_part = null;
try {
inline_part = yield get_file_part(
file,
inline_part = yield get_buffer_part(
email.cid_files[cid],
GLib.Path.get_basename(cid),
Geary.Mime.DispositionType.INLINE,
cancellable
);
} catch (GLib.Error err) {
warning(
"Error creating CID part %s: %s",
file.get_path(),
cid,
err.message
);
}
@ -288,15 +288,16 @@ public class Geary.RFC822.Message : BaseObject, EmailHeaderSet {
CID_URL_PREFIX + cid)) {
GMime.Object? inline_part = null;
try {
inline_part = yield get_file_part(
inline_part = yield get_buffer_part(
inline_files[name],
GLib.Path.get_basename(name),
Geary.Mime.DispositionType.INLINE,
cancellable
);
} catch (GLib.Error err) {
warning(
"Error creating inline file part %s: %s",
inline_files[name].get_path(),
name,
err.message
);
}
@ -441,6 +442,56 @@ public class Geary.RFC822.Message : BaseObject, EmailHeaderSet {
GMime.StreamGIO stream = new GMime.StreamGIO(file);
stream.set_owner(false);
return yield finalise_attachment_part(stream, part, content_type, cancellable);
}
/**
* Create a GMime part for the provided attachment buffer
*/
private async GMime.Part? get_buffer_part(Memory.Buffer buffer,
string basename,
Geary.Mime.DispositionType disposition,
GLib.Cancellable cancellable)
throws Error {
Mime.ContentType? mime_type = Mime.ContentType.guess_type(
basename,
buffer
);
if (mime_type == null) {
throw new RFC822Error.INVALID(
_("Could not determine mime type for “%s”.").printf(basename)
);
}
GMime.ContentType? content_type = new GMime.ContentType.from_string(mime_type.get_mime_type());
if (content_type == null) {
throw new RFC822Error.INVALID(
_("Could not determine content type for mime type “%s” on “%s”.").printf(mime_type.to_string(), basename)
);
}
GMime.Part part = new GMime.Part();
part.set_disposition(disposition.serialize());
part.set_filename(basename);
part.set_content_type(content_type);
GMime.StreamMem stream = Utils.create_stream_mem(buffer);
return yield finalise_attachment_part(stream, part, content_type, cancellable);
}
/**
* Set encoding and content object on GMime part
*/
private async GMime.Part finalise_attachment_part(GMime.Stream stream,
GMime.Part part,
GMime.ContentType content_type,
GLib.Cancellable cancellable)
throws Error {
// Text parts should be scanned fully to determine best
// (i.e. most compact) transport encoding to use, but
// that's usually fine since they tend to be

View file

@ -6,5 +6,6 @@
<file>basic-multipart-alternative.eml</file>
<file>basic-multipart-tnef.eml</file>
<file>geary-0.6-db.tar.xz</file>
<file>test-attachment-image.png</file>
</gresource>
</gresources>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -0,0 +1,53 @@
/*
* Copyright 2016-2018 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.
*/
class Geary.ComposedEmailTest: TestCase {
private const string IMG_CONTAINING_HTML_BODY = "<img src=\"test.png\" />";
public ComposedEmailTest() {
base("Geary.ComposedEmailTest");
add_test("contains_inline_img_src", contains_inline_img_src);
add_test("replace_inline_img_src", replace_inline_img_src);
}
public void contains_inline_img_src() throws Error {
ComposedEmail composed = build_composed_with_img_src();
assert_true(composed.contains_inline_img_src("test.png"), "Expected matched image source");
assert_false(composed.contains_inline_img_src("missing.png"), "Expected missing image");
}
public void replace_inline_img_src() throws Error {
ComposedEmail composed = build_composed_with_img_src();
assert_true(composed.replace_inline_img_src("test.png", "updated.png"), "Expected replacement success");
assert_false(composed.replace_inline_img_src("missing.png", "updated.png"), "Expected replacement failure");
assert_true(composed.contains_inline_img_src("updated.png"), "Expected new image source");
assert_true(composed.replace_inline_img_src("updated.png", "1234567.png"), "Expected replacement success for same length filename");
assert_true(composed.contains_inline_img_src("1234567.png"), "Expected new same length image source");
}
private ComposedEmail build_composed_with_img_src() {
RFC822.MailboxAddress to = new RFC822.MailboxAddress(
"Test", "test@example.com"
);
RFC822.MailboxAddress from = new RFC822.MailboxAddress(
"Sender", "sender@example.com"
);
return new Geary.ComposedEmail(
new GLib.DateTime.now_local(),
new Geary.RFC822.MailboxAddresses.single(from),
new Geary.RFC822.MailboxAddresses.single(to),
null,
null,
null,
null,
IMG_CONTAINING_HTML_BODY
);
}
}

View file

@ -29,6 +29,11 @@ This is the second line.
""";
private static string SIMPLE_MULTIRECIPIENT_TO_CC_BCC = "From: Jill Smith <jill@somewhere.tld>\r\nTo: Jane Doe <jdoe@somewhere.tld>\r\nCc: Jane Doe CC <jdoe_cc@somewhere.tld>\r\nBcc: Jane Doe BCC <jdoe_bcc@somewhere.tld>\r\nSubject: Re: Saying Hello\r\nDate: Fri, 21 Nov 1997 10:01:10 -0600\r\n\r\nThis is a reply to your hello.\r\n\r\n";
private static string NETWORK_BUFFER_EXPECTED = "From: Alice <alice@example.net>\r\nSender: Bob <bob@example.net>\r\nTo: Charlie <charlie@example.net>\r\nCC: Dave <dave@example.net>\r\nBCC: Eve <eve@example.net>\r\nReply-To: \"Alice: Personal Account\" <alice@example.org>\r\nSubject: Re: Basic text/plain message\r\nDate: Fri, 21 Nov 1997 10:01:10 -0600\r\nMessage-ID: <3456@example.net>\r\nIn-Reply-To: <1234@local.machine.example>\r\nReferences: <1234@local.machine.example>\r\nX-Mailer: Geary Test Suite 1.0\r\n\r\nThis is the first line.\r\n\r\nThis is the second line.\r\n\r\n";
private static string TEST_ATTACHMENT_IMAGE_FILENAME = "test-attachment-image.png";
public MessageTest() {
base("Geary.RFC822.MessageTest");
add_test("basic_message_from_buffer", basic_message_from_buffer);
@ -49,6 +54,11 @@ This is the second line.
add_test("multipart_alternative_as_html",
multipart_alternative_as_html);
add_test("get_preview", get_preview);
add_test("get_recipients", get_recipients);
add_test("get_searchable_body", get_searchable_body);
add_test("get_searchable_recipients", get_searchable_recipients);
add_test("get_network_buffer", get_network_buffer);
add_test("from_composed_email_inline_attachments", from_composed_email_inline_attachments);
}
public void basic_message_from_buffer() throws Error {
@ -169,6 +179,106 @@ This is the second line.
assert(multipart_signed.get_preview() == MULTIPART_SIGNED_MESSAGE_PREVIEW);
}
public void get_recipients() throws Error {
Message test = string_to_message(SIMPLE_MULTIRECIPIENT_TO_CC_BCC);
Gee.List<RFC822.MailboxAddress>? addresses = test.get_recipients();
Gee.List<string> verify_list = new Gee.ArrayList<string>();
verify_list.add("Jane Doe <jdoe@somewhere.tld>");
verify_list.add("Jane Doe CC <jdoe_cc@somewhere.tld>");
verify_list.add("Jane Doe BCC <jdoe_bcc@somewhere.tld>");
assert_addresses_list(addresses, verify_list, "get_recipients");
}
public void get_searchable_body() throws Error {
Message test = resource_to_message(BASIC_TEXT_HTML);
string searchable = test.get_searchable_body();
assert_true(searchable.contains("This is the first line"), "Expected body text");
assert_false(searchable.contains("<P>"), "Expected html removed");
}
public void get_searchable_recipients() throws Error {
Message test = string_to_message(SIMPLE_MULTIRECIPIENT_TO_CC_BCC);
string searchable = test.get_searchable_recipients();
assert_true(searchable.contains("Jane Doe <jdoe@somewhere.tld>"), "Expected to address");
assert_true(searchable.contains("Jane Doe CC <jdoe_cc@somewhere.tld>"), "Expected cc address");
assert_true(searchable.contains("Jane Doe BCC <jdoe_bcc@somewhere.tld>"), "Expected bcc address");
}
public void get_network_buffer() throws Error {
Message test = resource_to_message(BASIC_TEXT_PLAIN);
Memory.Buffer buffer = test.get_network_buffer(true);
assert_true(buffer.to_string() == NETWORK_BUFFER_EXPECTED, "Network buffer differs");
}
public void from_composed_email_inline_attachments() throws Error {
RFC822.MailboxAddress to = new RFC822.MailboxAddress(
"Test", "test@example.com"
);
RFC822.MailboxAddress from = new RFC822.MailboxAddress(
"Sender", "sender@example.com"
);
Geary.ComposedEmail composed = new Geary.ComposedEmail(
new GLib.DateTime.now_local(),
new Geary.RFC822.MailboxAddresses.single(from),
new Geary.RFC822.MailboxAddresses.single(to),
null,
null,
null,
null,
"<img src=\"cid:test-attachment-image.png\" /><img src=\"needing_cid.png\" />"
);
GLib.File resource =
GLib.File.new_for_uri(RESOURCE_URI).resolve_relative_path(TEST_ATTACHMENT_IMAGE_FILENAME);
uint8[] contents;
resource.load_contents(null, out contents, null);
Geary.Memory.ByteBuffer buffer = new Geary.Memory.ByteBuffer(contents, contents.length);
composed.cid_files[TEST_ATTACHMENT_IMAGE_FILENAME] = buffer;
Geary.Memory.ByteBuffer buffer2 = new Geary.Memory.ByteBuffer(contents, contents.length);
composed.inline_files["needing_cid.png"] = buffer2;
this.message_from_composed_email.begin(
composed,
async_complete_full
);
Geary.RFC822.Message message = message_from_composed_email.end(async_result());
Gee.List<Part> attachments = message.get_attachments();
bool found_first = false;
bool found_second = false;
bool second_id_renamed = false;
foreach (Part part in attachments) {
if (part.get_clean_filename() == TEST_ATTACHMENT_IMAGE_FILENAME) {
found_first = true;
} else if (part.get_clean_filename() == "needing_cid.png") {
found_second = true;
second_id_renamed = part.content_id != "needing_cid.png";
}
}
assert_true(found_first, "Expected CID attachment");
assert_true(found_second, "Expected inline attachment");
assert_true(second_id_renamed, "Expected inline attachment renamed");
string html_body = message.get_html_body(null);
assert_false(html_body.contains("src=\"needing_cid.png\""), "Expected updated attachment content ID");
Memory.Buffer out_buffer = message.get_native_buffer();
assert_true(out_buffer.size > (buffer.size+buffer2.size), "Expected sizeable message");
}
private async Geary.RFC822.Message message_from_composed_email(Geary.ComposedEmail composed) {
return yield new Geary.RFC822.Message.from_composed_email(
composed,
GMime.utils_generate_message_id(composed.from.get(0).domain),
null
);
}
private Message resource_to_message(string path) throws Error {
GLib.File resource =
GLib.File.new_for_uri(RESOURCE_URI).resolve_relative_path(path);
@ -208,6 +318,17 @@ This is the second line.
assert_string(expected, addresses.to_rfc822_string());
}
private void assert_addresses_list(Gee.List<RFC822.MailboxAddress>? addresses,
Gee.List<string> expected,
string context)
throws Error {
assert_non_null(addresses, context + " not null");
assert_true(addresses.size == expected.size, context + " size");
foreach (RFC822.MailboxAddress address in addresses) {
assert_true(expected.contains(address.to_rfc822_string()), context + " missing");
}
}
private void assert_message_id_list(Geary.RFC822.MessageIDList? ids,
string expected)
throws Error {

View file

@ -27,6 +27,7 @@ geary_test_engine_sources = [
'engine/api/geary-engine-test.vala',
'engine/api/geary-folder-path-test.vala',
'engine/api/geary-service-information-test.vala',
'engine/api/geary-composed-email-test.vala',
'engine/app/app-conversation-test.vala',
'engine/app/app-conversation-monitor-test.vala',
'engine/app/app-conversation-set-test.vala',

View file

@ -76,6 +76,7 @@ int main(string[] args) {
engine.add_suite(new Geary.RFC822.PartTest().get_suite());
engine.add_suite(new Geary.RFC822.Utils.Test().get_suite());
engine.add_suite(new Geary.String.Test().get_suite());
engine.add_suite(new Geary.ComposedEmailTest().get_suite());
/*
* Run the tests

View file

@ -93,6 +93,12 @@ ComposerPageState.prototype = {
}
}, true);
// Handle file drag & drop
document.body.addEventListener("drop", state.handleFileDrop, true);
document.body.addEventListener("allowDrop", function(e) {
ev.preventDefault();
}, true);
// Search for and remove a particular styling when we quote
// text. If that style exists in the quoted text, we alter it
// slightly so we don't mess with it later.
@ -380,6 +386,31 @@ ComposerPageState.prototype = {
}
}
return inPart;
},
handleFileDrop: function(dropEvent) {
dropEvent.preventDefault();
for (var i = 0; i < dropEvent.dataTransfer.files.length; i++) {
const file = dropEvent.dataTransfer.files[i];
if (!file.type.startsWith('image/'))
continue;
const reader = new FileReader();
reader.onload = (function(filename, imageType) { return function(loadEvent) {
// Remove prefixed file type and encoding type
var parts = loadEvent.target.result.split(",");
if (parts.length < 2)
return;
window.webkit.messageHandlers.dragDropReceived.postMessage({
fileName: encodeURIComponent(filename),
fileType: imageType,
content: parts[1]
});
}; })(file.name, file.type);
reader.readAsDataURL(file);
}
}
};