mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			318 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			318 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import Uppy from "@uppy/core";
 | 
						|
import ProgressBar from "@uppy/progress-bar";
 | 
						|
import XHRUpload from "@uppy/xhr-upload";
 | 
						|
import $ from "jquery";
 | 
						|
 | 
						|
import * as compose_actions from "./compose_actions";
 | 
						|
import * as compose_state from "./compose_state";
 | 
						|
import * as compose_ui from "./compose_ui";
 | 
						|
import {csrf_token} from "./csrf";
 | 
						|
import {$t} from "./i18n";
 | 
						|
import {page_params} from "./page_params";
 | 
						|
 | 
						|
// Show the upload button only if the browser supports it.
 | 
						|
export function feature_check($upload_button) {
 | 
						|
    if (window.XMLHttpRequest && new window.XMLHttpRequest().upload) {
 | 
						|
        $upload_button.removeClass("notdisplayed");
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export function get_translated_status(file) {
 | 
						|
    const status = $t({defaultMessage: "Uploading {filename}…"}, {filename: file.name});
 | 
						|
    return "[" + status + "]()";
 | 
						|
}
 | 
						|
 | 
						|
export function get_item(key, config) {
 | 
						|
    if (!config) {
 | 
						|
        throw new 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 "#compose .file_input";
 | 
						|
            case "source":
 | 
						|
                return "compose-file-input";
 | 
						|
            case "drag_drop_container":
 | 
						|
                return $("#compose");
 | 
						|
            case "markdown_preview_hide_button":
 | 
						|
                return $("#compose .undo_markdown_preview");
 | 
						|
            default:
 | 
						|
                throw new Error(`Invalid key name for mode "${config.mode}"`);
 | 
						|
        }
 | 
						|
    } else if (config.mode === "edit") {
 | 
						|
        if (!config.row) {
 | 
						|
            throw new Error("Missing row in config");
 | 
						|
        }
 | 
						|
        switch (key) {
 | 
						|
            case "textarea":
 | 
						|
                return $(`#edit_form_${CSS.escape(config.row)} .message_edit_content`);
 | 
						|
            case "send_button":
 | 
						|
                return $(`#edit_form_${CSS.escape(config.row)} .message_edit_content`)
 | 
						|
                    .closest(".message_edit_form")
 | 
						|
                    .find(".message_edit_save");
 | 
						|
            case "send_status_identifier":
 | 
						|
                return `#message-edit-send-status-${CSS.escape(config.row)}`;
 | 
						|
            case "send_status":
 | 
						|
                return $(`#message-edit-send-status-${CSS.escape(config.row)}`);
 | 
						|
            case "send_status_close_button":
 | 
						|
                return $(`#message-edit-send-status-${CSS.escape(config.row)}`).find(
 | 
						|
                    ".send-status-close",
 | 
						|
                );
 | 
						|
            case "send_status_message":
 | 
						|
                return $(`#message-edit-send-status-${CSS.escape(config.row)}`).find(".error-msg");
 | 
						|
            case "file_input_identifier":
 | 
						|
                return `#edit_form_${CSS.escape(config.row)} .file_input`;
 | 
						|
            case "source":
 | 
						|
                return "message-edit-file-input";
 | 
						|
            case "drag_drop_container":
 | 
						|
                return $(`#zfilt${CSS.escape(config.row)} .message_edit_form`);
 | 
						|
            case "markdown_preview_hide_button":
 | 
						|
                return $(`#edit_form_${CSS.escape(config.row)} .undo_markdown_preview`);
 | 
						|
            default:
 | 
						|
                throw new Error(`Invalid key name for mode "${config.mode}"`);
 | 
						|
        }
 | 
						|
    } else {
 | 
						|
        throw new Error("Invalid upload mode!");
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export function hide_upload_status(config) {
 | 
						|
    get_item("send_button", config).prop("disabled", false);
 | 
						|
    get_item("send_status", config).removeClass("alert-info").hide();
 | 
						|
}
 | 
						|
 | 
						|
export function show_error_message(
 | 
						|
    config,
 | 
						|
    message = $t({defaultMessage: "An unknown error occurred."}),
 | 
						|
) {
 | 
						|
    get_item("send_button", config).prop("disabled", false);
 | 
						|
    get_item("send_status", config).addClass("alert-error").removeClass("alert-info").show();
 | 
						|
    get_item("send_status_message", config).text(message);
 | 
						|
}
 | 
						|
 | 
						|
export function upload_files(uppy, config, files) {
 | 
						|
    if (files.length === 0) {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
    if (page_params.max_file_upload_size_mib === 0) {
 | 
						|
        show_error_message(
 | 
						|
            config,
 | 
						|
            $t({
 | 
						|
                defaultMessage: "File and image uploads have been disabled for this organization.",
 | 
						|
            }),
 | 
						|
        );
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    // If we're looking at a markdown preview, switch back to the edit
 | 
						|
    // UI.  This is important for all the later logic around focus
 | 
						|
    // (etc.) to work correctly.
 | 
						|
    //
 | 
						|
    // We implement this transition through triggering a click on the
 | 
						|
    // toggle button to take advantage of the existing plumbing for
 | 
						|
    // handling the compose and edit UIs.
 | 
						|
    if (get_item("markdown_preview_hide_button", config).is(":visible")) {
 | 
						|
        get_item("markdown_preview_hide_button", config).trigger("click");
 | 
						|
    }
 | 
						|
 | 
						|
    get_item("send_button", config).prop("disabled", true);
 | 
						|
    get_item("send_status", config).addClass("alert-info").removeClass("alert-error").show();
 | 
						|
    get_item("send_status_message", config).html($("<p>").text($t({defaultMessage: "Uploading…"})));
 | 
						|
    get_item("send_status_close_button", config).one("click", () => {
 | 
						|
        for (const file of uppy.getFiles()) {
 | 
						|
            compose_ui.replace_syntax(
 | 
						|
                get_translated_status(file),
 | 
						|
                "",
 | 
						|
                get_item("textarea", config),
 | 
						|
            );
 | 
						|
        }
 | 
						|
        compose_ui.autosize_textarea(get_item("textarea", config));
 | 
						|
        uppy.cancelAll();
 | 
						|
        get_item("textarea", config).trigger("focus");
 | 
						|
        setTimeout(() => {
 | 
						|
            hide_upload_status(config);
 | 
						|
        }, 500);
 | 
						|
    });
 | 
						|
 | 
						|
    for (const file of files) {
 | 
						|
        try {
 | 
						|
            compose_ui.insert_syntax_and_focus(
 | 
						|
                get_translated_status(file),
 | 
						|
                get_item("textarea", config),
 | 
						|
            );
 | 
						|
            compose_ui.autosize_textarea(get_item("textarea", config));
 | 
						|
            uppy.addFile({
 | 
						|
                source: get_item("source", config),
 | 
						|
                name: file.name,
 | 
						|
                type: file.type,
 | 
						|
                data: file,
 | 
						|
            });
 | 
						|
        } catch {
 | 
						|
            // Errors are handled by info-visible and upload-error event callbacks.
 | 
						|
            break;
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export function setup_upload(config) {
 | 
						|
    const uppy = new Uppy({
 | 
						|
        debug: false,
 | 
						|
        autoProceed: true,
 | 
						|
        restrictions: {
 | 
						|
            maxFileSize: page_params.max_file_upload_size_mib * 1024 * 1024,
 | 
						|
        },
 | 
						|
        locale: {
 | 
						|
            strings: {
 | 
						|
                exceedsSize: $t({defaultMessage: "This file exceeds maximum allowed size of"}),
 | 
						|
                failedToUpload: $t({defaultMessage: "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: $t({
 | 
						|
                    defaultMessage: "Upload stalled for %'{seconds}' seconds, aborting.",
 | 
						|
                }),
 | 
						|
            },
 | 
						|
        },
 | 
						|
    });
 | 
						|
 | 
						|
    uppy.use(ProgressBar, {
 | 
						|
        target: get_item("send_status_identifier", config),
 | 
						|
        hideAfterFinish: false,
 | 
						|
    });
 | 
						|
 | 
						|
    $("body").on("change", get_item("file_input_identifier", config), (event) => {
 | 
						|
        const files = event.target.files;
 | 
						|
        upload_files(uppy, config, files);
 | 
						|
        event.target.value = "";
 | 
						|
    });
 | 
						|
 | 
						|
    const $drag_drop_container = 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;
 | 
						|
        upload_files(uppy, config, files);
 | 
						|
    });
 | 
						|
 | 
						|
    $drag_drop_container.on("paste", (event) => {
 | 
						|
        const clipboard_data = event.clipboardData || event.originalEvent.clipboardData;
 | 
						|
        if (!clipboard_data) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        const items = clipboard_data.items;
 | 
						|
        const files = [];
 | 
						|
        for (const item of items) {
 | 
						|
            if (item.kind !== "file") {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
            const file = item.getAsFile();
 | 
						|
            files.push(file);
 | 
						|
        }
 | 
						|
        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.at(-1);
 | 
						|
        if (config.mode === "compose" && !compose_state.composing()) {
 | 
						|
            compose_actions.start("stream");
 | 
						|
        }
 | 
						|
        const filename_uri = "[" + filename + "](" + uri + ")";
 | 
						|
        compose_ui.replace_syntax(
 | 
						|
            get_translated_status(file),
 | 
						|
            filename_uri,
 | 
						|
            get_item("textarea", config),
 | 
						|
        );
 | 
						|
        compose_ui.autosize_textarea(get_item("textarea", config));
 | 
						|
    });
 | 
						|
 | 
						|
    uppy.on("complete", () => {
 | 
						|
        let uploads_in_progress = false;
 | 
						|
        for (const file of uppy.getFiles()) {
 | 
						|
            if (file.progress.uploadComplete) {
 | 
						|
                // The uploaded files should be removed since uppy don't allow files in the store
 | 
						|
                // to be re-uploaded again.
 | 
						|
                uppy.removeFile(file.id);
 | 
						|
            } else {
 | 
						|
                // Happens when user tries to upload files when there is already an existing batch
 | 
						|
                // being uploaded. So when the first batch of files complete, the second batch would
 | 
						|
                // still be in progress.
 | 
						|
                uploads_in_progress = true;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        const has_errors = get_item("send_status", config).hasClass("alert-error");
 | 
						|
        if (!uploads_in_progress && !has_errors) {
 | 
						|
            setTimeout(() => {
 | 
						|
                hide_upload_status(config);
 | 
						|
            }, 500);
 | 
						|
        }
 | 
						|
    });
 | 
						|
 | 
						|
    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;
 | 
						|
        }
 | 
						|
 | 
						|
        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();
 | 
						|
            show_error_message(config, info.message);
 | 
						|
        }
 | 
						|
    });
 | 
						|
 | 
						|
    uppy.on("upload-error", (file, error, response) => {
 | 
						|
        const message = response ? response.body.msg : undefined;
 | 
						|
        uppy.cancelAll();
 | 
						|
        show_error_message(config, message);
 | 
						|
        compose_ui.replace_syntax(get_translated_status(file), "", get_item("textarea", config));
 | 
						|
        compose_ui.autosize_textarea(get_item("textarea", config));
 | 
						|
    });
 | 
						|
 | 
						|
    uppy.on("restriction-failed", (file) => {
 | 
						|
        compose_ui.replace_syntax(get_translated_status(file), "", get_item("textarea", config));
 | 
						|
        compose_ui.autosize_textarea(get_item("textarea", config));
 | 
						|
    });
 | 
						|
 | 
						|
    return uppy;
 | 
						|
}
 |