upload: Replace jQuery filedrop with Uppy.

This commit is contained in:
Vishnu KS 2019-11-21 12:24:55 +08:00 committed by Tim Abbott
parent 25bfe135b8
commit 5bab2a3762
16 changed files with 813 additions and 1089 deletions

View File

@ -107,10 +107,6 @@ Files: static/generated/emoji/images/emoji/unicode/*
Copyright: Google, Inc. Copyright: Google, Inc.
License: Apache-2.0 License: Apache-2.0
Files: static/third/jquery-filedrop/jquery.filedrop.js
Copyright: Resopollution
License: Expat
Files: static/third/jquery-idle/jquery.idle.js Files: static/third/jquery-idle/jquery.idle.js
Copyright: 2011-2013 Henrique Boaventura Copyright: 2011-2013 Henrique Boaventura
License: Expat License: Expat

View File

@ -812,65 +812,6 @@ run_test('finish', () => {
}()); }());
}); });
run_test('abort_xhr', () => {
$("#compose-send-button").attr('disabled', 'disabled');
let compose_removedata_checked = false;
$('#compose').removeData = function (sel) {
assert.equal(sel, 'filedrop_xhr');
compose_removedata_checked = true;
};
let xhr_abort_checked = false;
$("#compose").data = function (sel) {
assert.equal(sel, 'filedrop_xhr');
return {
abort: function () {
xhr_abort_checked = true;
},
};
};
compose.abort_xhr();
assert.equal($("#compose-send-button").attr(), undefined);
assert(xhr_abort_checked);
assert(compose_removedata_checked);
});
function verify_filedrop_payload(payload) {
assert.equal(payload.url, '/json/user_uploads');
assert.equal(payload.fallback_id, 'file_input');
assert.equal(payload.paramname, 'file');
assert.equal(payload.max_file_upload_size, 512);
assert.equal(payload.data.csrfmiddlewaretoken, 'fake-csrf-token');
assert.deepEqual(payload.raw_droppable, ['text/uri-list', 'text/plain']);
assert.equal(typeof payload.drop, 'function');
assert.equal(typeof payload.progressUpdated, 'function');
assert.equal(typeof payload.error, 'function');
assert.equal(typeof payload.uploadFinished, 'function');
assert.equal(typeof payload.rawDrop, 'function');
}
function test_raw_file_drop(raw_drop_func) {
compose_state.set_message_type(false);
let compose_actions_start_checked = false;
global.compose_actions = {
start: function (msg_type) {
assert.equal(msg_type, 'stream');
compose_actions_start_checked = true;
},
};
$("#compose-textarea").val('Old content ');
let compose_ui_autosize_textarea_checked = false;
compose_ui.autosize_textarea = function () {
compose_ui_autosize_textarea_checked = true;
};
// Call the method here!
raw_drop_func('new contents');
assert(compose_actions_start_checked);
assert.equal($("#compose-textarea").val(), 'Old content new contents');
assert(compose_ui_autosize_textarea_checked);
}
run_test('warn_if_private_stream_is_linked', () => { run_test('warn_if_private_stream_is_linked', () => {
stream_data.add_sub({ stream_data.add_sub({
name: compose_state.stream_name(), name: compose_state.stream_name(),
@ -954,13 +895,18 @@ run_test('initialize', () => {
global.document = 'document-stub'; global.document = 'document-stub';
global.csrf_token = 'fake-csrf-token'; global.csrf_token = 'fake-csrf-token';
let filedrop_in_compose_checked = false;
page_params.max_file_upload_size = 512; page_params.max_file_upload_size = 512;
$("#compose").filedrop = function (payload) {
verify_filedrop_payload(payload);
test_raw_file_drop(payload.rawDrop);
filedrop_in_compose_checked = true; let setup_upload_called = false;
let uppy_cancel_all_called = false;
upload.setup_upload = function (config) {
assert.equal(config.mode, "compose");
setup_upload_called = true;
return {
cancelAll: () => {
uppy_cancel_all_called = true;
},
};
}; };
compose.initialize(); compose.initialize();
@ -968,14 +914,11 @@ run_test('initialize', () => {
assert(resize_watch_manual_resize_checked); assert(resize_watch_manual_resize_checked);
assert(xmlhttprequest_checked); assert(xmlhttprequest_checked);
assert(!$("#compose #attach_files").hasClass("notdisplayed")); assert(!$("#compose #attach_files").hasClass("notdisplayed"));
assert(filedrop_in_compose_checked); assert(setup_upload_called);
function reset_jquery() { function reset_jquery() {
// Avoid leaks. // Avoid leaks.
set_global('$', global.make_zjquery()); set_global('$', global.make_zjquery());
// Bypass filedrop (we already tested it above).
$("#compose").filedrop = noop;
} }
let compose_actions_start_checked; let compose_actions_start_checked;
@ -1013,6 +956,18 @@ run_test('initialize', () => {
assert(compose_actions_start_checked); assert(compose_actions_start_checked);
}()); }());
(function test_abort_xhr() {
$("#compose-send-button").attr('disabled', 'disabled');
reset_jquery();
compose.initialize();
compose.abort_xhr();
assert.equal($("#compose-send-button").attr(), undefined);
assert(uppy_cancel_all_called);
}());
}); });
run_test('update_fade', () => { run_test('update_fade', () => {

View File

@ -212,13 +212,18 @@ run_test('start', () => {
// Cancel compose. // Cancel compose.
let pill_cleared; let pill_cleared;
compose_pm_pill.clear = function () { compose_pm_pill.clear = function () {
pill_cleared = true; pill_cleared = true;
}; };
let abort_xhr_called = false;
compose.abort_xhr = () => {
abort_xhr_called = true;
};
assert_hidden('#compose_controls'); assert_hidden('#compose_controls');
cancel(); cancel();
assert(abort_xhr_called);
assert(pill_cleared); assert(pill_cleared);
assert_visible('#compose_controls'); assert_visible('#compose_controls');
assert_hidden('#private-message'); assert_hidden('#private-message');

View File

@ -162,7 +162,7 @@ page_params.starred_messages = [];
page_params.presences = []; page_params.presences = [];
$('#tab_bar').append = () => {}; $('#tab_bar').append = () => {};
$('#compose').filedrop = () => {}; upload.setup_upload = () => {};
server_events.home_view_loaded = () => true; server_events.home_view_loaded = () => true;

View File

@ -1,3 +1,5 @@
const rewiremock = require("rewiremock/node");
set_global('$', global.make_zjquery()); set_global('$', global.make_zjquery());
set_global('document', { set_global('document', {
location: { }, location: { },
@ -9,7 +11,7 @@ set_global('i18n', global.stub_i18n);
set_global('page_params', { set_global('page_params', {
max_file_upload_size: 25, max_file_upload_size: 25,
}); });
set_global('csrf_token', { }); set_global('csrf_token', "csrf_token");
set_global('bridge', false); set_global('bridge', false);
// Setting these up so that we can test that links to uploads within messages are // Setting these up so that we can test that links to uploads within messages are
@ -20,177 +22,500 @@ global.document.location.host = 'foo.com';
zrequire('compose_ui'); zrequire('compose_ui');
zrequire('compose_state'); zrequire('compose_state');
zrequire('compose'); zrequire('compose');
zrequire('compose_actions');
const plugin_stub = {
prototype: {
constructor: null,
},
};
zrequire('upload'); zrequire('upload');
const upload_opts = upload.options({ mode: "compose" }); run_test('make_upload_absolute', () => {
let uri = "/user_uploads/5/d4/6lSlfIPIg9nDI2Upj0Mq_EbE/kerala.png";
const expected_uri = "https://foo.com/user_uploads/5/d4/6lSlfIPIg9nDI2Upj0Mq_EbE/kerala.png";
assert.equal(upload.make_upload_absolute(uri), expected_uri);
run_test('upload_started', () => { uri = "https://foo.com/user_uploads/5/d4/6lSlfIPIg9nDI2Upj0Mq_EbE/alappuzha.png";
$("#compose-send-button").prop('disabled', false); assert.equal(upload.make_upload_absolute(uri), uri);
});
run_test('get_item', () => {
assert.equal(upload.get_item("textarea", {mode: "compose"}), $('#compose-textarea'));
assert.equal(upload.get_item("send_status_message", {mode: "compose"}), $('#compose-error-msg'));
assert.equal(upload.get_item("file_input_identifier", {mode: "compose"}), "#file_input");
assert.equal(upload.get_item("source", {mode: "compose"}), "compose-file-input");
assert.equal(upload.get_item("drag_drop_container", {mode: "compose"}), $('#compose'));
assert.equal(upload.get_item("textarea", {mode: "edit", row: 1}), $('#message_edit_content_1'));
$('#message_edit_content_2').closest = () => {
$('#message_edit_form').set_find_results('.message_edit_save', $('.message_edit_save'));
return $('#message_edit_form');
};
assert.equal(upload.get_item("send_button", {mode: "edit", row: 2}), $('.message_edit_save'));
assert.equal(upload.get_item("send_status_identifier", {mode: "edit", row: 11}), "#message-edit-send-status-11");
assert.equal(upload.get_item("send_status", {mode: "edit", row: 75}), $("#message-edit-send-status-75"));
$('#message-edit-send-status-2').set_find_results('.send-status-close', $('.send-status-close'));
assert.equal(upload.get_item("send_status_close_button", {mode: "edit", row: 2}), $('.send-status-close'));
$('#message-edit-send-status-22').set_find_results('.error-msg', $('.error-msg'));
assert.equal(upload.get_item("send_status_message", {mode: "edit", row: 22}), $('.error-msg'));
assert.equal(upload.get_item("file_input_identifier", {mode: "edit", row: 123}), "#message_edit_file_input_123");
assert.equal(upload.get_item("source", {mode: "edit", row: 123}), "message-edit-file-input");
assert.equal(upload.get_item("drag_drop_container", {mode: "edit", row: 1}), $("#message_edit_form"));
assert.throws(
() => {
upload.get_item("textarea");
},
{
name: "Error",
message: "Missing config",
}
);
assert.throws(
() => {
upload.get_item("textarea", {mode: "edit"});
},
{
name: "Error",
message: "Missing row in config",
}
);
assert.throws(
() => {
upload.get_item("textarea", {mode: "blah"});
},
{
name: "Error",
message: "Invalid upload mode!",
}
);
assert.throws(
() => {
upload.get_item("invalid", {mode: "compose"});
},
{
name: "Error",
message: 'Invalid key name for mode "compose"',
}
);
assert.throws(
() => {
upload.get_item("invalid", {mode: "edit", row: 20});
},
{
name: "Error",
message: 'Invalid key name for mode "edit"',
}
);
});
run_test('hide_upload_status', () => {
$('#compose-send-button').prop("disabled", "");
$('#compose-send-status').addClass("alert-info").show();
upload.hide_upload_status({mode: "compose"});
assert.equal($('#compose-send-button').prop("disabled"), false);
assert.equal($('#compose-send-button').hasClass("alert-info"), false);
assert.equal($('#compose-send-button').visible(), false);
});
run_test('show_error_message', () => {
$('#compose-send-button').prop("disabled", "");
$('#compose-send-status').addClass("alert-info").removeClass("alert-error").hide();
$('#compose-error-msg').text("");
$('#compose-error-msg').hide();
upload.show_error_message({mode: "compose"}, "Error message");
assert.equal($('#compose-send-button').prop("disabled"), false);
assert($('#compose-send-status').hasClass("alert-error"));
assert.equal($('#compose-send-status').hasClass("alert-info"), false);
assert($('#compose-send-status').visible());
assert.equal($('#compose-error-msg').text(), "Error message");
upload.show_error_message({mode: "compose"});
assert.equal($('#compose-error-msg').text(), "translated: An unknown error occurred.");
});
run_test('upload_files', () => {
let cancel_all_counter = 0;
const files = [
{
name: "budapest.png",
type: "image/png",
},
];
let uppy_add_file_called = false;
const uppy = {
cancelAll: () => {
cancel_all_counter += 1;
},
addFile: (params) => {
uppy_add_file_called = true;
assert.equal(params.source, "compose-file-input");
assert.equal(params.name, "budapest.png");
assert.equal(params.type, "image/png");
assert.equal(params.data, files[0]);
},
};
let hide_upload_status_called = false;
upload.hide_upload_status = (config) => {
hide_upload_status_called = true;
assert(config.mode, "compose");
};
const config = {mode: "compose"};
upload.upload_files(uppy, config, []);
assert.equal(cancel_all_counter, 1);
assert(hide_upload_status_called);
page_params.max_file_upload_size = 0;
let show_error_message_called = false;
upload.show_error_message = (config, message) => {
show_error_message_called = true;
assert.equal(config.mode, "compose");
assert.equal(message, "translated: File and image uploads have been disabled for this organization.");
};
upload.upload_files(uppy, config, files);
assert(show_error_message_called);
page_params.max_file_upload_size = 25;
let on_click_close_button_callback;
$(".compose-send-status-close").one = (event, callback) => {
assert.equal(event, "click");
on_click_close_button_callback = callback;
};
let compose_ui_insert_syntax_and_focus_called = false;
compose_ui.insert_syntax_and_focus = (syntax, textarea) => {
assert.equal(syntax, "[Uploading budapest.png…]()");
assert.equal(textarea, $("#compose-textarea"));
compose_ui_insert_syntax_and_focus_called = true;
};
let compose_ui_autosize_textarea_called = false;
compose_ui.autosize_textarea = () => {
compose_ui_autosize_textarea_called = true;
};
$("#compose-send-button").attr("disabled", false);
$("#compose-send-status").removeClass("alert-info").hide(); $("#compose-send-status").removeClass("alert-info").hide();
$(".compose-send-status-close").one = function (ev_name, handler) { upload.upload_files(uppy, config, files);
assert.equal(ev_name, 'click');
assert(handler);
};
$("#compose-error-msg").html('');
const test_html = '<div class="progress active">' +
'<div class="bar" id="compose-upload-bar-1549958107000" style="width: 0"></div>' +
'</div>';
$("#compose-send-status").append = function (html) {
assert.equal(html, test_html);
};
$('#compose-textarea').caret = function () {
return 0;
};
document.execCommand = function (command, show_default, value) {
assert.equal(value, "[Uploading some-file…]() ");
};
upload_opts.drop();
upload_opts.uploadStarted(0, {
trackingId: "1549958107000",
name: 'some-file',
}, 1);
assert.equal($("#compose-send-button").attr("disabled"), ''); assert.equal($("#compose-send-button").attr("disabled"), '');
assert($("#compose-send-status").hasClass("alert-info")); assert($("#compose-send-status").hasClass("alert-info"));
assert($("#compose-send-status").visible()); assert($("#compose-send-status").visible());
assert.equal($("<p>").text(), 'translated: Uploading…'); assert.equal($("<p>").text(), 'translated: Uploading…');
assert(compose_ui_insert_syntax_and_focus_called);
assert(compose_ui_autosize_textarea_called);
assert(uppy_add_file_called);
global.patch_builtin("setTimeout", (func) => {
func();
});
hide_upload_status_called = false;
on_click_close_button_callback();
assert.equal(cancel_all_counter, 2);
assert(hide_upload_status_called);
}); });
run_test('progress_updated', () => { run_test('uppy_config', () => {
let width_update_checked = false; let uppy_stub_called = false;
$("#compose-upload-bar-1549958107000").width = function (width_percent) { let uppy_set_meta_called = false;
assert.equal(width_percent, '39%'); let uppy_used_xhrupload = false;
width_update_checked = true; let uppy_used_progressbar = false;
function uppy_stub(config) {
uppy_stub_called = true;
assert.equal(config.debug, false);
assert.equal(config.autoProceed, true);
assert.equal(config.restrictions.maxFileSize, 25 * 1024 * 1024);
assert.equal(Object.keys(config.locale.strings).length, 2);
assert("exceedsSize" in config.locale.strings);
return {
setMeta: (params) => {
uppy_set_meta_called = true;
assert.equal(params.csrfmiddlewaretoken, 'csrf_token');
},
use: (func, params) => {
const func_name = func.name;
if (func_name === "XHRUpload") {
uppy_used_xhrupload = true;
assert.equal(params.endpoint, '/json/user_uploads');
assert.equal(params.formData, true);
assert.equal(params.fieldName, 'file');
assert.equal(params.limit, 5);
assert.equal(Object.keys(params.locale.strings).length, 1);
assert("timedOut" in params.locale.strings);
} else if (func_name === "ProgressBar") {
uppy_used_progressbar = true;
assert.equal(params.target, '#compose-send-status');
assert.equal(params.hideAfterFinish, false);
} else {
/* istanbul ignore next */
assert.fail(`Missing tests for ${func_name}`);
}
},
on: () => {},
};
}
uppy_stub.Plugin = plugin_stub;
rewiremock.proxy(() => require("../../static/js/upload"), {'@uppy/core': uppy_stub});
upload.setup_upload({mode: "compose"});
assert.equal(uppy_stub_called, true);
assert.equal(uppy_set_meta_called, true);
assert.equal(uppy_used_xhrupload, true);
assert.equal(uppy_used_progressbar, true);
});
run_test('file_input', () => {
set_global('$', global.make_zjquery());
upload.setup_upload({mode: "compose"});
const change_handler = $("body").get_on_handler("change", "#file_input");
const files = ["file1", "file2"];
const event = {
target: {
files: files,
},
}; };
upload_opts.progressUpdated(1, {trackingId: "1549958107000"}, 39); let upload_files_called = false;
assert(width_update_checked); upload.upload_files = (uppy, config, files) => {
assert.equal(config.mode, "compose");
assert.equal(files, files);
upload_files_called = true;
};
change_handler(event);
assert(upload_files_called);
}); });
run_test('upload_error', () => { run_test('file_drop', () => {
function setup_test() { set_global('$', global.make_zjquery());
$("#compose-send-status").removeClass("alert-error");
$("#compose-send-status").addClass("alert-info");
$("#compose-send-button").attr("disabled", 'disabled');
$("#compose-error-msg").text('');
$("#compose-upload-bar-1549958107000").parent = function () { upload.setup_upload({mode: "compose"});
return { remove: function () {} };
let prevent_default_counter = 0;
const drag_event = {
preventDefault: () => {
prevent_default_counter += 1;
},
};
const dragover_handler = $("#compose").get_on_handler("dragover");
dragover_handler(drag_event);
assert.equal(prevent_default_counter, 1);
const dragenter_handler = $("#compose").get_on_handler("dragenter");
dragenter_handler(drag_event);
assert.equal(prevent_default_counter, 2);
const files = ["file1", "file2"];
const drop_event = {
preventDefault: () => {
prevent_default_counter += 1;
},
originalEvent: {
dataTransfer: {
files: files,
},
},
};
const drop_handler = $("#compose").get_on_handler("drop");
let upload_files_called = false;
upload.upload_files = () => {upload_files_called = true;};
drop_handler(drop_event);
assert.equal(prevent_default_counter, 3);
assert.equal(upload_files_called, true);
});
run_test('copy_paste', () => {
set_global('$', global.make_zjquery());
upload.setup_upload({mode: "compose"});
const paste_handler = $("#compose").get_on_handler("paste");
let get_as_file_called = false;
let event = {
originalEvent: {
clipboardData: {
items: [
{
kind: "file",
getAsFile: () => {
get_as_file_called = true;
},
},
{
kind: "notfile",
},
],
},
},
};
let upload_files_called = false;
upload.upload_files = () => {
upload_files_called = true;
};
paste_handler(event);
assert(get_as_file_called);
assert(upload_files_called);
upload_files_called = false;
event = {
originalEvent: {},
};
paste_handler(event);
assert.equal(upload_files_called, false);
});
run_test('uppy_events', () => {
set_global('$', global.make_zjquery());
const callbacks = {};
let uppy_cancel_all_counter = 0;
let state = {};
function uppy_stub() {
return {
setMeta: () => {},
use: () => {},
cancelAll: () => {
uppy_cancel_all_counter += 1;
},
on: (event_name, callback) => {
callbacks[event_name] = callback;
},
getState: () => {
return {
info: {
type: state.type,
details: state.details,
message: state.message,
},
};
},
}; };
} }
uppy_stub.Plugin = plugin_stub;
rewiremock.proxy(() => require("../../static/js/upload"), {'@uppy/core': uppy_stub});
upload.setup_upload({mode: "compose"});
assert.equal(Object.keys(callbacks).length, 4);
function assert_side_effects(msg) { const on_upload_success_callback = callbacks["upload-success"];
assert($("#compose-send-status").hasClass("alert-error")); const file = {
assert(!$("#compose-send-status").hasClass("alert-info")); name: "copenhagen.png",
assert.equal($("#compose-send-button").prop("disabled"), false); };
assert.equal($("#compose-error-msg").text(), msg); let response = {
} body: {
uri: "/user_uploads/4/cb/rue1c-MlMUjDAUdkRrEM4BTJ/copenhagen.png",
},
};
let compose_actions_start_called = false;
compose_actions.start = () => {
compose_actions_start_called = true;
};
let compose_ui_replace_syntax_called = false;
compose_ui.replace_syntax = (old_syntax, new_syntax, textarea) => {
compose_ui_replace_syntax_called = true;
assert.equal(old_syntax, "[Uploading copenhagen.png…]()");
assert.equal(new_syntax, "[copenhagen.png](https://foo.com/user_uploads/4/cb/rue1c-MlMUjDAUdkRrEM4BTJ/copenhagen.png)");
assert.equal(textarea, $('#compose-textarea'));
};
let compose_ui_autosize_textarea_called = false;
compose_ui.autosize_textarea = () => {
compose_ui_autosize_textarea_called = true;
};
on_upload_success_callback(file, response);
assert(compose_actions_start_called);
assert(compose_ui_replace_syntax_called);
assert(compose_ui_autosize_textarea_called);
function test(err, msg, server_response = null, file = {}) { response = {
setup_test(); body: {
file.trackingId = "1549958107000"; uri: undefined,
upload_opts.error(err, server_response, file); },
assert_side_effects(msg); };
} compose_actions_start_called = false;
compose_ui_replace_syntax_called = false;
compose_ui_autosize_textarea_called = false;
on_upload_success_callback(file, response);
assert.equal(compose_actions_start_called, false);
assert.equal(compose_ui_replace_syntax_called, false);
assert.equal(compose_ui_autosize_textarea_called, false);
const msg_prefix = 'translated: '; const on_complete_callback = callbacks.complete;
const msg_1 = 'File upload is not yet available for your browser.'; global.patch_builtin('setTimeout', (func) => {
const msg_2 = 'Unable to upload that many files at once.'; func();
const msg_3 = '"foobar.txt" was too large; the maximum file size is 25MB.'; });
const msg_4 = 'Sorry, the file was too large.'; let hide_upload_status_called = false;
const msg_5 = 'An unknown error occurred.'; upload.hide_upload_status = () => {
const msg_6 = 'File and image uploads have been disabled for this organization.'; hide_upload_status_called = true;
};
assert.equal(uppy_cancel_all_counter, 0);
on_complete_callback();
assert.equal(uppy_cancel_all_counter, 1);
assert(hide_upload_status_called);
test('BrowserNotSupported', msg_prefix + msg_1); state = {
test('TooManyFiles', msg_prefix + msg_2); type: "error",
test('FileTooLarge', msg_prefix + msg_3, null, {name: 'foobar.txt'}); details: "Some Error",
test(413, msg_prefix + msg_4); message: "Some error message",
test(400, 'ちょっと…', {msg: 'ちょっと…'}); };
test('Do-not-match-any-case', msg_prefix + msg_5); const on_info_visible_callback = callbacks["info-visible"];
let show_error_message_called = false;
upload.show_error_message = (config, message) => {
show_error_message_called = true;
assert.equal(config.mode, "compose");
assert.equal(message, "Some error message");
};
on_info_visible_callback();
assert.equal(uppy_cancel_all_counter, 2);
assert(show_error_message_called);
// If uploading files has been disabled, then a different error message is state = {
// displayed when a user tries to paste or drag a file onto the UI. type: "error",
page_params.max_file_upload_size = 0; message: "No Internet connection",
test('FileTooLarge', msg_prefix + msg_6, null); };
}); on_info_visible_callback();
assert.equal(uppy_cancel_all_counter, 2);
run_test('upload_finish', () => {
function test(i, response, textbox_val) { state = {
let compose_ui_autosize_textarea_checked = false; type: "error",
let compose_actions_start_checked = false; details: "Upload Error",
let syntax_to_replace; };
let syntax_to_replace_with; on_info_visible_callback();
let file_input_clear = false; assert.equal(uppy_cancel_all_counter, 2);
function setup() { const on_upload_error_callback = callbacks["upload-error"];
$("#compose-textarea").val(''); show_error_message_called = false;
compose_ui.autosize_textarea = function () { upload.show_error_message = (config, message) => {
compose_ui_autosize_textarea_checked = true; show_error_message_called = true;
}; assert.equal(config.mode, "compose");
compose_ui.replace_syntax = function (old_syntax, new_syntax) { assert.equal(message, "Response message");
syntax_to_replace = old_syntax; };
syntax_to_replace_with = new_syntax; response = {
}; body: {
compose_state.set_message_type(); msg: "Response message",
global.compose_actions = { },
start: function (msg_type) { };
assert.equal(msg_type, 'stream'); on_upload_error_callback(null, null, response);
compose_actions_start_checked = true; assert.equal(uppy_cancel_all_counter, 3);
}, assert(show_error_message_called);
};
$("#compose-send-button").attr('disabled', 'disabled'); upload.show_error_message = (config, message) => {
$("#compose-send-status").addClass("alert-info"); show_error_message_called = true;
$("#compose-send-status").show(); assert.equal(config.mode, "compose");
assert.equal(message, null);
$('#file_input').clone = function (param) { };
assert(param); on_upload_error_callback(null, null);
return $('#file_input'); assert.equal(uppy_cancel_all_counter, 4);
}; assert(show_error_message_called);
$('#file_input').replaceWith = function (elem) {
assert.equal(elem, $('#file_input'));
file_input_clear = true;
};
$("#compose-upload-bar-1549958107000").parent = function () {
return { remove: function () {$('div.progress.active').length = 0;} };
};
}
function assert_side_effects() {
if (response.uri) {
assert.equal(syntax_to_replace, '[Uploading some-file…]()');
assert.equal(syntax_to_replace_with, textbox_val);
assert(compose_actions_start_checked);
assert(compose_ui_autosize_textarea_checked);
assert.equal($("#compose-send-button").prop('disabled'), false);
assert(!$('#compose-send-status').hasClass('alert-info'));
assert(!$('#compose-send-status').visible());
assert(file_input_clear);
}
}
global.patch_builtin('setTimeout', function (func) {
func();
});
$("#compose-upload-bar-1549958107000").width = function (width_percent) {
assert.equal(width_percent, '100%');
};
setup();
upload_opts.uploadFinished(i, {
trackingId: "1549958107000",
name: 'some-file',
}, response);
upload_opts.progressUpdated(1, {trackingId: "1549958107000"}, 100);
assert_side_effects();
}
const msg_1 = '[pasted image](https://foo.com/uploads/122456)';
const msg_2 = '[foobar.jpeg](https://foo.com/user_uploads/foobar.jpeg)';
test(-1, {}, '');
test(-1, {uri: 'https://foo.com/uploads/122456'}, msg_1);
test(1, {uri: '/user_uploads/foobar.jpeg'}, msg_2);
}); });

View File

@ -11,6 +11,9 @@
"@babel/preset-env": "^7.5.5", "@babel/preset-env": "^7.5.5",
"@babel/preset-typescript": "^7.3.3", "@babel/preset-typescript": "^7.3.3",
"@babel/register": "^7.6.2", "@babel/register": "^7.6.2",
"@uppy/core": "^1.7.1",
"@uppy/progress-bar": "^1.3.4",
"@uppy/xhr-upload": "^1.4.2",
"autoprefixer": "^9.6.1", "autoprefixer": "^9.6.1",
"autosize": "^4.0.2", "autosize": "^4.0.2",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",

View File

@ -3,7 +3,6 @@ import "./common.js";
// Import Third party libraries // Import Third party libraries
import "../../third/bootstrap-notify/js/bootstrap-notify.js"; import "../../third/bootstrap-notify/js/bootstrap-notify.js";
import "../../third/bootstrap-typeahead/typeahead.js"; import "../../third/bootstrap-typeahead/typeahead.js";
import "../../third/jquery-filedrop/jquery.filedrop.js";
import "jquery-caret-plugin/src/jquery.caret.js"; import "jquery-caret-plugin/src/jquery.caret.js";
import "../../third/jquery-idle/jquery.idle.js"; import "../../third/jquery-idle/jquery.idle.js";
import "spectrum-colorpicker"; import "spectrum-colorpicker";

View File

@ -18,3 +18,5 @@ import "font-awesome/css/font-awesome.css";
import "../../assets/icons/zulip-icons.font.js"; import "../../assets/icons/zulip-icons.font.js";
import "source-sans-pro/source-sans-pro.css"; import "source-sans-pro/source-sans-pro.css";
import "../../styles/pygments.scss"; import "../../styles/pygments.scss";
import "@uppy/core/dist/style.css";
import "@uppy/progress-bar/dist/style.css";

View File

@ -17,6 +17,7 @@ const render_compose_private_stream_alert = require("../templates/compose_privat
let user_acknowledged_all_everyone; let user_acknowledged_all_everyone;
let user_acknowledged_announce; let user_acknowledged_announce;
let wildcard_mention; let wildcard_mention;
let uppy;
exports.all_everyone_warn_threshold = 15; exports.all_everyone_warn_threshold = 15;
exports.announce_warn_threshold = 60; exports.announce_warn_threshold = 60;
@ -146,11 +147,7 @@ function update_fade() {
exports.abort_xhr = function () { exports.abort_xhr = function () {
$("#compose-send-button").prop("disabled", false); $("#compose-send-button").prop("disabled", false);
const xhr = $("#compose").data("filedrop_xhr"); uppy.cancelAll();
if (xhr !== undefined) {
xhr.abort();
$("#compose").removeData("filedrop_xhr");
}
}; };
exports.empty_topic_placeholder = function () { exports.empty_topic_placeholder = function () {
@ -1090,11 +1087,9 @@ exports.initialize = function () {
exports.clear_preview_area(); exports.clear_preview_area();
}); });
$("#compose").filedrop( uppy = upload.setup_upload({
upload.options({ mode: "compose",
mode: 'compose', });
})
);
$("#compose-textarea").focus(function () { $("#compose-textarea").focus(function () {
const opts = { const opts = {

View File

@ -316,8 +316,7 @@ exports.paste_handler = function (event) {
const mdImageRegex = /^!\[.*\]\(.*\)$/; const mdImageRegex = /^!\[.*\]\(.*\)$/;
if (text.match(mdImageRegex)) { if (text.match(mdImageRegex)) {
// This block catches cases where we are pasting an // This block catches cases where we are pasting an
// image into Zulip, which should be handled by the // image into Zulip, which is handled by upload.js.
// jQuery filedrop library, not this code path.
return; return;
} }
event.preventDefault(); event.preventDefault();

View File

@ -375,12 +375,10 @@ function start_edit_with_content(row, content, edit_box_open_callback) {
edit_box_open_callback(); edit_box_open_callback();
} }
row.find('#message_edit_form').filedrop( upload.setup_upload({
upload.options({ mode: 'edit',
mode: 'edit', row: rows.id(row),
row: rows.id(row), });
})
);
} }
exports.start = function (row, edit_box_open_callback) { exports.start = function (row, edit_box_open_callback) {

View File

@ -1,10 +1,14 @@
function make_upload_absolute(uri) { const Uppy = require('@uppy/core');
const XHRUpload = require('@uppy/xhr-upload');
const ProgressBar = require('@uppy/progress-bar');
exports.make_upload_absolute = function (uri) {
if (uri.startsWith(compose.uploads_path)) { if (uri.startsWith(compose.uploads_path)) {
// Rewrite the URI to a usable link // Rewrite the URI to a usable link
return compose.uploads_domain + uri; return compose.uploads_domain + uri;
} }
return uri; return uri;
} };
// Show the upload button only if the browser supports it. // Show the upload button only if the browser supports it.
exports.feature_check = function (upload_button) { exports.feature_check = function (upload_button) {
@ -13,187 +17,235 @@ exports.feature_check = function (upload_button) {
} }
}; };
exports.options = function (config) { exports.get_item = function (key, config) {
let textarea; if (!config) {
let send_button; throw Error("Missing config");
let send_status; }
let send_status_close; if (config.mode === "compose") {
let error_msg; switch (key) {
let upload_bar; case "textarea":
let file_input; return $('#compose-textarea');
case "send_button":
switch (config.mode) { return $('#compose-send-button');
case 'compose': case "send_status_identifier":
textarea = $('#compose-textarea'); return '#compose-send-status';
send_button = $('#compose-send-button'); case "send_status":
send_status = $('#compose-send-status'); return $('#compose-send-status');
send_status_close = $('.compose-send-status-close'); case "send_status_close_button":
error_msg = $('#compose-error-msg'); return $('.compose-send-status-close');
upload_bar = 'compose-upload-bar'; case "send_status_message":
file_input = 'file_input'; return $('#compose-error-msg');
break; case "file_input_identifier":
case 'edit': return "#file_input";
textarea = $('#message_edit_content_' + config.row); case "source":
send_button = textarea.closest('#message_edit_form').find('.message_edit_save'); return "compose-file-input";
send_status = $('#message-edit-send-status-' + config.row); case "drag_drop_container":
send_status_close = send_status.find('.send-status-close'); return $("#compose");
error_msg = send_status.find('.error-msg'); default:
upload_bar = 'message-edit-upload-bar-' + config.row; throw Error(`Invalid key name for mode "${config.mode}"`);
file_input = 'message_edit_file_input_' + config.row; }
break; } else if (config.mode === "edit") {
default: if (!config.row) {
throw Error("Missing row in config");
}
switch (key) {
case "textarea":
return $('#message_edit_content_' + config.row);
case "send_button":
return $('#message_edit_content_' + config.row).closest('#message_edit_form').find('.message_edit_save');
case "send_status_identifier":
return '#message-edit-send-status-' + config.row;
case "send_status":
return $('#message-edit-send-status-' + config.row);
case "send_status_close_button":
return $('#message-edit-send-status-' + config.row).find('.send-status-close');
case "send_status_message":
return $('#message-edit-send-status-' + config.row).find('.error-msg');
case "file_input_identifier":
return '#message_edit_file_input_' + config.row;
case "source":
return "message-edit-file-input";
case "drag_drop_container":
return $("#message_edit_form");
default:
throw Error(`Invalid key name for mode "${config.mode}"`);
}
} else {
throw Error("Invalid upload mode!"); throw Error("Invalid upload mode!");
} }
};
const hide_upload_status = function () { exports.hide_upload_status = function (config) {
send_button.prop("disabled", false); exports.get_item("send_button", config).prop("disabled", false);
send_status.removeClass("alert-info").hide(); exports.get_item("send_status", config).removeClass("alert-info").hide();
$('div.progress.active').remove(); };
};
const drop = function () { exports.show_error_message = function (config, message) {
send_button.attr("disabled", ""); if (!message) {
send_status.addClass("alert-info").show(); message = i18n.t("An unknown error occurred.");
send_status_close.one('click', function () { }
setTimeout(function () { exports.get_item("send_button", config).prop("disabled", false);
hide_upload_status(); exports.get_item("send_status", config).addClass("alert-error").removeClass("alert-info").show();
}, 500); exports.get_item("send_status_message", config).text(message);
compose.abort_xhr(); };
});
};
const uploadStarted = function (i, file) { exports.upload_files = function (uppy, config, files) {
error_msg.html($("<p>").text(i18n.t("Uploading…"))); if (files.length === 0) {
// file.lastModified is unique for each upload, and was previously used to track each uppy.cancelAll();
// upload. But, when an image is pasted into Safari, it looks like the lastModified time exports.hide_upload_status(config);
// gets changed by the time the image upload is finished, and we lose track of the return;
// uploaded images. Instead, we set a random ID for each image, to track it. }
if (!file.trackingId) { // The conditional check is present to make this easy to test if (page_params.max_file_upload_size === 0) {
file.trackingId = Math.random().toString().substring(2); // Use digits after the `.` exports.show_error_message(config, i18n.t('File and image uploads have been disabled for this organization.'));
return;
}
exports.get_item("send_button", config).attr("disabled", "");
exports.get_item("send_status", config).addClass("alert-info").removeClass("alert-error").show();
exports.get_item("send_status_message", config).html($("<p>").text(i18n.t("Uploading…")));
exports.get_item("send_status_close_button", config).one('click', function () {
uppy.cancelAll();
setTimeout(function () {
exports.hide_upload_status(config);
}, 500);
});
for (const file of files) {
try {
compose_ui.insert_syntax_and_focus("[Uploading " + file.name + "…]()", exports.get_item("textarea", config));
compose_ui.autosize_textarea();
uppy.addFile({
source: exports.get_item("source", config),
name: file.name,
type: file.type,
data: file,
});
} catch (error) {
// Errors are handled by info-visible and upload-error event callbacks.
} }
send_status.append('<div class="progress active">' + }
'<div class="bar" id="' + upload_bar + '-' + file.trackingId + '" style="width: 0"></div>' + };
'</div>');
compose_ui.insert_syntax_and_focus("[Uploading " + file.name + "…]()", textarea);
};
const progressUpdated = function (i, file, progress) { exports.setup_upload = function (config) {
$("#" + upload_bar + '-' + file.trackingId).width(progress + "%"); const uppy = Uppy({
}; debug: false,
autoProceed: true,
const uploadError = function (error_code, server_response, file) { restrictions: {
let msg; maxFileSize: page_params.max_file_upload_size * 1024 * 1024,
send_status.addClass("alert-error").removeClass("alert-info"); },
send_button.prop("disabled", false); locale: {
if (file !== undefined) { strings: {
$("#" + upload_bar + '-' + file.trackingId).parent().remove(); exceedsSize: i18n.t('This file exceeds maximum allowed size of'),
failedToUpload: i18n.t('Failed to upload %{file}'),
},
},
});
uppy.setMeta({
csrfmiddlewaretoken: csrf_token,
});
uppy.use(
XHRUpload, {
endpoint: '/json/user_uploads',
formData: true,
fieldName: 'file',
// Number of concurrent uploads
limit: 5,
locale: {
strings: {
timedOut: i18n.t('Upload stalled for %{seconds} seconds, aborting.'),
},
},
} }
);
switch (error_code) { uppy.use(ProgressBar, {
case 'BrowserNotSupported': target: exports.get_item("send_status_identifier", config),
msg = i18n.t("File upload is not yet available for your browser."); hideAfterFinish: false,
break; });
case 'TooManyFiles':
msg = i18n.t("Unable to upload that many files at once.");
break;
case 'FileTooLarge':
if (page_params.max_file_upload_size > 0) {
// sanitization not needed as the file name is not potentially parsed as HTML, etc.
const context = {
file_name: file.name,
file_size: page_params.max_file_upload_size,
};
msg = i18n.t('"__file_name__" was too large; the maximum file size is __file_size__MB.',
context);
} else {
// If uploading files has been disabled.
msg = i18n.t('File and image uploads have been disabled for this organization.');
}
break;
case 413: // HTTP status "Request Entity Too Large"
msg = i18n.t("Sorry, the file was too large.");
break;
case 400: {
const server_message = server_response && server_response.msg;
msg = server_message || i18n.t("An unknown error occurred.");
break;
}
default:
msg = i18n.t("An unknown error occurred.");
break;
}
error_msg.text(msg);
};
const uploadFinished = function (i, file, response) { $("body").on("change", exports.get_item("file_input_identifier", config), (event) => {
if (response.uri === undefined) { const files = event.target.files;
exports.upload_files(uppy, config, files);
});
const drag_drop_container = exports.get_item("drag_drop_container", config);
drag_drop_container.on("dragover", (event) => event.preventDefault());
drag_drop_container.on("dragenter", (event) => event.preventDefault());
drag_drop_container.on("drop", (event) => {
event.preventDefault();
const files = event.originalEvent.dataTransfer.files;
exports.upload_files(uppy, config, files);
});
drag_drop_container.on("paste", (event) => {
const clipboard_data = event.clipboardData || event.originalEvent.clipboardData;
if (!clipboard_data) {
return; return;
} }
const split_uri = response.uri.split("/"); const items = clipboard_data.items;
const files = [];
for (const item of items) {
if (item.kind !== "file") {
continue;
}
const file = item.getAsFile();
files.push(file);
}
exports.upload_files(uppy, config, files);
});
uppy.on('upload-success', (file, response) => {
const uri = response.body.uri;
if (uri === undefined) {
return;
}
const split_uri = uri.split("/");
const filename = split_uri[split_uri.length - 1]; const filename = split_uri[split_uri.length - 1];
// Urgh, yet another hack to make sure we're "composing" if (!compose_state.composing()) {
// when text gets added into the composebox.
if (config.mode === 'compose' && !compose_state.composing()) {
compose_actions.start('stream'); compose_actions.start('stream');
} else if (config.mode === 'edit' && document.activeElement !== textarea) {
// If we are editing, focus on the edit message box
textarea.focus();
}
const uri = make_upload_absolute(response.uri);
if (i === -1) {
// This is a paste, so there's no filename. Show the image directly
const pasted_image_uri = "[pasted image](" + uri + ")";
compose_ui.replace_syntax("[Uploading " + file.name + "…]()", pasted_image_uri, textarea);
} else {
// This is a dropped file, so make the filename a link to the image
const filename_uri = "[" + filename + "](" + uri + ")";
compose_ui.replace_syntax("[Uploading " + file.name + "…]()", filename_uri, textarea);
} }
const absolute_uri = upload.make_upload_absolute(uri);
const filename_uri = "[" + filename + "](" + absolute_uri + ")";
compose_ui.replace_syntax("[Uploading " + file.name + "…]()", filename_uri, exports.get_item("textarea", config));
compose_ui.autosize_textarea(); compose_ui.autosize_textarea();
});
uppy.on('complete', () => {
setTimeout(function () { setTimeout(function () {
$("#" + upload_bar + '-' + file.trackingId).parent().remove(); uppy.cancelAll();
if ($('div.progress.active').length === 0) { exports.hide_upload_status(config);
hide_upload_status(file);
}
}, 500); }, 500);
});
// In order to upload the same file twice in a row, we need to clear out uppy.on('info-visible', () => {
// the file input element, so that the next time we use the file dialog, const info = uppy.getState().info;
// an actual change event is fired. IE doesn't allow .val('') so we if (info.type === "error" && info.message === "No Internet connection") {
// need to clone it. (Taken from the jQuery form plugin) // server_events already handles the case of no internet.
if (/MSIE/.test(navigator.userAgent)) { return;
$('#' + file_input).replaceWith($('#' + file_input).clone(true));
} else {
$('#' + file_input).val('');
} }
};
return { if (info.type === "error" && info.details === "Upload Error") {
url: "/json/user_uploads", // The server errors come under 'Upload Error'. But we can't handle them
fallback_id: file_input, // Target for standard file dialog // here because info object don't contain response.body.msg received from
paramname: "file", // the server. Server errors are hence handled by on('upload-error').
max_file_upload_size: page_params.max_file_upload_size, return;
data: { }
// the token isn't automatically included in filedrop's post
csrfmiddlewaretoken: csrf_token, if (info.type === "error") {
}, // The remaining errors are mostly frontend errors like file being too large
raw_droppable: ['text/uri-list', 'text/plain'], // for upload.
drop: drop, uppy.cancelAll();
uploadStarted: uploadStarted, exports.show_error_message(config, info.message);
progressUpdated: progressUpdated, }
error: uploadError, });
uploadFinished: uploadFinished,
rawDrop: function (contents) { uppy.on('upload-error', (file, error, response) => {
if (!compose_state.composing()) { const message = response ? response.body.msg : null;
compose_actions.start('stream'); uppy.cancelAll();
} exports.show_error_message(config, message);
textarea.val(textarea.val() + contents); });
compose_ui.autosize_textarea();
}, return uppy;
};
}; };
window.upload = exports; window.upload = exports;

View File

@ -1,680 +0,0 @@
/*global jQuery:false, alert:false */
/** @preserve
Software from "jQuery Filedrop 0.1.0", a jQuery plugin for html5 dragging files,
is Copyright (c) Resopollution and is provided under the following license:
--
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--
*/
/*
* Default text - jQuery plugin for html5 dragging files from desktop to browser
*
* Author: Weixi Yen
*
* Email: [Firstname][Lastname]@gmail.com
*
* Copyright (c) 2010 Resopollution
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/mit-license.php
*
* Project home:
* http://www.github.com/weixiyen/jquery-filedrop
*
* Version: 0.1.0
*
* Features:
* Allows sending of extra parameters with file.
* Works with Firefox 3.6+
* Future-compliant with HTML5 spec (will work with Webkit browsers and IE9)
* Usage:
* See README at project homepage
*
*/
;(function($) {
var default_opts = {
fallback_id: '',
url: '',
refresh: 1000,
paramname: 'userfile',
allowedfiletypes:[],
raw_droppable:[],
maxfiles: 25, // Ignored if queuefiles is set > 0
max_file_upload_size: 1, // MB file size limit
queuefiles: 0, // Max files before queueing (for large volume uploads)
queuewait: 200, // Queue wait time if full
data: {},
headers: {},
drop: empty,
rawDrop: empty,
dragStart: empty,
dragEnter: empty,
dragOver: empty,
dragLeave: empty,
docEnter: empty,
docOver: empty,
docLeave: empty,
beforeEach: empty,
afterAll: empty,
rename: empty,
error: function(err, response, file, i) {
alert(err);
},
uploadStarted: empty,
uploadFinished: empty,
progressUpdated: empty,
globalProgressUpdated: empty,
speedUpdated: empty
},
errors = ["BrowserNotSupported", "TooManyFiles", "FileTooLarge", "FileTypeNotAllowed", "NotFound", "NotReadable", "AbortError", "ReadError"],
doc_leave_timer, stop_loop = false,
files_count = 0,
files;
$.fn.filedrop = function(options) {
var opts = $.extend({}, default_opts, options),
global_progress = []
// Zulip modification: keep a pointer to the object that the function
// was invoked on.
var caller = this;
this.on('drop', drop).on('dragstart', opts.dragStart).on('dragenter', dragEnter).on('dragover', dragOver).on('dragleave', dragLeave);
this.on('paste', paste);
this.on('imagedata-upload.zulip', uploadRawImageData);
$(document).on('drop', docDrop).on('dragenter', docEnter).on('dragover', docOver).on('dragleave', docLeave);
$('#' + opts.fallback_id).change(function(e) {
opts.drop(e);
files = e.target.files;
files_count = files.length;
upload();
});
function drop(e) {
if (!e.originalEvent.dataTransfer) {
return;
}
files = e.originalEvent.dataTransfer.files;
function has_type(dom_stringlist, type) {
var j;
for (j = 0; j < dom_stringlist.length; j++) {
if (dom_stringlist[j] === type) {
return true;
}
}
return false;
}
if (files.length === 0) {
var i;
for (i = 0; i < opts.raw_droppable.length; i++) {
var type = opts.raw_droppable[i];
if (has_type(e.originalEvent.dataTransfer.types, type)) {
opts.rawDrop(e.originalEvent.dataTransfer.getData(type));
return false;
}
}
}
if( opts.drop.call(this, e) === false ) return false;
if (files === null || files === undefined || files.length === 0) {
opts.error(errors[0]);
return false;
}
files_count = files.length;
upload();
e.preventDefault();
return false;
}
function sendRawImageData(event, image) {
function finished_callback(serverResponse, timeDiff, xhr) {
return opts.uploadFinished(-1, image.file, serverResponse, timeDiff, xhr);
}
var url_params = "?mimetype=" + encodeURIComponent(image.type);
do_xhr("pasted_image", image.data, image.type, {file: image.file}, url_params, finished_callback, function () {});
}
function uploadRawImageData(event, image) {
// Call the user callback to initialize the drop event
if( opts.drop.call(this, undefined) === false ) return false;
sendRawImageData(event, image);
}
function dataIsImage(data) {
// Check if the clipboard data is actually an image or a thumbnail of the
// copied text.
var text = data.getData('text/html');
if (!text) {
// No html is present, when pasting from image viewers or a screenshot
return true;
}
try {
var html = $.parseHTML(text);
} catch(e) {
// This is really a problem with the software, where we copied the text
// from - but we just let the default browser behavior prevail
return false;
}
// Some software like MS Word adds an image thumbnail, when text is
// copied. We would like to paste the actual text, instead of the
// thumbnail image in this case.
// When an image copied in a (modern?) Browser, a 'text/html' item is
// present in the clipboard, which has an img tag for the copied image
// (along with may be a meta tag)
var allowedTags = ["META", "IMG"];
for (var i=0; i < html.length; i += 1){
if (allowedTags.indexOf(html[i].nodeName) < 0){
return false;
}
}
return true;
}
function paste(event) {
if (event.originalEvent.clipboardData === undefined ||
event.originalEvent.clipboardData.items === undefined) {
return;
}
// Check if the data in the clipboard is really an image, or just a
// thumbnail of the copied text.
if (!dataIsImage(event.originalEvent.clipboardData)){
return;
}
// Take the first image pasted in the clipboard
var match_re = /image.*/;
var item;
$.each(event.originalEvent.clipboardData.items, function (idx, this_event) {
if (this_event.type.match(match_re)) {
item = this_event;
return false;
}
});
if (item === undefined) {
return;
}
// Call the user callback to initialize the drop event
if( opts.drop.call(this, event) === false ) return false;
// Read the data of the drop in as binary data, and send it to the server
var data = item.getAsFile();
var reader = new FileReader();
reader.onload = function(event) {
sendRawImageData(event, {type: data.type, data: event.target.result, file: data});
};
reader.readAsBinaryString(data);
opts.uploadStarted(undefined, data);
// Once the upload has started, the event needn't be processed further. This seems to be required on Safari to
// prevent the Copied Image URL from being pasted along with the uploaded image URL.
event.stopPropagation();
event.preventDefault();
}
function getBuilder(filename, filedata, mime, boundary) {
var dashdash = '--',
crlf = '\r\n',
builder = '';
if (opts.data) {
var params = $.param(opts.data).replace(/\+/g, '%20').split(/&/);
$.each(params, function() {
var pair = this.split("=", 2),
name = decodeURIComponent(pair[0]),
val = decodeURIComponent(pair[1]);
builder += dashdash;
builder += boundary;
builder += crlf;
builder += 'Content-Disposition: form-data; name="' + name + '"';
builder += crlf;
builder += crlf;
builder += val;
builder += crlf;
});
}
builder += dashdash;
builder += boundary;
builder += crlf;
builder += 'Content-Disposition: form-data; name="' + opts.paramname + '"';
builder += '; filename="' + encodeURIComponent(filename) + '"';
builder += crlf;
builder += 'Content-Type: ' + mime;
builder += crlf;
builder += crlf;
builder += filedata;
builder += crlf;
builder += dashdash;
builder += boundary;
builder += dashdash;
builder += crlf;
return builder;
}
function progress(e) {
if (e.lengthComputable) {
var percentage = Math.round((e.loaded * 100) / e.total);
if (this.currentProgress !== percentage) {
this.currentProgress = percentage;
opts.progressUpdated(this.index, this.file, this.currentProgress);
global_progress[this.global_progress_index] = this.currentProgress;
globalProgress();
var elapsed = new Date().getTime();
var diffTime = elapsed - this.currentStart;
if (diffTime >= opts.refresh) {
var diffData = e.loaded - this.startData;
var speed = diffData / diffTime; // KB per second
opts.speedUpdated(this.index, this.file, speed);
this.startData = e.loaded;
this.currentStart = elapsed;
}
}
}
}
function globalProgress() {
if (global_progress.length === 0) {
return;
}
var total = 0, index;
for (index in global_progress) {
if(global_progress.hasOwnProperty(index)) {
total = total + global_progress[index];
}
}
opts.globalProgressUpdated(Math.round(total / global_progress.length));
}
function do_xhr(filename, filedata, mime, upload_args, extra_url_args, finished_callback, on_error) {
var xhr = new XMLHttpRequest(),
start_time = new Date().getTime(),
global_progress_index = global_progress.length,
boundary = '------multipartformboundary' + (new Date()).getTime(),
upload = xhr.upload;
// Zulip modification: Shunt the XHR into the parent object so we
// can interrupt it later.
caller.data("filedrop_xhr", xhr);
if (opts.withCredentials) {
xhr.withCredentials = opts.withCredentials;
}
var builder = builder = getBuilder(filename, filedata, mime, boundary);
upload = $.extend(upload, upload_args);
upload.downloadStartTime = start_time;
upload.currentStart = start_time;
upload.currentProgress = 0;
upload.global_progress_index = global_progress_index;
upload.startData = 0;
upload.addEventListener("progress", progress, false);
// Allow url to be a method
if (jQuery.isFunction(opts.url)) {
xhr.open("POST", opts.url() + extra_url_args, true);
} else {
xhr.open("POST", opts.url + extra_url_args, true);
}
xhr.setRequestHeader('content-type', 'multipart/form-data; boundary=' + boundary);
// Add headers
$.each(opts.headers, function(k, v) {
xhr.setRequestHeader(k, v);
});
xhr.sendAsBinary(builder);
global_progress[global_progress_index] = 0;
globalProgress();
xhr.onload = function() {
var serverResponse = null;
if (xhr.responseText) {
try {
serverResponse = JSON.parse(xhr.responseText);
}
catch (e) {
serverResponse = xhr.responseText;
}
}
var now = new Date().getTime(),
timeDiff = now - start_time,
result = finished_callback(serverResponse, timeDiff, xhr);
// Make sure the global progress is updated
global_progress[global_progress_index] = 100;
globalProgress();
if (result === false) {
stop_loop = true;
}
// Pass any errors to the error option
if (xhr.status < 200 || xhr.status > 299) {
on_error(xhr.status, serverResponse);
}
};
}
// Respond to an upload
function upload() {
stop_loop = false;
if (!files) {
opts.error(errors[0]);
return false;
}
if (opts.allowedfiletypes.push && opts.allowedfiletypes.length) {
for(var fileIndex = files.length;fileIndex--;) {
if(!files[fileIndex].type || $.inArray(files[fileIndex].type, opts.allowedfiletypes) < 0) {
opts.error(errors[3], null, files[fileIndex], fileIndex);
return false;
}
}
}
var filesDone = 0,
filesRejected = 0;
if (files_count > opts.maxfiles && opts.queuefiles === 0) {
opts.error(errors[1]);
return false;
}
// Define queues to manage upload process
var workQueue = [];
var processingQueue = [];
var doneQueue = [];
// Add everything to the workQueue
for (var i = 0; i < files_count; i++) {
workQueue.push(i);
}
// Helper function to enable pause of processing to wait
// for in process queue to complete
var pause = function(timeout) {
setTimeout(process, timeout);
return;
};
// Process an upload, recursive
var process = function() {
var fileIndex;
if (stop_loop) {
return false;
}
// Check to see if are in queue mode
if (opts.queuefiles > 0 && processingQueue.length >= opts.queuefiles) {
return pause(opts.queuewait);
} else {
// Take first thing off work queue
fileIndex = workQueue[0];
workQueue.splice(0, 1);
// Add to processing queue
processingQueue.push(fileIndex);
}
try {
if (beforeEach(files[fileIndex]) !== false) {
if (fileIndex === files_count) {
return;
}
var reader = new FileReader(),
max_file_size = 1048576 * opts.max_file_upload_size;
reader.index = fileIndex;
if (files[fileIndex].size > max_file_size) {
opts.error(errors[2], null, files[fileIndex], fileIndex);
// Remove from queue
processingQueue.forEach(function(value, key) {
if (value === fileIndex) {
processingQueue.splice(key, 1);
}
});
filesRejected++;
return true;
}
reader.onerror = function(e) {
switch(e.target.error.code) {
case e.target.error.NOT_FOUND_ERR:
opts.error(errors[4]);
return false;
case e.target.error.NOT_READABLE_ERR:
opts.error(errors[5]);
return false;
case e.target.error.ABORT_ERR:
opts.error(errors[6]);
return false;
default:
opts.error(errors[7]);
return false;
};
};
reader.onloadend = !opts.beforeSend ? send : function (e) {
opts.beforeSend(files[fileIndex], fileIndex, function () { send(e); });
};
reader.readAsBinaryString(files[fileIndex]);
} else {
filesRejected++;
}
} catch (err) {
// Remove from queue
processingQueue.forEach(function(value, key) {
if (value === fileIndex) {
processingQueue.splice(key, 1);
}
});
opts.error(errors[0]);
return false;
}
// If we still have work to do,
if (workQueue.length > 0) {
process();
}
};
var send = function(e) {
var fileIndex = ((typeof(e.srcElement) === "undefined") ? e.target : e.srcElement).index;
// Sometimes the index is not attached to the
// event object. Find it by size. Hack for sure.
if (e.target.index === undefined) {
e.target.index = getIndexBySize(e.total);
}
var file = files[e.target.index],
index = e.target.index;
function finished_callback(serverResponse, timeDiff, xhr) {
filesDone++;
var result = opts.uploadFinished(index, file, serverResponse, timeDiff, xhr);
if (filesDone === (files_count - filesRejected)) {
afterAll();
}
// Remove from processing queue
processingQueue.forEach(function(value, key) {
if (value === fileIndex) {
processingQueue.splice(key, 1);
}
});
// Add to donequeue
doneQueue.push(fileIndex);
return result;
}
function on_error(status_code, response) {
opts.error(status_code, response, file, fileIndex);
}
var fileName,
fileData = e.target.result;
if (typeof newName === "string") {
fileName = newName;
} else {
fileName = file.name;
}
var extra_opts = { file: files[e.target.index],
index: e.target.index };
do_xhr(fileName, fileData, file.type, extra_opts, "", finished_callback, on_error);
opts.uploadStarted(index, file, files_count);
};
// Initiate the processing loop
process();
}
function getIndexBySize(size) {
for (var i = 0; i < files_count; i++) {
if (files[i].size === size) {
return i;
}
}
return undefined;
}
function rename(name) {
return opts.rename(name);
}
function beforeEach(file) {
return opts.beforeEach(file);
}
function afterAll() {
return opts.afterAll();
}
function dragEnter(e) {
clearTimeout(doc_leave_timer);
e.preventDefault();
opts.dragEnter.call(this, e);
}
function dragOver(e) {
clearTimeout(doc_leave_timer);
e.preventDefault();
opts.docOver.call(this, e);
opts.dragOver.call(this, e);
}
function dragLeave(e) {
clearTimeout(doc_leave_timer);
opts.dragLeave.call(this, e);
e.stopPropagation();
}
function docDrop(e) {
e.preventDefault();
opts.docLeave.call(this, e);
return false;
}
function docEnter(e) {
clearTimeout(doc_leave_timer);
e.preventDefault();
opts.docEnter.call(this, e);
return false;
}
function docOver(e) {
clearTimeout(doc_leave_timer);
e.preventDefault();
opts.docOver.call(this, e);
return false;
}
function docLeave(e) {
doc_leave_timer = setTimeout((function(_this) {
return function() {
opts.docLeave.call(_this, e);
};
})(this), 200);
}
return this;
};
function empty() {}
try {
if (XMLHttpRequest.prototype.sendAsBinary) {
return;
}
XMLHttpRequest.prototype.sendAsBinary = function(datastr) {
function byteValue(x) {
return x.charCodeAt(0) & 0xff;
}
var ords = Array.prototype.map.call(datastr, byteValue);
var ui8a = new Uint8Array(ords);
this.send(ui8a.buffer);
};
} catch (e) {}
})(jQuery);

View File

@ -154,7 +154,6 @@ EXEMPT_FILES = {
'static/js/ui_util.js', 'static/js/ui_util.js',
'static/js/unread_ops.js', 'static/js/unread_ops.js',
'static/js/unread_ui.js', 'static/js/unread_ui.js',
'static/js/upload.js',
'static/js/upload_widget.js', 'static/js/upload_widget.js',
'static/js/user_status_ui.js', 'static/js/user_status_ui.js',
'static/js/zcommand.js', 'static/js/zcommand.js',

View File

@ -26,4 +26,4 @@ LATEST_RELEASE_ANNOUNCEMENT = "https://blog.zulip.org/2019/12/13/zulip-2-1-relea
# historical commits sharing the same major version, in which case a # historical commits sharing the same major version, in which case a
# minor version bump suffices. # minor version bump suffices.
PROVISION_VERSION = '73.0' PROVISION_VERSION = '73.1'

View File

@ -1247,6 +1247,55 @@
semver "^6.3.0" semver "^6.3.0"
tsutils "^3.17.1" tsutils "^3.17.1"
"@uppy/companion-client@^1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@uppy/companion-client/-/companion-client-1.4.1.tgz#138032c145ef0961f7f3a047b36b593d6fc772d2"
integrity sha512-ZQpEibQMDRwCzp3zugRHlCl/ne7UpCF+4ZfayhspGt7nz8tuUZXuDH15LhyMS06Y9S/kXTRMrA/w5bY42QtHDw==
dependencies:
namespace-emitter "^2.0.1"
"@uppy/core@^1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@uppy/core/-/core-1.7.1.tgz#cfedd5787a8d5feca604230732617fd9ca454362"
integrity sha512-6ZT3gGMp74lAf+y2VYBWaMZvpo5CSh5HQJW3R2rUcBD9VV+IDyX+Kbt86vGL9bOBOpSCl4BOzZqfLe7kT7nrMg==
dependencies:
"@uppy/store-default" "^1.2.0"
"@uppy/utils" "^2.1.2"
cuid "^2.1.1"
lodash.throttle "^4.1.1"
mime-match "^1.0.2"
namespace-emitter "^2.0.1"
preact "8.2.9"
"@uppy/progress-bar@^1.3.4":
version "1.3.4"
resolved "https://registry.yarnpkg.com/@uppy/progress-bar/-/progress-bar-1.3.4.tgz#b818f18cf11db5e06fb9edb8ba86cc9b99855cc8"
integrity sha512-gpVNpVpUjT7eCVrmK0Z6lKRH5hfAyiwcoT+L6EOoVPW2XnNEKG5jcxIJYqeiSD6OCJoptK4H3fxvzwcQWdNEIA==
dependencies:
"@uppy/utils" "^2.1.2"
preact "8.2.9"
"@uppy/store-default@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@uppy/store-default/-/store-default-1.2.0.tgz#4007b84e6eef24b3f07b0fe5457548386cea77d9"
integrity sha512-mnkxdX4DJMP2nrBklh5MXdn31bAyBSlCcp4+BZanFwv4WiCEpg/ruYzNzaJ1nVyuINJEDj2nx/DWxo+1F6WuWw==
"@uppy/utils@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@uppy/utils/-/utils-2.1.2.tgz#979570c895d2d51a6afd329714714fab1957bbba"
integrity sha512-lMcqHQq1KLm3NcmjOVoVIBuyfw7P8qp2/g8LwvFgAbvCMOYh5DbZWJ7W1FrLFpyAQ4FxXHCbah0m+ZChBC2nzg==
dependencies:
lodash.throttle "^4.1.1"
"@uppy/xhr-upload@^1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@uppy/xhr-upload/-/xhr-upload-1.4.2.tgz#d14485f76c897ba6cd99e0238bc82547bb536961"
integrity sha512-32vqhODv4RVJZ1J6El/iFYa7sY27oubEuT2k1vsuqbsRRXSBHeraqxRyn10RoFuAB+aUmG1u9281LR7Y5YA0Pg==
dependencies:
"@uppy/companion-client" "^1.4.1"
"@uppy/utils" "^2.1.2"
cuid "^2.1.1"
"@vusion/webfonts-generator@^0.6.0": "@vusion/webfonts-generator@^0.6.0":
version "0.6.0" version "0.6.0"
resolved "https://registry.yarnpkg.com/@vusion/webfonts-generator/-/webfonts-generator-0.6.0.tgz#fb904121bcdd4bf31e0980742fc822b2f6cc0c23" resolved "https://registry.yarnpkg.com/@vusion/webfonts-generator/-/webfonts-generator-0.6.0.tgz#fb904121bcdd4bf31e0980742fc822b2f6cc0c23"
@ -3342,6 +3391,11 @@ cubic2quad@^1.0.0:
resolved "https://registry.yarnpkg.com/cubic2quad/-/cubic2quad-1.1.1.tgz#69b19c61a3f5b41ecf2f1d5fae8fb03415aa8b15" resolved "https://registry.yarnpkg.com/cubic2quad/-/cubic2quad-1.1.1.tgz#69b19c61a3f5b41ecf2f1d5fae8fb03415aa8b15"
integrity sha1-abGcYaP1tB7PLx1fro+wNBWqixU= integrity sha1-abGcYaP1tB7PLx1fro+wNBWqixU=
cuid@^2.1.1:
version "2.1.8"
resolved "https://registry.yarnpkg.com/cuid/-/cuid-2.1.8.tgz#cbb88f954171e0d5747606c0139fb65c5101eac0"
integrity sha512-xiEMER6E7TlTPnDxrM4eRiC6TRgjNX9xzEZ5U/Se2YJKr7Mq4pJn/2XEHjl3STcSh96GmkHPcBXLES8M29wyyg==
cwise-compiler@^1.0.0, cwise-compiler@^1.1.1, cwise-compiler@^1.1.2: cwise-compiler@^1.0.0, cwise-compiler@^1.1.1, cwise-compiler@^1.1.2:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/cwise-compiler/-/cwise-compiler-1.1.3.tgz#f4d667410e850d3a313a7d2db7b1e505bb034cc5" resolved "https://registry.yarnpkg.com/cwise-compiler/-/cwise-compiler-1.1.3.tgz#f4d667410e850d3a313a7d2db7b1e505bb034cc5"
@ -7380,6 +7434,13 @@ mime-db@1.43.0, "mime-db@>= 1.43.0 < 2":
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58"
integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==
mime-match@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/mime-match/-/mime-match-1.0.2.tgz#3f87c31e9af1a5fd485fb9db134428b23bbb7ba8"
integrity sha1-P4fDHprxpf1IX7nbE0Qosju7e6g=
dependencies:
wildcard "^1.1.0"
mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
version "2.1.26" version "2.1.26"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06"
@ -7648,6 +7709,11 @@ mute-stream@0.0.8:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
namespace-emitter@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz#978d51361c61313b4e6b8cf6f3853d08dfa2b17c"
integrity sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==
nan@^2.10.0, nan@^2.12.1: nan@^2.10.0, nan@^2.12.1:
version "2.14.0" version "2.14.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
@ -9239,6 +9305,11 @@ potpack@^1.0.1:
resolved "https://registry.yarnpkg.com/potpack/-/potpack-1.0.1.tgz#d1b1afd89e4c8f7762865ec30bd112ab767e2ebf" resolved "https://registry.yarnpkg.com/potpack/-/potpack-1.0.1.tgz#d1b1afd89e4c8f7762865ec30bd112ab767e2ebf"
integrity sha512-15vItUAbViaYrmaB/Pbw7z6qX2xENbFSTA7Ii4tgbPtasxm5v6ryKhKtL91tpWovDJzTiZqdwzhcFBCwiMVdVw== integrity sha512-15vItUAbViaYrmaB/Pbw7z6qX2xENbFSTA7Ii4tgbPtasxm5v6ryKhKtL91tpWovDJzTiZqdwzhcFBCwiMVdVw==
preact@8.2.9:
version "8.2.9"
resolved "https://registry.yarnpkg.com/preact/-/preact-8.2.9.tgz#813ba9dd45e5d97c5ea0d6c86d375b3be711cc40"
integrity sha512-ThuGXBmJS3VsT+jIP+eQufD3L8pRw/PY3FoCys6O9Pu6aF12Pn9zAJDX99TfwRAFOCEKm/P0lwiPTbqKMJp0fA==
prelude-ls@~1.1.2: prelude-ls@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@ -12617,6 +12688,11 @@ wide-align@^1.1.0:
dependencies: dependencies:
string-width "^1.0.2 || 2" string-width "^1.0.2 || 2"
wildcard@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-1.1.2.tgz#a7020453084d8cd2efe70ba9d3696263de1710a5"
integrity sha1-pwIEUwhNjNLv5wup02liY94XEKU=
winchan@^0.2.1: winchan@^0.2.1:
version "0.2.2" version "0.2.2"
resolved "https://registry.yarnpkg.com/winchan/-/winchan-0.2.2.tgz#6766917b88e5e1cb75f455ffc7cc13f51e5c834e" resolved "https://registry.yarnpkg.com/winchan/-/winchan-0.2.2.tgz#6766917b88e5e1cb75f455ffc7cc13f51e5c834e"