2015-07-08 16:34:26 +03:00
|
|
|
/*
|
2016-01-07 07:06:39 +03:00
|
|
|
Copyright 2015, 2016 OpenMarket Ltd
|
2019-04-01 18:42:41 +03:00
|
|
|
Copyright 2019 New Vector Ltd
|
2020-05-24 17:47:52 +03:00
|
|
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
2015-07-08 16:34:26 +03:00
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2021-06-29 15:11:58 +03:00
|
|
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
2022-03-11 12:04:22 +03:00
|
|
|
import { MsgType } from "matrix-js-sdk/src/@types/event";
|
2022-04-05 20:29:27 +03:00
|
|
|
import encrypt from "matrix-encrypt-attachment";
|
2021-12-09 12:10:23 +03:00
|
|
|
import extractPngChunks from "png-chunks-extract";
|
2022-10-12 20:59:07 +03:00
|
|
|
import { IImageInfo } from "matrix-js-sdk/src/@types/partials";
|
2021-12-09 12:10:23 +03:00
|
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
2023-05-09 12:52:07 +03:00
|
|
|
import {
|
|
|
|
HTTPError,
|
|
|
|
IEventRelation,
|
|
|
|
ISendEventResponse,
|
|
|
|
MatrixEvent,
|
|
|
|
UploadOpts,
|
|
|
|
UploadProgress,
|
|
|
|
} from "matrix-js-sdk/src/matrix";
|
2022-03-11 12:04:22 +03:00
|
|
|
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
2022-10-12 20:59:07 +03:00
|
|
|
import { removeElement } from "matrix-js-sdk/src/utils";
|
2021-05-21 16:52:27 +03:00
|
|
|
|
2022-10-12 20:59:07 +03:00
|
|
|
import { IEncryptedFile, IMediaEventContent, IMediaEventInfo } from "./customisations/models/IMediaEventContent";
|
2020-05-14 05:41:41 +03:00
|
|
|
import dis from "./dispatcher/dispatcher";
|
2017-05-25 13:39:08 +03:00
|
|
|
import { _t } from "./languageHandler";
|
2019-04-01 18:42:41 +03:00
|
|
|
import Modal from "./Modal";
|
2020-06-01 19:05:53 +03:00
|
|
|
import Spinner from "./components/views/elements/Spinner";
|
2020-06-03 04:07:46 +03:00
|
|
|
import { Action } from "./dispatcher/actions";
|
2021-03-05 23:20:50 +03:00
|
|
|
import {
|
|
|
|
UploadCanceledPayload,
|
|
|
|
UploadErrorPayload,
|
|
|
|
UploadFinishedPayload,
|
|
|
|
UploadProgressPayload,
|
|
|
|
UploadStartedPayload,
|
|
|
|
} from "./dispatcher/payloads/UploadPayload";
|
2022-10-12 20:59:07 +03:00
|
|
|
import { RoomUpload } from "./models/RoomUpload";
|
2021-09-08 20:26:54 +03:00
|
|
|
import SettingsStore from "./settings/SettingsStore";
|
|
|
|
import { decorateStartSendingTime, sendRoundTripMetric } from "./sendTimePerformanceMetrics";
|
2022-01-26 12:04:19 +03:00
|
|
|
import { TimelineRenderingType } from "./contexts/RoomContext";
|
2022-03-21 15:03:59 +03:00
|
|
|
import { addReplyToMessageContent } from "./utils/Reply";
|
2022-03-03 02:33:40 +03:00
|
|
|
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
|
|
|
import UploadFailureDialog from "./components/views/dialogs/UploadFailureDialog";
|
|
|
|
import UploadConfirmDialog from "./components/views/dialogs/UploadConfirmDialog";
|
2022-03-25 00:13:11 +03:00
|
|
|
import { createThumbnail } from "./utils/image-media";
|
2023-03-23 14:47:40 +03:00
|
|
|
import { attachMentions, attachRelation } from "./components/views/rooms/SendMessageComposer";
|
2022-07-13 08:56:36 +03:00
|
|
|
import { doMaybeLocalRoomAction } from "./utils/local-room";
|
2022-10-19 15:07:03 +03:00
|
|
|
import { SdkContextClass } from "./contexts/SDKContext";
|
2016-11-15 14:22:39 +03:00
|
|
|
|
2019-01-14 20:10:20 +03:00
|
|
|
// scraped out of a macOS hidpi (5660ppm) screenshot png
|
|
|
|
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
|
|
|
|
const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
|
2016-11-15 14:22:39 +03:00
|
|
|
|
2019-04-08 21:07:17 +03:00
|
|
|
export class UploadCanceledError extends Error {}
|
2016-11-15 14:22:39 +03:00
|
|
|
|
2020-05-24 17:47:52 +03:00
|
|
|
interface IMediaConfig {
|
|
|
|
"m.upload.size"?: number;
|
|
|
|
}
|
|
|
|
|
2016-11-15 14:22:39 +03:00
|
|
|
/**
|
|
|
|
* Load a file into a newly created image element.
|
|
|
|
*
|
2019-04-01 18:42:41 +03:00
|
|
|
* @param {File} imageFile The file to load in an image element.
|
2016-11-15 14:22:39 +03:00
|
|
|
* @return {Promise} A promise that resolves with the html image element.
|
|
|
|
*/
|
2023-01-12 16:25:14 +03:00
|
|
|
async function loadImageElement(imageFile: File): Promise<{
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
img: HTMLImageElement;
|
|
|
|
}> {
|
2015-07-08 16:34:26 +03:00
|
|
|
// Load the file into an html element
|
2022-10-12 20:59:07 +03:00
|
|
|
const img = new Image();
|
2017-10-19 19:16:52 +03:00
|
|
|
const objectUrl = URL.createObjectURL(imageFile);
|
2019-04-09 13:32:44 +03:00
|
|
|
const imgPromise = new Promise((resolve, reject) => {
|
2023-01-12 16:25:14 +03:00
|
|
|
img.onload = function (): void {
|
2019-04-09 13:32:44 +03:00
|
|
|
URL.revokeObjectURL(objectUrl);
|
|
|
|
resolve(img);
|
|
|
|
};
|
2023-01-12 16:25:14 +03:00
|
|
|
img.onerror = function (e): void {
|
2019-04-09 13:32:44 +03:00
|
|
|
reject(e);
|
|
|
|
};
|
|
|
|
});
|
2017-10-19 19:16:52 +03:00
|
|
|
img.src = objectUrl;
|
2015-07-08 16:34:26 +03:00
|
|
|
|
2019-01-14 20:10:20 +03:00
|
|
|
// check for hi-dpi PNGs and fudge display resolution as needed.
|
|
|
|
// this is mainly needed for macOS screencaps
|
2023-02-16 20:21:44 +03:00
|
|
|
let parsePromise = Promise.resolve(false);
|
2019-01-14 20:10:20 +03:00
|
|
|
if (imageFile.type === "image/png") {
|
|
|
|
// in practice macOS happens to order the chunks so they fall in
|
|
|
|
// the first 0x1000 bytes (thanks to a massive ICC header).
|
|
|
|
// Thus we could slice the file down to only sniff the first 0x1000
|
2019-04-09 13:18:06 +03:00
|
|
|
// bytes (but this makes extractPngChunks choke on the corrupt file)
|
2019-01-14 20:10:20 +03:00
|
|
|
const headers = imageFile; //.slice(0, 0x1000);
|
2023-03-07 18:48:23 +03:00
|
|
|
parsePromise = readFileAsArrayBuffer(headers)
|
|
|
|
.then((arrayBuffer) => {
|
|
|
|
const buffer = new Uint8Array(arrayBuffer);
|
|
|
|
const chunks = extractPngChunks(buffer);
|
|
|
|
for (const chunk of chunks) {
|
|
|
|
if (chunk.name === "pHYs") {
|
|
|
|
if (chunk.data.byteLength !== PHYS_HIDPI.length) return false;
|
|
|
|
return chunk.data.every((val, i) => val === PHYS_HIDPI[i]);
|
|
|
|
}
|
2019-01-14 20:10:20 +03:00
|
|
|
}
|
2023-03-07 18:48:23 +03:00
|
|
|
return false;
|
|
|
|
})
|
|
|
|
.catch((e) => {
|
|
|
|
console.error("Failed to parse PNG", e);
|
|
|
|
return false;
|
|
|
|
});
|
2019-01-14 20:10:20 +03:00
|
|
|
}
|
|
|
|
|
2019-04-09 13:32:44 +03:00
|
|
|
const [hidpi] = await Promise.all([parsePromise, imgPromise]);
|
|
|
|
const width = hidpi ? img.width >> 1 : img.width;
|
|
|
|
const height = hidpi ? img.height >> 1 : img.height;
|
2021-06-29 15:11:58 +03:00
|
|
|
return { width, height, img };
|
2015-07-08 16:34:26 +03:00
|
|
|
}
|
|
|
|
|
2021-08-05 15:04:20 +03:00
|
|
|
// Minimum size for image files before we generate a thumbnail for them.
|
|
|
|
const IMAGE_SIZE_THRESHOLD_THUMBNAIL = 1 << 15; // 32KB
|
|
|
|
// Minimum size improvement for image thumbnails, if both are not met then don't bother uploading thumbnail.
|
|
|
|
const IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE = 1 << 16; // 1MB
|
|
|
|
const IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT = 0.1; // 10%
|
|
|
|
// We don't apply these thresholds to video thumbnails as a poster image is always useful
|
|
|
|
// and videos tend to be much larger.
|
|
|
|
|
2022-05-24 11:05:29 +03:00
|
|
|
// Image mime types for which to always include a thumbnail for even if it is larger than the input for wider support.
|
|
|
|
const ALWAYS_INCLUDE_THUMBNAIL = ["image/avif", "image/webp"];
|
|
|
|
|
2016-11-15 14:22:39 +03:00
|
|
|
/**
|
|
|
|
* Read the metadata for an image file and create and upload a thumbnail of the image.
|
|
|
|
*
|
|
|
|
* @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with.
|
|
|
|
* @param {String} roomId The ID of the room the image will be uploaded in.
|
2019-04-01 18:42:41 +03:00
|
|
|
* @param {File} imageFile The image to read and thumbnail.
|
2016-11-15 14:22:39 +03:00
|
|
|
* @return {Promise} A promise that resolves with the attachment info.
|
|
|
|
*/
|
2022-05-24 11:05:29 +03:00
|
|
|
async function infoForImageFile(
|
|
|
|
matrixClient: MatrixClient,
|
|
|
|
roomId: string,
|
|
|
|
imageFile: File,
|
|
|
|
): Promise<Partial<IMediaEventInfo>> {
|
2017-10-11 19:56:17 +03:00
|
|
|
let thumbnailType = "image/png";
|
2020-05-24 17:47:52 +03:00
|
|
|
if (imageFile.type === "image/jpeg") {
|
2016-11-15 14:22:39 +03:00
|
|
|
thumbnailType = "image/jpeg";
|
|
|
|
}
|
|
|
|
|
2021-08-05 15:04:20 +03:00
|
|
|
const imageElement = await loadImageElement(imageFile);
|
|
|
|
|
|
|
|
const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType);
|
|
|
|
const imageInfo = result.info;
|
|
|
|
|
2022-03-29 09:03:41 +03:00
|
|
|
// For lesser supported image types, always include the thumbnail even if it is larger
|
2022-05-24 11:05:29 +03:00
|
|
|
if (!ALWAYS_INCLUDE_THUMBNAIL.includes(imageFile.type)) {
|
2022-03-29 09:03:41 +03:00
|
|
|
// we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from.
|
2023-03-07 16:19:18 +03:00
|
|
|
const sizeDifference = imageFile.size - imageInfo.thumbnail_info!.size;
|
2022-03-29 09:03:41 +03:00
|
|
|
if (
|
|
|
|
// image is small enough already
|
|
|
|
imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL ||
|
|
|
|
// thumbnail is not sufficiently smaller than original
|
|
|
|
(sizeDifference <= IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE &&
|
|
|
|
sizeDifference <= imageFile.size * IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT)
|
|
|
|
) {
|
|
|
|
delete imageInfo["thumbnail_info"];
|
|
|
|
return imageInfo;
|
|
|
|
}
|
2021-08-05 15:04:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
const uploadResult = await uploadFile(matrixClient, roomId, result.thumbnail);
|
|
|
|
|
|
|
|
imageInfo["thumbnail_url"] = uploadResult.url;
|
|
|
|
imageInfo["thumbnail_file"] = uploadResult.file;
|
|
|
|
return imageInfo;
|
2016-11-15 14:22:39 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-05-21 23:04:36 +03:00
|
|
|
* Load a file into a newly created video element and pull some strings
|
|
|
|
* in an attempt to guarantee the first frame will be showing.
|
2016-11-15 14:22:39 +03:00
|
|
|
*
|
2019-04-01 18:42:41 +03:00
|
|
|
* @param {File} videoFile The file to load in an video element.
|
2016-11-15 14:22:39 +03:00
|
|
|
* @return {Promise} A promise that resolves with the video image element.
|
|
|
|
*/
|
2022-05-24 11:05:29 +03:00
|
|
|
function loadVideoElement(videoFile: File): Promise<HTMLVideoElement> {
|
2019-11-12 14:40:38 +03:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
// Load the file into an html element
|
|
|
|
const video = document.createElement("video");
|
2021-05-21 23:04:36 +03:00
|
|
|
video.preload = "metadata";
|
|
|
|
video.playsInline = true;
|
|
|
|
video.muted = true;
|
2019-11-12 14:40:38 +03:00
|
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
2023-01-12 16:25:14 +03:00
|
|
|
reader.onload = function (ev): void {
|
2019-11-12 14:40:38 +03:00
|
|
|
// Wait until we have enough data to thumbnail the first frame.
|
2023-01-12 16:25:14 +03:00
|
|
|
video.onloadeddata = async function (): Promise<void> {
|
2019-11-12 14:40:38 +03:00
|
|
|
resolve(video);
|
2021-05-21 23:04:36 +03:00
|
|
|
video.pause();
|
2019-11-12 14:40:38 +03:00
|
|
|
};
|
2023-01-12 16:25:14 +03:00
|
|
|
video.onerror = function (e): void {
|
2019-11-12 14:40:38 +03:00
|
|
|
reject(e);
|
|
|
|
};
|
2021-05-21 23:04:36 +03:00
|
|
|
|
2023-02-16 20:21:44 +03:00
|
|
|
let dataUrl = ev.target?.result as string;
|
2022-03-22 15:23:25 +03:00
|
|
|
// Chrome chokes on quicktime but likes mp4, and `file.type` is
|
|
|
|
// read only, so do this horrible hack to unbreak quicktime
|
2023-02-16 20:21:44 +03:00
|
|
|
if (dataUrl?.startsWith("data:video/quicktime;")) {
|
2022-03-22 15:23:25 +03:00
|
|
|
dataUrl = dataUrl.replace("data:video/quicktime;", "data:video/mp4;");
|
|
|
|
}
|
|
|
|
|
|
|
|
video.src = dataUrl;
|
2021-05-21 23:04:36 +03:00
|
|
|
video.load();
|
|
|
|
video.play();
|
2016-07-19 18:05:15 +03:00
|
|
|
};
|
2023-01-12 16:25:14 +03:00
|
|
|
reader.onerror = function (e): void {
|
2019-11-12 14:40:38 +03:00
|
|
|
reject(e);
|
2016-07-19 18:05:15 +03:00
|
|
|
};
|
2019-11-12 14:40:38 +03:00
|
|
|
reader.readAsDataURL(videoFile);
|
|
|
|
});
|
2016-07-19 18:05:15 +03:00
|
|
|
}
|
|
|
|
|
2016-11-15 14:22:39 +03:00
|
|
|
/**
|
|
|
|
* Read the metadata for a video file and create and upload a thumbnail of the video.
|
|
|
|
*
|
|
|
|
* @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with.
|
|
|
|
* @param {String} roomId The ID of the room the video will be uploaded to.
|
2019-04-01 18:42:41 +03:00
|
|
|
* @param {File} videoFile The video to read and thumbnail.
|
2016-11-15 14:22:39 +03:00
|
|
|
* @return {Promise} A promise that resolves with the attachment info.
|
|
|
|
*/
|
2022-05-24 11:05:29 +03:00
|
|
|
function infoForVideoFile(
|
|
|
|
matrixClient: MatrixClient,
|
|
|
|
roomId: string,
|
|
|
|
videoFile: File,
|
|
|
|
): Promise<Partial<IMediaEventInfo>> {
|
2016-11-15 14:22:39 +03:00
|
|
|
const thumbnailType = "image/jpeg";
|
|
|
|
|
2021-10-28 11:40:38 +03:00
|
|
|
let videoInfo: Partial<IMediaEventInfo>;
|
2021-06-02 07:21:04 +03:00
|
|
|
return loadVideoElement(videoFile)
|
|
|
|
.then((video) => {
|
2016-11-15 14:22:39 +03:00
|
|
|
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
|
2021-06-02 07:21:04 +03:00
|
|
|
})
|
|
|
|
.then((result) => {
|
2016-11-15 14:22:39 +03:00
|
|
|
videoInfo = result.info;
|
|
|
|
return uploadFile(matrixClient, roomId, result.thumbnail);
|
2021-06-02 07:21:04 +03:00
|
|
|
})
|
|
|
|
.then((result) => {
|
2016-11-15 14:22:39 +03:00
|
|
|
videoInfo.thumbnail_url = result.url;
|
|
|
|
videoInfo.thumbnail_file = result.file;
|
|
|
|
return videoInfo;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-11-08 14:42:20 +03:00
|
|
|
/**
|
|
|
|
* Read the file as an ArrayBuffer.
|
2019-04-01 18:42:41 +03:00
|
|
|
* @param {File} file The file to read
|
2016-11-08 14:42:20 +03:00
|
|
|
* @return {Promise} A promise that resolves with an ArrayBuffer when the file
|
|
|
|
* is read.
|
|
|
|
*/
|
2020-05-24 17:47:52 +03:00
|
|
|
function readFileAsArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
|
2019-11-12 14:40:38 +03:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const reader = new FileReader();
|
2023-01-12 16:25:14 +03:00
|
|
|
reader.onload = function (e): void {
|
2023-02-16 20:21:44 +03:00
|
|
|
resolve(e.target?.result as ArrayBuffer);
|
2019-11-12 14:40:38 +03:00
|
|
|
};
|
2023-01-12 16:25:14 +03:00
|
|
|
reader.onerror = function (e): void {
|
2019-11-12 14:40:38 +03:00
|
|
|
reject(e);
|
|
|
|
};
|
|
|
|
reader.readAsArrayBuffer(file);
|
|
|
|
});
|
2016-11-08 14:42:20 +03:00
|
|
|
}
|
|
|
|
|
2016-11-15 14:22:39 +03:00
|
|
|
/**
|
|
|
|
* Upload the file to the content repository.
|
|
|
|
* If the room is encrypted then encrypt the file before uploading.
|
|
|
|
*
|
|
|
|
* @param {MatrixClient} matrixClient The matrix client to upload the file with.
|
|
|
|
* @param {String} roomId The ID of the room being uploaded to.
|
|
|
|
* @param {File} file The file to upload.
|
2017-07-14 19:01:03 +03:00
|
|
|
* @param {Function?} progressHandler optional callback to be called when a chunk of
|
|
|
|
* data is uploaded.
|
2022-10-12 20:59:07 +03:00
|
|
|
* @param {AbortController?} controller optional abortController to use for this upload.
|
2016-11-15 14:22:39 +03:00
|
|
|
* @return {Promise} A promise that resolves with an object.
|
|
|
|
* If the file is unencrypted then the object will have a "url" key.
|
|
|
|
* If the file is encrypted then the object will have a "file" key.
|
|
|
|
*/
|
2022-10-12 20:59:07 +03:00
|
|
|
export async function uploadFile(
|
2021-06-02 07:21:04 +03:00
|
|
|
matrixClient: MatrixClient,
|
|
|
|
roomId: string,
|
|
|
|
file: File | Blob,
|
2022-10-12 20:59:07 +03:00
|
|
|
progressHandler?: UploadOpts["progressHandler"],
|
|
|
|
controller?: AbortController,
|
|
|
|
): Promise<{ url?: string; file?: IEncryptedFile }> {
|
|
|
|
const abortController = controller ?? new AbortController();
|
|
|
|
|
|
|
|
// If the room is encrypted then encrypt the file before uploading it.
|
2016-11-15 14:22:39 +03:00
|
|
|
if (matrixClient.isRoomEncrypted(roomId)) {
|
|
|
|
// First read the file into memory.
|
2022-10-12 20:59:07 +03:00
|
|
|
const data = await readFileAsArrayBuffer(file);
|
|
|
|
if (abortController.signal.aborted) throw new UploadCanceledError();
|
2021-10-28 11:40:38 +03:00
|
|
|
|
2022-10-12 20:59:07 +03:00
|
|
|
// Then encrypt the file.
|
|
|
|
const encryptResult = await encrypt.encryptAttachment(data);
|
|
|
|
if (abortController.signal.aborted) throw new UploadCanceledError();
|
|
|
|
|
|
|
|
// Pass the encrypted data as a Blob to the uploader.
|
|
|
|
const blob = new Blob([encryptResult.data]);
|
|
|
|
|
|
|
|
const { content_uri: url } = await matrixClient.uploadContent(blob, {
|
|
|
|
progressHandler,
|
|
|
|
abortController,
|
|
|
|
includeFilename: false,
|
2022-12-30 11:34:38 +03:00
|
|
|
type: "application/octet-stream",
|
2022-10-12 20:59:07 +03:00
|
|
|
});
|
|
|
|
if (abortController.signal.aborted) throw new UploadCanceledError();
|
|
|
|
|
|
|
|
// If the attachment is encrypted then bundle the URL along with the information
|
|
|
|
// needed to decrypt the attachment and add it under a file key.
|
|
|
|
return {
|
|
|
|
file: {
|
|
|
|
...encryptResult.info,
|
|
|
|
url,
|
|
|
|
} as IEncryptedFile,
|
2019-04-08 19:53:39 +03:00
|
|
|
};
|
2016-11-15 14:22:39 +03:00
|
|
|
} else {
|
2022-10-12 20:59:07 +03:00
|
|
|
const { content_uri: url } = await matrixClient.uploadContent(file, { progressHandler, abortController });
|
|
|
|
if (abortController.signal.aborted) throw new UploadCanceledError();
|
|
|
|
// If the attachment isn't encrypted then include the URL directly.
|
|
|
|
return { url };
|
2016-11-15 14:22:39 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-01 18:42:41 +03:00
|
|
|
export default class ContentMessages {
|
2022-10-12 20:59:07 +03:00
|
|
|
private inprogress: RoomUpload[] = [];
|
2023-02-16 20:21:44 +03:00
|
|
|
private mediaConfig: IMediaConfig | null = null;
|
2019-04-01 18:42:41 +03:00
|
|
|
|
2022-01-26 12:04:19 +03:00
|
|
|
public sendStickerContentToRoom(
|
2021-12-03 11:22:13 +03:00
|
|
|
url: string,
|
|
|
|
roomId: string,
|
|
|
|
threadId: string | null,
|
|
|
|
info: IImageInfo,
|
|
|
|
text: string,
|
|
|
|
matrixClient: MatrixClient,
|
2022-01-26 12:04:19 +03:00
|
|
|
): Promise<ISendEventResponse> {
|
2022-07-13 08:56:36 +03:00
|
|
|
return doMaybeLocalRoomAction(
|
|
|
|
roomId,
|
|
|
|
(actualRoomId: string) => matrixClient.sendStickerMessage(actualRoomId, threadId, url, info, text),
|
|
|
|
matrixClient,
|
|
|
|
).catch((e) => {
|
2021-10-15 17:31:29 +03:00
|
|
|
logger.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
2018-03-29 18:24:03 +03:00
|
|
|
throw e;
|
2018-03-29 18:23:20 +03:00
|
|
|
});
|
2018-01-04 12:53:26 +03:00
|
|
|
}
|
|
|
|
|
2022-01-26 12:04:19 +03:00
|
|
|
public getUploadLimit(): number | null {
|
2020-05-24 17:47:52 +03:00
|
|
|
if (this.mediaConfig !== null && this.mediaConfig["m.upload.size"] !== undefined) {
|
|
|
|
return this.mediaConfig["m.upload.size"];
|
2019-04-02 12:50:17 +03:00
|
|
|
} else {
|
|
|
|
return null;
|
2019-04-01 18:42:41 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-26 12:04:19 +03:00
|
|
|
public async sendContentListToRoom(
|
2021-11-03 11:43:24 +03:00
|
|
|
files: File[],
|
|
|
|
roomId: string,
|
2022-02-22 14:14:56 +03:00
|
|
|
relation: IEventRelation | undefined,
|
2021-11-03 11:43:24 +03:00
|
|
|
matrixClient: MatrixClient,
|
2022-01-26 12:04:19 +03:00
|
|
|
context = TimelineRenderingType.Room,
|
|
|
|
): Promise<void> {
|
2019-04-01 18:42:41 +03:00
|
|
|
if (matrixClient.isGuest()) {
|
2021-06-29 15:11:58 +03:00
|
|
|
dis.dispatch({ action: "require_registration" });
|
2019-04-01 18:42:41 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-10-19 15:07:03 +03:00
|
|
|
const replyToEvent = SdkContextClass.instance.roomViewStore.getQuotingEvent();
|
2020-06-01 17:00:55 +03:00
|
|
|
if (!this.mediaConfig) {
|
|
|
|
// hot-path optimization to not flash a spinner if we don't need to
|
2023-02-03 18:27:47 +03:00
|
|
|
const modal = Modal.createDialog(Spinner, undefined, "mx_Dialog_spinner");
|
2023-03-16 15:24:22 +03:00
|
|
|
await Promise.race([this.ensureMediaConfigFetched(matrixClient), modal.finished]);
|
|
|
|
if (!this.mediaConfig) {
|
|
|
|
// User cancelled by clicking away on the spinner
|
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
modal.close();
|
|
|
|
}
|
2020-06-01 17:00:55 +03:00
|
|
|
}
|
2019-04-01 18:42:41 +03:00
|
|
|
|
2023-02-16 20:21:44 +03:00
|
|
|
const tooBigFiles: File[] = [];
|
|
|
|
const okFiles: File[] = [];
|
2019-04-01 18:42:41 +03:00
|
|
|
|
2022-05-04 00:04:37 +03:00
|
|
|
for (const file of files) {
|
|
|
|
if (this.isFileSizeAcceptable(file)) {
|
|
|
|
okFiles.push(file);
|
2019-04-01 18:42:41 +03:00
|
|
|
} else {
|
2022-05-04 00:04:37 +03:00
|
|
|
tooBigFiles.push(file);
|
2019-04-01 18:42:41 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tooBigFiles.length > 0) {
|
2023-02-28 13:31:48 +03:00
|
|
|
const { finished } = Modal.createDialog(UploadFailureDialog, {
|
2020-05-24 17:47:52 +03:00
|
|
|
badFiles: tooBigFiles,
|
|
|
|
totalFiles: files.length,
|
|
|
|
contentMessages: this,
|
2019-04-01 18:42:41 +03:00
|
|
|
});
|
2020-07-13 02:19:15 +03:00
|
|
|
const [shouldContinue] = await finished;
|
2019-04-01 18:42:41 +03:00
|
|
|
if (!shouldContinue) return;
|
|
|
|
}
|
|
|
|
|
2019-06-16 13:43:13 +03:00
|
|
|
let uploadAll = false;
|
2019-12-27 16:59:57 +03:00
|
|
|
// Promise to complete before sending next file into room, used for synchronisation of file-sending
|
|
|
|
// to match the order the files were specified in
|
2021-06-02 07:21:04 +03:00
|
|
|
let promBefore: Promise<any> = Promise.resolve();
|
2019-04-01 18:42:41 +03:00
|
|
|
for (let i = 0; i < okFiles.length; ++i) {
|
|
|
|
const file = okFiles[i];
|
2022-07-13 08:56:36 +03:00
|
|
|
const loopPromiseBefore = promBefore;
|
|
|
|
|
2019-06-16 13:43:13 +03:00
|
|
|
if (!uploadAll) {
|
2023-02-28 13:31:48 +03:00
|
|
|
const { finished } = Modal.createDialog(UploadConfirmDialog, {
|
2022-06-14 19:51:51 +03:00
|
|
|
file,
|
|
|
|
currentIndex: i,
|
|
|
|
totalFiles: okFiles.length,
|
|
|
|
});
|
2020-07-13 02:19:15 +03:00
|
|
|
const [shouldContinue, shouldUploadAll] = await finished;
|
2019-06-16 13:43:13 +03:00
|
|
|
if (!shouldContinue) break;
|
2020-05-24 17:47:52 +03:00
|
|
|
if (shouldUploadAll) {
|
|
|
|
uploadAll = true;
|
|
|
|
}
|
2019-06-16 13:43:13 +03:00
|
|
|
}
|
2021-11-24 11:40:15 +03:00
|
|
|
|
2023-05-23 18:24:12 +03:00
|
|
|
promBefore = doMaybeLocalRoomAction(
|
|
|
|
roomId,
|
|
|
|
(actualRoomId) =>
|
|
|
|
this.sendContentToRoom(
|
|
|
|
file,
|
|
|
|
actualRoomId,
|
|
|
|
relation,
|
|
|
|
matrixClient,
|
|
|
|
replyToEvent ?? undefined,
|
|
|
|
loopPromiseBefore,
|
|
|
|
),
|
|
|
|
matrixClient,
|
2022-07-13 08:56:36 +03:00
|
|
|
);
|
2022-03-21 15:03:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (replyToEvent) {
|
|
|
|
// Clear event being replied to
|
|
|
|
dis.dispatch({
|
|
|
|
action: "reply_to_event",
|
|
|
|
event: null,
|
|
|
|
context,
|
|
|
|
});
|
2020-05-24 18:00:57 +03:00
|
|
|
}
|
2022-01-26 12:04:19 +03:00
|
|
|
|
|
|
|
// Focus the correct composer
|
|
|
|
dis.dispatch({
|
|
|
|
action: Action.FocusSendMessageComposer,
|
|
|
|
context,
|
|
|
|
});
|
2020-05-24 18:00:57 +03:00
|
|
|
}
|
|
|
|
|
2022-10-12 20:59:07 +03:00
|
|
|
public getCurrentUploads(relation?: IEventRelation): RoomUpload[] {
|
|
|
|
return this.inprogress.filter((roomUpload) => {
|
|
|
|
const noRelation = !relation && !roomUpload.relation;
|
|
|
|
const matchingRelation =
|
|
|
|
relation &&
|
|
|
|
roomUpload.relation &&
|
|
|
|
relation.rel_type === roomUpload.relation.rel_type &&
|
|
|
|
relation.event_id === roomUpload.relation.event_id;
|
2021-11-03 11:43:24 +03:00
|
|
|
|
2022-10-12 20:59:07 +03:00
|
|
|
return (noRelation || matchingRelation) && !roomUpload.cancelled;
|
2021-11-03 11:43:24 +03:00
|
|
|
});
|
2020-05-24 18:00:57 +03:00
|
|
|
}
|
|
|
|
|
2022-10-12 20:59:07 +03:00
|
|
|
public cancelUpload(upload: RoomUpload): void {
|
|
|
|
upload.abort();
|
|
|
|
dis.dispatch<UploadCanceledPayload>({ action: Action.UploadCanceled, upload });
|
2019-04-01 18:42:41 +03:00
|
|
|
}
|
|
|
|
|
2022-10-12 20:59:07 +03:00
|
|
|
public async sendContentToRoom(
|
2021-11-03 11:43:24 +03:00
|
|
|
file: File,
|
|
|
|
roomId: string,
|
2022-02-22 14:14:56 +03:00
|
|
|
relation: IEventRelation | undefined,
|
2021-11-03 11:43:24 +03:00
|
|
|
matrixClient: MatrixClient,
|
2022-03-21 15:03:59 +03:00
|
|
|
replyToEvent: MatrixEvent | undefined,
|
2022-10-12 20:59:07 +03:00
|
|
|
promBefore?: Promise<any>,
|
2023-01-12 16:25:14 +03:00
|
|
|
): Promise<void> {
|
2022-10-12 20:59:07 +03:00
|
|
|
const fileName = file.name || _t("Attachment");
|
|
|
|
const content: Omit<IMediaEventContent, "info"> & { info: Partial<IMediaEventInfo> } = {
|
|
|
|
body: fileName,
|
2015-12-02 21:16:16 +03:00
|
|
|
info: {
|
|
|
|
size: file.size,
|
2017-10-11 19:56:17 +03:00
|
|
|
},
|
2022-05-24 11:05:29 +03:00
|
|
|
msgtype: MsgType.File, // set more specifically later
|
2015-12-02 21:16:16 +03:00
|
|
|
};
|
|
|
|
|
2023-03-23 14:47:40 +03:00
|
|
|
// Attach mentions, which really only applies if there's a replyToEvent.
|
|
|
|
attachMentions(matrixClient.getSafeUserId(), content, null, replyToEvent);
|
2022-03-31 20:40:35 +03:00
|
|
|
attachRelation(content, relation);
|
2022-03-21 15:03:59 +03:00
|
|
|
if (replyToEvent) {
|
|
|
|
addReplyToMessageContent(content, replyToEvent, {
|
|
|
|
includeLegacyFallback: false,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-09-08 20:26:54 +03:00
|
|
|
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
|
|
|
decorateStartSendingTime(content);
|
|
|
|
}
|
|
|
|
|
2015-12-02 21:16:16 +03:00
|
|
|
// if we have a mime type for the file, add it to the message metadata
|
|
|
|
if (file.type) {
|
|
|
|
content.info.mimetype = file.type;
|
|
|
|
}
|
|
|
|
|
2022-10-12 20:59:07 +03:00
|
|
|
const upload = new RoomUpload(roomId, fileName, relation, file.size);
|
|
|
|
this.inprogress.push(upload);
|
|
|
|
dis.dispatch<UploadStartedPayload>({ action: Action.UploadStarted, upload });
|
|
|
|
|
2023-01-12 16:25:14 +03:00
|
|
|
function onProgress(progress: UploadProgress): void {
|
2022-10-12 20:59:07 +03:00
|
|
|
upload.onProgress(progress);
|
|
|
|
dis.dispatch<UploadProgressPayload>({ action: Action.UploadProgress, upload });
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (file.type.startsWith("image/")) {
|
2021-10-28 11:40:38 +03:00
|
|
|
content.msgtype = MsgType.Image;
|
2022-10-12 20:59:07 +03:00
|
|
|
try {
|
|
|
|
const imageInfo = await infoForImageFile(matrixClient, roomId, file);
|
2020-10-13 19:36:40 +03:00
|
|
|
Object.assign(content.info, imageInfo);
|
2022-10-12 20:59:07 +03:00
|
|
|
} catch (e) {
|
2023-05-09 12:52:07 +03:00
|
|
|
if (e instanceof HTTPError) {
|
|
|
|
// re-throw to main upload error handler
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
// Otherwise we failed to thumbnail, fall back to uploading an m.file
|
2021-10-15 17:30:53 +03:00
|
|
|
logger.error(e);
|
2021-10-28 11:40:38 +03:00
|
|
|
content.msgtype = MsgType.File;
|
2022-10-12 20:59:07 +03:00
|
|
|
}
|
2020-05-24 17:47:52 +03:00
|
|
|
} else if (file.type.indexOf("audio/") === 0) {
|
2021-10-28 11:40:38 +03:00
|
|
|
content.msgtype = MsgType.Audio;
|
2020-05-24 17:47:52 +03:00
|
|
|
} else if (file.type.indexOf("video/") === 0) {
|
2021-10-28 11:40:38 +03:00
|
|
|
content.msgtype = MsgType.Video;
|
2022-10-12 20:59:07 +03:00
|
|
|
try {
|
|
|
|
const videoInfo = await infoForVideoFile(matrixClient, roomId, file);
|
2020-10-13 19:36:40 +03:00
|
|
|
Object.assign(content.info, videoInfo);
|
2022-10-12 20:59:07 +03:00
|
|
|
} catch (e) {
|
2022-05-24 11:05:29 +03:00
|
|
|
// Failed to thumbnail, fall back to uploading an m.file
|
|
|
|
logger.error(e);
|
2021-10-28 11:40:38 +03:00
|
|
|
content.msgtype = MsgType.File;
|
2022-10-12 20:59:07 +03:00
|
|
|
}
|
2019-11-12 14:40:38 +03:00
|
|
|
} else {
|
2021-10-28 11:40:38 +03:00
|
|
|
content.msgtype = MsgType.File;
|
2019-11-12 14:40:38 +03:00
|
|
|
}
|
2015-12-02 21:16:16 +03:00
|
|
|
|
2022-10-12 20:59:07 +03:00
|
|
|
if (upload.cancelled) throw new UploadCanceledError();
|
|
|
|
const result = await uploadFile(matrixClient, roomId, file, onProgress, upload.abortController);
|
|
|
|
content.file = result.file;
|
|
|
|
content.url = result.url;
|
2020-05-24 17:13:53 +03:00
|
|
|
|
2022-10-12 20:59:07 +03:00
|
|
|
if (upload.cancelled) throw new UploadCanceledError();
|
|
|
|
// Await previous message being sent into the room
|
|
|
|
if (promBefore) await promBefore;
|
2015-12-02 21:16:16 +03:00
|
|
|
|
2022-10-12 20:59:07 +03:00
|
|
|
if (upload.cancelled) throw new UploadCanceledError();
|
|
|
|
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null;
|
|
|
|
|
2023-02-24 18:28:40 +03:00
|
|
|
const response = await matrixClient.sendMessage(roomId, threadId ?? null, content);
|
2017-07-14 19:01:03 +03:00
|
|
|
|
2021-09-08 20:26:54 +03:00
|
|
|
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
2022-10-12 20:59:07 +03:00
|
|
|
sendRoundTripMetric(matrixClient, roomId, response.event_id);
|
2021-09-08 20:26:54 +03:00
|
|
|
}
|
2022-10-12 20:59:07 +03:00
|
|
|
|
|
|
|
dis.dispatch<UploadFinishedPayload>({ action: Action.UploadFinished, upload });
|
|
|
|
dis.dispatch({ action: "message_sent" });
|
|
|
|
} catch (error) {
|
|
|
|
// 413: File was too big or upset the server in some way:
|
|
|
|
// clear the media size limit so we fetch it again next time we try to upload
|
2023-05-16 16:25:43 +03:00
|
|
|
if (error instanceof HTTPError && error.httpStatus === 413) {
|
2022-10-12 20:59:07 +03:00
|
|
|
this.mediaConfig = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!upload.cancelled) {
|
2021-06-29 15:11:58 +03:00
|
|
|
let desc = _t("The file '%(fileName)s' failed to upload.", { fileName: upload.fileName });
|
2023-05-16 16:25:43 +03:00
|
|
|
if (error instanceof HTTPError && error.httpStatus === 413) {
|
2019-04-01 18:42:41 +03:00
|
|
|
desc = _t("The file '%(fileName)s' exceeds this homeserver's size limit for uploads", {
|
2021-06-29 15:11:58 +03:00
|
|
|
fileName: upload.fileName,
|
2019-04-01 18:42:41 +03:00
|
|
|
});
|
2015-12-03 13:52:06 +03:00
|
|
|
}
|
2022-06-14 19:51:51 +03:00
|
|
|
Modal.createDialog(ErrorDialog, {
|
2017-05-23 17:16:31 +03:00
|
|
|
title: _t("Upload Failed"),
|
|
|
|
description: desc,
|
2015-12-03 13:52:06 +03:00
|
|
|
});
|
2021-06-29 15:11:58 +03:00
|
|
|
dis.dispatch<UploadErrorPayload>({ action: Action.UploadFailed, upload, error });
|
2016-02-15 22:29:56 +03:00
|
|
|
}
|
2022-10-12 20:59:07 +03:00
|
|
|
} finally {
|
|
|
|
removeElement(this.inprogress, (e) => e.promise === upload.promise);
|
|
|
|
}
|
2015-07-08 16:34:26 +03:00
|
|
|
}
|
|
|
|
|
2023-01-12 16:25:14 +03:00
|
|
|
private isFileSizeAcceptable(file: File): boolean {
|
2020-05-24 18:00:57 +03:00
|
|
|
if (
|
|
|
|
this.mediaConfig !== null &&
|
|
|
|
this.mediaConfig["m.upload.size"] !== undefined &&
|
|
|
|
file.size > this.mediaConfig["m.upload.size"]
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
2015-12-02 21:16:16 +03:00
|
|
|
}
|
|
|
|
|
2022-05-24 11:05:29 +03:00
|
|
|
private ensureMediaConfigFetched(matrixClient: MatrixClient): Promise<void> {
|
2023-02-16 20:21:44 +03:00
|
|
|
if (this.mediaConfig !== null) return Promise.resolve();
|
2020-05-24 18:00:57 +03:00
|
|
|
|
2021-09-21 18:48:09 +03:00
|
|
|
logger.log("[Media Config] Fetching");
|
2021-05-20 15:23:17 +03:00
|
|
|
return matrixClient
|
|
|
|
.getMediaConfig()
|
|
|
|
.then((config) => {
|
2021-09-21 18:48:09 +03:00
|
|
|
logger.log("[Media Config] Fetched config:", config);
|
2020-05-24 18:00:57 +03:00
|
|
|
return config;
|
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
// Media repo can't or won't report limits, so provide an empty object (no limits).
|
2021-09-21 18:48:09 +03:00
|
|
|
logger.log("[Media Config] Could not fetch config, so not limiting uploads.");
|
2020-05-24 18:00:57 +03:00
|
|
|
return {};
|
|
|
|
})
|
|
|
|
.then((config) => {
|
|
|
|
this.mediaConfig = config;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-01-12 16:25:14 +03:00
|
|
|
public static sharedInstance(): ContentMessages {
|
2020-07-20 22:43:49 +03:00
|
|
|
if (window.mxContentMessages === undefined) {
|
|
|
|
window.mxContentMessages = new ContentMessages();
|
2015-12-02 21:16:16 +03:00
|
|
|
}
|
2020-07-20 22:43:49 +03:00
|
|
|
return window.mxContentMessages;
|
2015-12-02 21:16:16 +03:00
|
|
|
}
|
|
|
|
}
|