From 5bab2a3762bee8038dd3b7af7fd8fbbeaa1b1804 Mon Sep 17 00:00:00 2001 From: Vishnu KS Date: Thu, 21 Nov 2019 12:24:55 +0800 Subject: [PATCH] upload: Replace jQuery filedrop with Uppy. --- docs/THIRDPARTY | 4 - frontend_tests/node_tests/compose.js | 91 +-- frontend_tests/node_tests/compose_actions.js | 7 +- frontend_tests/node_tests/ui_init.js | 2 +- frontend_tests/node_tests/upload.js | 631 ++++++++++++---- package.json | 3 + static/js/bundles/app.js | 1 - static/js/bundles/common.js | 2 + static/js/compose.js | 15 +- static/js/copy_and_paste.js | 3 +- static/js/message_edit.js | 10 +- static/js/upload.js | 374 +++++----- .../third/jquery-filedrop/jquery.filedrop.js | 680 ------------------ tools/test-js-with-node | 1 - version.py | 2 +- yarn.lock | 76 ++ 16 files changed, 813 insertions(+), 1089 deletions(-) delete mode 100644 static/third/jquery-filedrop/jquery.filedrop.js diff --git a/docs/THIRDPARTY b/docs/THIRDPARTY index 4d74968269..248dbc3908 100644 --- a/docs/THIRDPARTY +++ b/docs/THIRDPARTY @@ -107,10 +107,6 @@ Files: static/generated/emoji/images/emoji/unicode/* Copyright: Google, Inc. License: Apache-2.0 -Files: static/third/jquery-filedrop/jquery.filedrop.js -Copyright: Resopollution -License: Expat - Files: static/third/jquery-idle/jquery.idle.js Copyright: 2011-2013 Henrique Boaventura License: Expat diff --git a/frontend_tests/node_tests/compose.js b/frontend_tests/node_tests/compose.js index cf6e9a234c..daddd2a02a 100644 --- a/frontend_tests/node_tests/compose.js +++ b/frontend_tests/node_tests/compose.js @@ -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', () => { stream_data.add_sub({ name: compose_state.stream_name(), @@ -954,13 +895,18 @@ run_test('initialize', () => { global.document = 'document-stub'; global.csrf_token = 'fake-csrf-token'; - let filedrop_in_compose_checked = false; 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(); @@ -968,14 +914,11 @@ run_test('initialize', () => { assert(resize_watch_manual_resize_checked); assert(xmlhttprequest_checked); assert(!$("#compose #attach_files").hasClass("notdisplayed")); - assert(filedrop_in_compose_checked); + assert(setup_upload_called); function reset_jquery() { // Avoid leaks. set_global('$', global.make_zjquery()); - - // Bypass filedrop (we already tested it above). - $("#compose").filedrop = noop; } let compose_actions_start_checked; @@ -1013,6 +956,18 @@ run_test('initialize', () => { 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', () => { diff --git a/frontend_tests/node_tests/compose_actions.js b/frontend_tests/node_tests/compose_actions.js index 13dac8319e..ae23fb63d3 100644 --- a/frontend_tests/node_tests/compose_actions.js +++ b/frontend_tests/node_tests/compose_actions.js @@ -212,13 +212,18 @@ run_test('start', () => { // Cancel compose. let pill_cleared; - compose_pm_pill.clear = function () { pill_cleared = true; }; + let abort_xhr_called = false; + compose.abort_xhr = () => { + abort_xhr_called = true; + }; + assert_hidden('#compose_controls'); cancel(); + assert(abort_xhr_called); assert(pill_cleared); assert_visible('#compose_controls'); assert_hidden('#private-message'); diff --git a/frontend_tests/node_tests/ui_init.js b/frontend_tests/node_tests/ui_init.js index bbb1e2c14e..edb3f179ed 100644 --- a/frontend_tests/node_tests/ui_init.js +++ b/frontend_tests/node_tests/ui_init.js @@ -162,7 +162,7 @@ page_params.starred_messages = []; page_params.presences = []; $('#tab_bar').append = () => {}; -$('#compose').filedrop = () => {}; +upload.setup_upload = () => {}; server_events.home_view_loaded = () => true; diff --git a/frontend_tests/node_tests/upload.js b/frontend_tests/node_tests/upload.js index 75944f1daa..926459d9a5 100644 --- a/frontend_tests/node_tests/upload.js +++ b/frontend_tests/node_tests/upload.js @@ -1,3 +1,5 @@ +const rewiremock = require("rewiremock/node"); + set_global('$', global.make_zjquery()); set_global('document', { location: { }, @@ -9,7 +11,7 @@ set_global('i18n', global.stub_i18n); set_global('page_params', { max_file_upload_size: 25, }); -set_global('csrf_token', { }); +set_global('csrf_token', "csrf_token"); set_global('bridge', false); // 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_state'); zrequire('compose'); +zrequire('compose_actions'); + +const plugin_stub = { + prototype: { + constructor: null, + }, +}; + 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', () => { - $("#compose-send-button").prop('disabled', false); + uri = "https://foo.com/user_uploads/5/d4/6lSlfIPIg9nDI2Upj0Mq_EbE/alappuzha.png"; + 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-close").one = function (ev_name, handler) { - assert.equal(ev_name, 'click'); - assert(handler); - }; - $("#compose-error-msg").html(''); - const test_html = '
' + - '
' + - '
'; - $("#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); - + upload.upload_files(uppy, config, files); assert.equal($("#compose-send-button").attr("disabled"), ''); assert($("#compose-send-status").hasClass("alert-info")); assert($("#compose-send-status").visible()); assert.equal($("

").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', () => { - let width_update_checked = false; - $("#compose-upload-bar-1549958107000").width = function (width_percent) { - assert.equal(width_percent, '39%'); - width_update_checked = true; +run_test('uppy_config', () => { + let uppy_stub_called = false; + let uppy_set_meta_called = false; + let uppy_used_xhrupload = false; + 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); - assert(width_update_checked); + let upload_files_called = false; + 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', () => { - function setup_test() { - $("#compose-send-status").removeClass("alert-error"); - $("#compose-send-status").addClass("alert-info"); - $("#compose-send-button").attr("disabled", 'disabled'); - $("#compose-error-msg").text(''); +run_test('file_drop', () => { + set_global('$', global.make_zjquery()); - $("#compose-upload-bar-1549958107000").parent = function () { - return { remove: function () {} }; + upload.setup_upload({mode: "compose"}); + + 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) { - assert($("#compose-send-status").hasClass("alert-error")); - assert(!$("#compose-send-status").hasClass("alert-info")); - assert.equal($("#compose-send-button").prop("disabled"), false); - assert.equal($("#compose-error-msg").text(), msg); - } + const on_upload_success_callback = callbacks["upload-success"]; + const file = { + name: "copenhagen.png", + }; + 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 = {}) { - setup_test(); - file.trackingId = "1549958107000"; - upload_opts.error(err, server_response, file); - assert_side_effects(msg); - } + response = { + body: { + uri: undefined, + }, + }; + 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 msg_1 = 'File upload is not yet available for your browser.'; - const msg_2 = 'Unable to upload that many files at once.'; - const msg_3 = '"foobar.txt" was too large; the maximum file size is 25MB.'; - const msg_4 = 'Sorry, the file was too large.'; - const msg_5 = 'An unknown error occurred.'; - const msg_6 = 'File and image uploads have been disabled for this organization.'; + const on_complete_callback = callbacks.complete; + global.patch_builtin('setTimeout', (func) => { + func(); + }); + let hide_upload_status_called = false; + upload.hide_upload_status = () => { + 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); - test('TooManyFiles', msg_prefix + msg_2); - test('FileTooLarge', msg_prefix + msg_3, null, {name: 'foobar.txt'}); - test(413, msg_prefix + msg_4); - test(400, 'ちょっと…', {msg: 'ちょっと…'}); - test('Do-not-match-any-case', msg_prefix + msg_5); + state = { + type: "error", + details: "Some Error", + message: "Some error message", + }; + 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 - // displayed when a user tries to paste or drag a file onto the UI. - page_params.max_file_upload_size = 0; - test('FileTooLarge', msg_prefix + msg_6, null); -}); - -run_test('upload_finish', () => { - function test(i, response, textbox_val) { - let compose_ui_autosize_textarea_checked = false; - let compose_actions_start_checked = false; - let syntax_to_replace; - let syntax_to_replace_with; - let file_input_clear = false; - - function setup() { - $("#compose-textarea").val(''); - compose_ui.autosize_textarea = function () { - compose_ui_autosize_textarea_checked = true; - }; - compose_ui.replace_syntax = function (old_syntax, new_syntax) { - syntax_to_replace = old_syntax; - syntax_to_replace_with = new_syntax; - }; - compose_state.set_message_type(); - global.compose_actions = { - start: function (msg_type) { - assert.equal(msg_type, 'stream'); - compose_actions_start_checked = true; - }, - }; - $("#compose-send-button").attr('disabled', 'disabled'); - $("#compose-send-status").addClass("alert-info"); - $("#compose-send-status").show(); - - $('#file_input').clone = function (param) { - assert(param); - return $('#file_input'); - }; - - $('#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); + state = { + type: "error", + message: "No Internet connection", + }; + on_info_visible_callback(); + assert.equal(uppy_cancel_all_counter, 2); + + state = { + type: "error", + details: "Upload Error", + }; + on_info_visible_callback(); + assert.equal(uppy_cancel_all_counter, 2); + + const on_upload_error_callback = callbacks["upload-error"]; + show_error_message_called = false; + upload.show_error_message = (config, message) => { + show_error_message_called = true; + assert.equal(config.mode, "compose"); + assert.equal(message, "Response message"); + }; + response = { + body: { + msg: "Response message", + }, + }; + on_upload_error_callback(null, null, response); + assert.equal(uppy_cancel_all_counter, 3); + assert(show_error_message_called); + + upload.show_error_message = (config, message) => { + show_error_message_called = true; + assert.equal(config.mode, "compose"); + assert.equal(message, null); + }; + on_upload_error_callback(null, null); + assert.equal(uppy_cancel_all_counter, 4); + assert(show_error_message_called); }); diff --git a/package.json b/package.json index 8afa51e41d..76d7f78821 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,9 @@ "@babel/preset-env": "^7.5.5", "@babel/preset-typescript": "^7.3.3", "@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", "autosize": "^4.0.2", "babel-loader": "^8.0.6", diff --git a/static/js/bundles/app.js b/static/js/bundles/app.js index d5e4fcbb40..761d14a727 100644 --- a/static/js/bundles/app.js +++ b/static/js/bundles/app.js @@ -3,7 +3,6 @@ import "./common.js"; // Import Third party libraries import "../../third/bootstrap-notify/js/bootstrap-notify.js"; import "../../third/bootstrap-typeahead/typeahead.js"; -import "../../third/jquery-filedrop/jquery.filedrop.js"; import "jquery-caret-plugin/src/jquery.caret.js"; import "../../third/jquery-idle/jquery.idle.js"; import "spectrum-colorpicker"; diff --git a/static/js/bundles/common.js b/static/js/bundles/common.js index fca9df10f2..1d68200dde 100644 --- a/static/js/bundles/common.js +++ b/static/js/bundles/common.js @@ -18,3 +18,5 @@ import "font-awesome/css/font-awesome.css"; import "../../assets/icons/zulip-icons.font.js"; import "source-sans-pro/source-sans-pro.css"; import "../../styles/pygments.scss"; +import "@uppy/core/dist/style.css"; +import "@uppy/progress-bar/dist/style.css"; diff --git a/static/js/compose.js b/static/js/compose.js index 30a374fabd..774b084d1c 100644 --- a/static/js/compose.js +++ b/static/js/compose.js @@ -17,6 +17,7 @@ const render_compose_private_stream_alert = require("../templates/compose_privat let user_acknowledged_all_everyone; let user_acknowledged_announce; let wildcard_mention; +let uppy; exports.all_everyone_warn_threshold = 15; exports.announce_warn_threshold = 60; @@ -146,11 +147,7 @@ function update_fade() { exports.abort_xhr = function () { $("#compose-send-button").prop("disabled", false); - const xhr = $("#compose").data("filedrop_xhr"); - if (xhr !== undefined) { - xhr.abort(); - $("#compose").removeData("filedrop_xhr"); - } + uppy.cancelAll(); }; exports.empty_topic_placeholder = function () { @@ -1090,11 +1087,9 @@ exports.initialize = function () { exports.clear_preview_area(); }); - $("#compose").filedrop( - upload.options({ - mode: 'compose', - }) - ); + uppy = upload.setup_upload({ + mode: "compose", + }); $("#compose-textarea").focus(function () { const opts = { diff --git a/static/js/copy_and_paste.js b/static/js/copy_and_paste.js index c2562e0584..e1e7c7494f 100644 --- a/static/js/copy_and_paste.js +++ b/static/js/copy_and_paste.js @@ -316,8 +316,7 @@ exports.paste_handler = function (event) { const mdImageRegex = /^!\[.*\]\(.*\)$/; if (text.match(mdImageRegex)) { // This block catches cases where we are pasting an - // image into Zulip, which should be handled by the - // jQuery filedrop library, not this code path. + // image into Zulip, which is handled by upload.js. return; } event.preventDefault(); diff --git a/static/js/message_edit.js b/static/js/message_edit.js index 74ad618aaf..47e7b51721 100644 --- a/static/js/message_edit.js +++ b/static/js/message_edit.js @@ -375,12 +375,10 @@ function start_edit_with_content(row, content, edit_box_open_callback) { edit_box_open_callback(); } - row.find('#message_edit_form').filedrop( - upload.options({ - mode: 'edit', - row: rows.id(row), - }) - ); + upload.setup_upload({ + mode: 'edit', + row: rows.id(row), + }); } exports.start = function (row, edit_box_open_callback) { diff --git a/static/js/upload.js b/static/js/upload.js index 2d30894ba2..12019fb9e4 100644 --- a/static/js/upload.js +++ b/static/js/upload.js @@ -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)) { // Rewrite the URI to a usable link return compose.uploads_domain + uri; } return uri; -} +}; // Show the upload button only if the browser supports it. exports.feature_check = function (upload_button) { @@ -13,187 +17,235 @@ exports.feature_check = function (upload_button) { } }; -exports.options = function (config) { - let textarea; - let send_button; - let send_status; - let send_status_close; - let error_msg; - let upload_bar; - let file_input; - - switch (config.mode) { - case 'compose': - textarea = $('#compose-textarea'); - send_button = $('#compose-send-button'); - send_status = $('#compose-send-status'); - send_status_close = $('.compose-send-status-close'); - error_msg = $('#compose-error-msg'); - upload_bar = 'compose-upload-bar'; - file_input = 'file_input'; - break; - case 'edit': - textarea = $('#message_edit_content_' + config.row); - send_button = textarea.closest('#message_edit_form').find('.message_edit_save'); - send_status = $('#message-edit-send-status-' + config.row); - send_status_close = send_status.find('.send-status-close'); - error_msg = send_status.find('.error-msg'); - upload_bar = 'message-edit-upload-bar-' + config.row; - file_input = 'message_edit_file_input_' + config.row; - break; - default: +exports.get_item = function (key, config) { + if (!config) { + throw Error("Missing config"); + } + if (config.mode === "compose") { + switch (key) { + case "textarea": + return $('#compose-textarea'); + case "send_button": + return $('#compose-send-button'); + case "send_status_identifier": + return '#compose-send-status'; + case "send_status": + return $('#compose-send-status'); + case "send_status_close_button": + return $('.compose-send-status-close'); + case "send_status_message": + return $('#compose-error-msg'); + case "file_input_identifier": + return "#file_input"; + case "source": + return "compose-file-input"; + case "drag_drop_container": + return $("#compose"); + default: + throw Error(`Invalid key name for mode "${config.mode}"`); + } + } else if (config.mode === "edit") { + 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!"); } +}; - const hide_upload_status = function () { - send_button.prop("disabled", false); - send_status.removeClass("alert-info").hide(); - $('div.progress.active').remove(); - }; +exports.hide_upload_status = function (config) { + exports.get_item("send_button", config).prop("disabled", false); + exports.get_item("send_status", config).removeClass("alert-info").hide(); +}; - const drop = function () { - send_button.attr("disabled", ""); - send_status.addClass("alert-info").show(); - send_status_close.one('click', function () { - setTimeout(function () { - hide_upload_status(); - }, 500); - compose.abort_xhr(); - }); - }; +exports.show_error_message = function (config, message) { + if (!message) { + message = i18n.t("An unknown error occurred."); + } + exports.get_item("send_button", config).prop("disabled", false); + exports.get_item("send_status", config).addClass("alert-error").removeClass("alert-info").show(); + exports.get_item("send_status_message", config).text(message); +}; - const uploadStarted = function (i, file) { - error_msg.html($("

").text(i18n.t("Uploading…"))); - // file.lastModified is unique for each upload, and was previously used to track each - // upload. But, when an image is pasted into Safari, it looks like the lastModified time - // gets changed by the time the image upload is finished, and we lose track of the - // 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 - file.trackingId = Math.random().toString().substring(2); // Use digits after the `.` +exports.upload_files = function (uppy, config, files) { + if (files.length === 0) { + uppy.cancelAll(); + exports.hide_upload_status(config); + return; + } + if (page_params.max_file_upload_size === 0) { + 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($("

").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('

' + - '
' + - '
'); - compose_ui.insert_syntax_and_focus("[Uploading " + file.name + "…]()", textarea); - }; + } +}; - const progressUpdated = function (i, file, progress) { - $("#" + upload_bar + '-' + file.trackingId).width(progress + "%"); - }; - - const uploadError = function (error_code, server_response, file) { - let msg; - send_status.addClass("alert-error").removeClass("alert-info"); - send_button.prop("disabled", false); - if (file !== undefined) { - $("#" + upload_bar + '-' + file.trackingId).parent().remove(); +exports.setup_upload = function (config) { + const uppy = Uppy({ + debug: false, + autoProceed: true, + restrictions: { + maxFileSize: page_params.max_file_upload_size * 1024 * 1024, + }, + locale: { + strings: { + 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) { - case 'BrowserNotSupported': - msg = i18n.t("File upload is not yet available for your browser."); - 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); - }; + uppy.use(ProgressBar, { + target: exports.get_item("send_status_identifier", config), + hideAfterFinish: false, + }); - const uploadFinished = function (i, file, response) { - if (response.uri === undefined) { + $("body").on("change", exports.get_item("file_input_identifier", config), (event) => { + 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; } - 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]; - // Urgh, yet another hack to make sure we're "composing" - // when text gets added into the composebox. - if (config.mode === 'compose' && !compose_state.composing()) { + if (!compose_state.composing()) { 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(); + }); + uppy.on('complete', () => { setTimeout(function () { - $("#" + upload_bar + '-' + file.trackingId).parent().remove(); - if ($('div.progress.active').length === 0) { - hide_upload_status(file); - } + uppy.cancelAll(); + exports.hide_upload_status(config); }, 500); + }); - // In order to upload the same file twice in a row, we need to clear out - // the file input element, so that the next time we use the file dialog, - // an actual change event is fired. IE doesn't allow .val('') so we - // need to clone it. (Taken from the jQuery form plugin) - if (/MSIE/.test(navigator.userAgent)) { - $('#' + file_input).replaceWith($('#' + file_input).clone(true)); - } else { - $('#' + file_input).val(''); + uppy.on('info-visible', () => { + const info = uppy.getState().info; + if (info.type === "error" && info.message === "No Internet connection") { + // server_events already handles the case of no internet. + return; } - }; - return { - url: "/json/user_uploads", - fallback_id: file_input, // Target for standard file dialog - paramname: "file", - max_file_upload_size: page_params.max_file_upload_size, - data: { - // the token isn't automatically included in filedrop's post - csrfmiddlewaretoken: csrf_token, - }, - raw_droppable: ['text/uri-list', 'text/plain'], - drop: drop, - uploadStarted: uploadStarted, - progressUpdated: progressUpdated, - error: uploadError, - uploadFinished: uploadFinished, - rawDrop: function (contents) { - if (!compose_state.composing()) { - compose_actions.start('stream'); - } - textarea.val(textarea.val() + contents); - compose_ui.autosize_textarea(); - }, - }; + if (info.type === "error" && info.details === "Upload Error") { + // The server errors come under 'Upload Error'. But we can't handle them + // here because info object don't contain response.body.msg received from + // the server. Server errors are hence handled by on('upload-error'). + return; + } + + if (info.type === "error") { + // The remaining errors are mostly frontend errors like file being too large + // for upload. + uppy.cancelAll(); + exports.show_error_message(config, info.message); + } + }); + + uppy.on('upload-error', (file, error, response) => { + const message = response ? response.body.msg : null; + uppy.cancelAll(); + exports.show_error_message(config, message); + }); + + return uppy; }; window.upload = exports; diff --git a/static/third/jquery-filedrop/jquery.filedrop.js b/static/third/jquery-filedrop/jquery.filedrop.js deleted file mode 100644 index 0574d91265..0000000000 --- a/static/third/jquery-filedrop/jquery.filedrop.js +++ /dev/null @@ -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); diff --git a/tools/test-js-with-node b/tools/test-js-with-node index aaaaf29883..5d87002e4c 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -154,7 +154,6 @@ EXEMPT_FILES = { 'static/js/ui_util.js', 'static/js/unread_ops.js', 'static/js/unread_ui.js', - 'static/js/upload.js', 'static/js/upload_widget.js', 'static/js/user_status_ui.js', 'static/js/zcommand.js', diff --git a/version.py b/version.py index a6c68914e1..663ff64f0d 100644 --- a/version.py +++ b/version.py @@ -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 # minor version bump suffices. -PROVISION_VERSION = '73.0' +PROVISION_VERSION = '73.1' diff --git a/yarn.lock b/yarn.lock index c74062b559..9fec7379cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1247,6 +1247,55 @@ semver "^6.3.0" 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": version "0.6.0" 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" 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: version "1.1.3" 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" 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: version "2.1.26" 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" 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: version "2.14.0" 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" 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: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -12617,6 +12688,11 @@ wide-align@^1.1.0: dependencies: 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: version "0.2.2" resolved "https://registry.yarnpkg.com/winchan/-/winchan-0.2.2.tgz#6766917b88e5e1cb75f455ffc7cc13f51e5c834e"