2025-05-09 14:42:37 +02:00
|
|
|
import axios from "axios";
|
2020-11-21 14:57:43 +01:00
|
|
|
import * as ContentHelpers from "matrix-js-sdk/lib/content-helpers";
|
2025-04-22 13:01:18 +02:00
|
|
|
import imageResize from "image-resize";
|
2025-06-09 09:44:37 +02:00
|
|
|
import { AutoDiscovery, Method } from "matrix-js-sdk";
|
2025-05-09 14:42:37 +02:00
|
|
|
import User from "../models/user";
|
2025-03-31 16:33:54 +02:00
|
|
|
import prettyBytes from "pretty-bytes";
|
2025-05-09 14:42:37 +02:00
|
|
|
import { Thread } from "matrix-js-sdk/lib/models/thread";
|
2025-04-22 13:01:18 +02:00
|
|
|
import { imageSize } from "image-size";
|
2025-03-31 16:33:54 +02:00
|
|
|
import dayjs from "dayjs";
|
|
|
|
|
import aesjs from "aes-js";
|
2025-05-09 14:42:37 +02:00
|
|
|
import localizedFormat from "dayjs/plugin/localizedFormat";
|
|
|
|
|
import duration from "dayjs/plugin/duration";
|
|
|
|
|
import i18n from "./lang";
|
2025-06-09 09:44:37 +02:00
|
|
|
import { toRaw, isRef, isReactive, isProxy } from "vue";
|
|
|
|
|
import { UploadPromise } from "../models/attachment";
|
2025-09-16 12:45:50 +02:00
|
|
|
import png from "@/plugins/png.ts";
|
2023-01-09 14:21:08 +00:00
|
|
|
|
2023-09-29 10:28:33 +02:00
|
|
|
export const STATE_EVENT_ROOM_DELETION_NOTICE = "im.keanu.room_deletion_notice";
|
|
|
|
|
export const STATE_EVENT_ROOM_DELETED = "im.keanu.room_deleted";
|
|
|
|
|
|
2023-06-28 12:14:44 +00:00
|
|
|
export const ROOM_TYPE_DEFAULT = "im.keanu.room_type_default";
|
2023-02-17 22:00:47 +01:00
|
|
|
export const ROOM_TYPE_VOICE_MODE = "im.keanu.room_type_voice";
|
2023-06-28 12:14:44 +00:00
|
|
|
export const ROOM_TYPE_FILE_MODE = "im.keanu.room_type_file";
|
2024-04-03 09:34:24 +02:00
|
|
|
export const ROOM_TYPE_CHANNEL = "im.keanu.room_type_channel";
|
2023-02-17 22:00:47 +01:00
|
|
|
|
2023-08-07 14:13:35 +00:00
|
|
|
export const STATE_EVENT_ROOM_TYPE = "im.keanu.room_type";
|
2025-08-25 14:47:12 +02:00
|
|
|
export const CLIENT_EVENT_PROOF_HINT = "im.keanu.proof_hint";
|
2023-08-07 14:13:35 +00:00
|
|
|
|
2025-08-27 16:08:52 +02:00
|
|
|
export const THUMBNAIL_MAX_WIDTH = 640;
|
|
|
|
|
export const THUMBNAIL_MAX_HEIGHT = 640;
|
2025-06-09 09:44:37 +02:00
|
|
|
|
2021-01-20 11:32:21 +01:00
|
|
|
// Install extended localized format
|
2025-05-09 14:42:37 +02:00
|
|
|
dayjs.extend(localizedFormat);
|
2021-02-22 16:34:19 +01:00
|
|
|
dayjs.extend(duration);
|
2021-01-20 11:32:21 +01:00
|
|
|
|
2021-03-18 11:58:46 +01:00
|
|
|
// Store info about getUserMedia BEFORE we aply polyfill(s)!
|
2021-03-23 16:20:01 +01:00
|
|
|
var _browserCanRecordAudioF = function () {
|
2025-05-09 14:42:37 +02:00
|
|
|
var legacyGetUserMedia =
|
|
|
|
|
navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
|
|
|
|
|
return (
|
|
|
|
|
legacyGetUserMedia !== undefined || (navigator.mediaDevices && navigator.mediaDevices.getUserMedia !== undefined)
|
|
|
|
|
);
|
|
|
|
|
};
|
2021-03-18 11:58:46 +01:00
|
|
|
var _browserCanRecordAudio = _browserCanRecordAudioF();
|
|
|
|
|
|
2020-11-19 17:08:58 +01:00
|
|
|
class Util {
|
2025-03-31 16:33:54 +02:00
|
|
|
threadMessageType() {
|
|
|
|
|
return Thread.hasServerSideSupport ? "m.thread" : "io.element.thread";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getAttachment(matrixClient, useAuthedMedia, event, progressCallback, asBlob = false, abortController = undefined) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const content = event.getContent();
|
|
|
|
|
var url = null;
|
2025-05-13 16:34:12 +02:00
|
|
|
var mime = "image/png";
|
2025-03-31 16:33:54 +02:00
|
|
|
var file = null;
|
|
|
|
|
let decrypt = true;
|
|
|
|
|
if (content.url != null) {
|
2025-04-22 13:01:18 +02:00
|
|
|
url = matrixClient.mxcUrlToHttp(
|
|
|
|
|
content.url,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
useAuthedMedia
|
|
|
|
|
);
|
2025-03-31 16:33:54 +02:00
|
|
|
decrypt = false;
|
2025-05-13 16:34:12 +02:00
|
|
|
if (content.info) {
|
|
|
|
|
mime = content.info.mimetype;
|
|
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
} else if (content.file && content.file.url) {
|
|
|
|
|
file = content.file;
|
2025-04-22 13:01:18 +02:00
|
|
|
url = matrixClient.mxcUrlToHttp(
|
|
|
|
|
file.url,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
useAuthedMedia
|
|
|
|
|
);
|
2025-05-13 16:34:12 +02:00
|
|
|
mime = file.mimetype;
|
2025-03-31 16:33:54 +02:00
|
|
|
}
|
2024-06-28 11:49:55 +02:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
if (url == null) {
|
|
|
|
|
reject("No url found!");
|
|
|
|
|
}
|
2024-06-28 11:49:55 +02:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
axios
|
|
|
|
|
.get(url, {
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${matrixClient.getAccessToken()}`,
|
|
|
|
|
},
|
|
|
|
|
signal: abortController ? abortController.signal : undefined,
|
|
|
|
|
responseType: "arraybuffer",
|
|
|
|
|
onDownloadProgress: (progressEvent) => {
|
|
|
|
|
let percentCompleted = Math.floor((progressEvent.loaded * 100) / progressEvent.total);
|
|
|
|
|
if (progressCallback) {
|
|
|
|
|
progressCallback(percentCompleted);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
.then((response) => {
|
2025-04-22 13:01:18 +02:00
|
|
|
return decrypt ? this.decryptIfNeeded(file, response) : Promise.resolve({ buffer: response.data });
|
2025-03-31 16:33:54 +02:00
|
|
|
})
|
|
|
|
|
.then((bytes) => {
|
|
|
|
|
if (asBlob) {
|
2025-05-13 16:34:12 +02:00
|
|
|
resolve(new Blob([bytes.buffer], { type: mime }));
|
2023-05-26 15:56:59 +00:00
|
|
|
} else {
|
2025-05-13 16:34:12 +02:00
|
|
|
resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: mime })));
|
2025-03-31 16:33:54 +02:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
console.log("Download error: ", err);
|
|
|
|
|
reject(err);
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
if (progressCallback) {
|
|
|
|
|
progressCallback(null);
|
2023-05-26 15:56:59 +00:00
|
|
|
}
|
|
|
|
|
});
|
2025-03-31 16:33:54 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
async getThumbnail(matrixClient, useAuthedMedia, event, config, ignoredw, ignoredh) {
|
|
|
|
|
const content = event.getContent();
|
|
|
|
|
var url = null;
|
|
|
|
|
var mime = "image/png";
|
|
|
|
|
var file = null;
|
|
|
|
|
let decrypt = true;
|
|
|
|
|
if (!!content.info && !!content.info.thumbnail_url) {
|
2025-08-20 15:12:04 +02:00
|
|
|
url = matrixClient.mxcUrlToHttp(
|
|
|
|
|
content.info.thumbnail_url,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
useAuthedMedia
|
|
|
|
|
);
|
2025-06-09 09:44:37 +02:00
|
|
|
decrypt = false;
|
|
|
|
|
if (content.info.thumbnail_info) {
|
|
|
|
|
mime = content.info.thumbnail_info.mimetype;
|
2023-05-26 15:56:59 +00:00
|
|
|
}
|
2025-06-09 09:44:37 +02:00
|
|
|
} else if (content.url != null) {
|
2025-08-20 15:12:04 +02:00
|
|
|
url = matrixClient.mxcUrlToHttp(
|
|
|
|
|
content.url,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
useAuthedMedia
|
|
|
|
|
);
|
2025-06-09 09:44:37 +02:00
|
|
|
decrypt = false;
|
|
|
|
|
if (content.info) {
|
|
|
|
|
mime = content.info.mimetype;
|
2025-03-31 16:33:54 +02:00
|
|
|
}
|
2025-06-09 09:44:37 +02:00
|
|
|
} else if (content && content.info && content.info.thumbnail_file && content.info.thumbnail_file.url) {
|
|
|
|
|
file = content.info.thumbnail_file;
|
|
|
|
|
url = matrixClient.mxcUrlToHttp(file.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
|
|
|
|
|
mime = file.mimetype;
|
|
|
|
|
} else if (
|
|
|
|
|
content.file &&
|
|
|
|
|
content.file.url &&
|
|
|
|
|
this.getFileSize(event) > 0 &&
|
|
|
|
|
this.getFileSize(event) < config.maxSizeAutoDownloads
|
|
|
|
|
) {
|
|
|
|
|
// No thumb, use real url
|
|
|
|
|
file = content.file;
|
|
|
|
|
url = matrixClient.mxcUrlToHttp(file.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
|
|
|
|
|
mime = file.mimetype;
|
|
|
|
|
}
|
2020-12-03 10:00:23 +01:00
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
if (url == null) {
|
|
|
|
|
throw new Error("No url found!");
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-20 15:12:04 +02:00
|
|
|
const response = await axios.get(
|
|
|
|
|
url,
|
|
|
|
|
useAuthedMedia
|
|
|
|
|
? {
|
|
|
|
|
responseType: "arraybuffer",
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${matrixClient.getAccessToken()}`,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: { responseType: "arraybuffer" }
|
|
|
|
|
);
|
2025-06-09 09:44:37 +02:00
|
|
|
const bytes = decrypt ? await this.decryptIfNeeded(file, response) : { buffer: response.data };
|
|
|
|
|
return URL.createObjectURL(new Blob([bytes.buffer], { type: mime }));
|
2025-03-31 16:33:54 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-22 13:01:18 +02:00
|
|
|
b64toBuffer(val) {
|
|
|
|
|
const baseValue = val.replaceAll("-", "+").replaceAll("_", "/");
|
|
|
|
|
return Buffer.from(baseValue, "base64");
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
decryptIfNeeded(file, response) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2025-04-22 13:01:18 +02:00
|
|
|
const key = this.b64toBuffer(file.key.k);
|
|
|
|
|
const iv = this.b64toBuffer(file.iv);
|
|
|
|
|
const originalHash = this.b64toBuffer(file.hashes.sha256);
|
2025-03-31 16:33:54 +02:00
|
|
|
|
2025-04-22 13:01:18 +02:00
|
|
|
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
|
2025-03-31 16:33:54 +02:00
|
|
|
const data = new Uint8Array(response.data);
|
|
|
|
|
|
2025-04-22 13:01:18 +02:00
|
|
|
crypto.subtle
|
|
|
|
|
.digest("SHA-256", data)
|
|
|
|
|
.then((hash) => {
|
|
|
|
|
// Calculate sha256 and compare hashes
|
|
|
|
|
if (Buffer.compare(Buffer.from(hash), originalHash) != 0) {
|
|
|
|
|
reject("Hashes don't match!");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var decryptedBytes = aesCtr.decrypt(data);
|
|
|
|
|
resolve(decryptedBytes);
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
reject("Failed to calculate hash value");
|
|
|
|
|
});
|
2025-03-31 16:33:54 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-20 15:12:04 +02:00
|
|
|
sendTextMessage(matrixClient, roomId, text, editedEvent, replyToEvent, txnId) {
|
2025-09-11 16:00:18 +02:00
|
|
|
var content = ContentHelpers.makeTextMessage(text ?? "");
|
2025-03-31 16:33:54 +02:00
|
|
|
if (editedEvent) {
|
|
|
|
|
content["m.relates_to"] = {
|
|
|
|
|
rel_type: "m.replace",
|
|
|
|
|
event_id: editedEvent.getId(),
|
|
|
|
|
};
|
|
|
|
|
content["m.new_content"] = {
|
|
|
|
|
body: content.body,
|
|
|
|
|
msgtype: content.msgtype,
|
|
|
|
|
};
|
|
|
|
|
} else if (replyToEvent) {
|
|
|
|
|
content["m.relates_to"] = {
|
|
|
|
|
"m.in_reply_to": {
|
|
|
|
|
event_id: replyToEvent.getId(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let senderContent = replyToEvent.getContent();
|
|
|
|
|
|
|
|
|
|
const senderContentBody = Object.getOwnPropertyDescriptor(senderContent, "body")
|
|
|
|
|
? senderContent.body
|
|
|
|
|
: Object.values(senderContent)[0].question.body;
|
|
|
|
|
// Prefix the content with reply info (seems to be a legacy thing)
|
|
|
|
|
const prefix = senderContentBody
|
|
|
|
|
.split("\n")
|
|
|
|
|
.map((item, index) => {
|
|
|
|
|
return "> " + (index == 0 ? "<" + replyToEvent.getSender() + "> " : "") + item;
|
|
|
|
|
})
|
|
|
|
|
.join("\n");
|
|
|
|
|
content.body = prefix + "\n\n" + content.body;
|
|
|
|
|
}
|
2025-08-20 15:12:04 +02:00
|
|
|
return this.sendMessage(matrixClient, roomId, "m.room.message", content, txnId);
|
2025-03-31 16:33:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sendQuickReaction(matrixClient, roomId, emoji, event, extraData = {}) {
|
|
|
|
|
const content = {
|
|
|
|
|
"m.relates_to": Object.assign(extraData, {
|
|
|
|
|
key: emoji,
|
|
|
|
|
rel_type: "m.annotation",
|
|
|
|
|
event_id: event.getId(),
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
return this.sendMessage(matrixClient, roomId, "m.reaction", content);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createPoll(matrixClient, roomId, question, answers, isDisclosed) {
|
|
|
|
|
var idx = 0;
|
|
|
|
|
let answerData = answers.map((a) => {
|
|
|
|
|
idx++;
|
|
|
|
|
return { id: "" + idx, "org.matrix.msc1767.text": a.text };
|
|
|
|
|
});
|
|
|
|
|
const content = {
|
|
|
|
|
"org.matrix.msc3381.poll.start": {
|
|
|
|
|
question: {
|
|
|
|
|
"org.matrix.msc1767.text": question,
|
|
|
|
|
body: question,
|
|
|
|
|
},
|
|
|
|
|
kind: isDisclosed ? "org.matrix.msc3381.poll.disclosed" : "org.matrix.msc3381.poll.undisclosed",
|
|
|
|
|
max_selections: 1,
|
|
|
|
|
answers: answerData,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.start", content);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
closePoll(matrixClient, roomId, event) {
|
|
|
|
|
const content = {
|
|
|
|
|
"m.relates_to": {
|
|
|
|
|
rel_type: "m.reference",
|
|
|
|
|
event_id: event.getId(),
|
|
|
|
|
},
|
|
|
|
|
"org.matrix.msc3381.poll.end": {},
|
|
|
|
|
};
|
|
|
|
|
return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.end", content);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sendPollAnswer(matrixClient, roomId, answers, event) {
|
|
|
|
|
const content = {
|
|
|
|
|
"m.relates_to": {
|
|
|
|
|
rel_type: "m.reference",
|
|
|
|
|
event_id: event.getId(),
|
|
|
|
|
},
|
|
|
|
|
"org.matrix.msc3381.poll.response": {
|
|
|
|
|
answers: answers,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.response", content);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-20 15:12:04 +02:00
|
|
|
sendMessage(matrixClient, roomId, eventType, content, txnId) {
|
2025-03-31 16:33:54 +02:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
matrixClient
|
2025-08-20 15:12:04 +02:00
|
|
|
.sendEvent(roomId, eventType, content, txnId, undefined)
|
2025-03-31 16:33:54 +02:00
|
|
|
.then((result) => {
|
|
|
|
|
console.log("Message sent: ", result);
|
|
|
|
|
resolve(result.event_id);
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
console.log("Send error: ", err);
|
|
|
|
|
if (err && err.name == "UnknownDeviceError") {
|
|
|
|
|
console.log("Unknown devices. Mark as known before retrying.");
|
|
|
|
|
var setAsKnownPromises = [];
|
|
|
|
|
for (var user of Object.keys(err.devices)) {
|
|
|
|
|
const userDevices = err.devices[user];
|
|
|
|
|
for (var deviceId of Object.keys(userDevices)) {
|
|
|
|
|
const deviceInfo = userDevices[deviceId];
|
|
|
|
|
if (!deviceInfo.known) {
|
|
|
|
|
setAsKnownPromises.push(matrixClient.setDeviceKnown(user, deviceId, true));
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-11-19 17:08:58 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
Promise.all(setAsKnownPromises).then(() => {
|
|
|
|
|
// All devices now marked as "known", try to resend
|
|
|
|
|
let event = err.event;
|
|
|
|
|
if (!event) {
|
|
|
|
|
// Seems event is no longer send in the UnknownDevices error...
|
|
|
|
|
const room = matrixClient.getRoom(roomId);
|
|
|
|
|
if (room) {
|
|
|
|
|
event = room
|
|
|
|
|
.getLiveTimeline()
|
|
|
|
|
.getEvents()
|
|
|
|
|
.find((e) => {
|
|
|
|
|
// Find the exact match (= object equality)
|
|
|
|
|
return e.error === err;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
matrixClient
|
|
|
|
|
.resendEvent(event, matrixClient.getRoom(event.getRoomId()))
|
|
|
|
|
.then((result) => {
|
|
|
|
|
console.log("Message sent: ", result);
|
|
|
|
|
resolve(result.event_id);
|
2020-11-19 17:08:58 +01:00
|
|
|
})
|
2025-03-31 16:33:54 +02:00
|
|
|
.catch((err) => {
|
|
|
|
|
// Still error, abort
|
|
|
|
|
reject(err.toLocaleString());
|
2020-11-19 17:08:58 +01:00
|
|
|
});
|
2025-03-31 16:33:54 +02:00
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
reject(err.toLocaleString());
|
|
|
|
|
}
|
2020-11-19 17:08:58 +01:00
|
|
|
});
|
2025-03-31 16:33:54 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
async encryptFileAndGenerateInfo(data, mime) {
|
|
|
|
|
let key = Buffer.from(crypto.getRandomValues(new Uint8Array(256 / 8)));
|
|
|
|
|
let iv = Buffer.concat([Buffer.from(crypto.getRandomValues(new Uint8Array(8))), Buffer.alloc(8)]); // Initialization vector.
|
|
|
|
|
|
|
|
|
|
// Encrypt
|
|
|
|
|
let aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
|
|
|
|
|
let encryptedBytes = aesCtr.encrypt(data);
|
|
|
|
|
|
|
|
|
|
// Calculate sha256
|
|
|
|
|
let hash = await crypto.subtle.digest("SHA-256", encryptedBytes);
|
|
|
|
|
|
|
|
|
|
const jwk = {
|
|
|
|
|
kty: "oct",
|
|
|
|
|
key_ops: ["encrypt", "decrypt"],
|
|
|
|
|
alg: "A256CTR",
|
|
|
|
|
k: key.toString("base64").replaceAll(/\//g, "_").replaceAll(/\+/g, "-"),
|
|
|
|
|
ext: true,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const encryptedFile = {
|
|
|
|
|
mimetype: mime,
|
|
|
|
|
key: jwk,
|
|
|
|
|
iv: Buffer.from(iv).toString("base64").replace(/=/g, ""),
|
|
|
|
|
hashes: { sha256: Buffer.from(hash).toString("base64").replace(/=/g, "") },
|
|
|
|
|
v: "v2",
|
|
|
|
|
};
|
|
|
|
|
return [encryptedBytes, encryptedFile];
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-25 14:47:12 +02:00
|
|
|
sendFile(matrixClient, roomId, file, onUploadProgress, threadRoot, dimensions, thumbnail, proofHintFlags) {
|
2025-03-31 16:33:54 +02:00
|
|
|
const uploadPromise = new UploadPromise(undefined);
|
|
|
|
|
uploadPromise.wrappedPromise = new Promise((resolve, reject) => {
|
|
|
|
|
var reader = new FileReader();
|
2025-05-13 16:34:12 +02:00
|
|
|
reader.onload = async (e) => {
|
2025-03-31 16:33:54 +02:00
|
|
|
if (uploadPromise.aborted) {
|
|
|
|
|
reject("Aborted");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-06-09 09:44:37 +02:00
|
|
|
try {
|
|
|
|
|
const fileContents = e.target.result;
|
2020-12-03 10:00:23 +01:00
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
var data = new Uint8Array(fileContents);
|
|
|
|
|
let thumbnailData = undefined;
|
|
|
|
|
let thumbnailInfo = undefined;
|
2020-11-21 14:57:43 +01:00
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
const info = {
|
|
|
|
|
mimetype: file.type,
|
|
|
|
|
size: file.size,
|
|
|
|
|
};
|
2020-11-25 14:42:50 +01:00
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
// If audio, send duration in ms as well
|
|
|
|
|
if (file.duration) {
|
|
|
|
|
info.duration = file.duration;
|
|
|
|
|
}
|
2022-05-03 09:40:02 +00:00
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
var description = file.name;
|
|
|
|
|
var msgtype = "m.file";
|
2025-08-20 15:12:04 +02:00
|
|
|
|
2025-07-10 09:46:07 +02:00
|
|
|
if (thumbnail) {
|
|
|
|
|
thumbnailData = thumbnail.data;
|
|
|
|
|
thumbnailInfo = {
|
|
|
|
|
mimetype: thumbnail.mimetype,
|
|
|
|
|
size: thumbnail.size,
|
|
|
|
|
w: thumbnail.w,
|
|
|
|
|
h: thumbnail.h,
|
|
|
|
|
};
|
2025-08-20 15:12:04 +02:00
|
|
|
}
|
|
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
if (file.type.startsWith("image/")) {
|
|
|
|
|
msgtype = "m.image";
|
|
|
|
|
|
|
|
|
|
// Generate thumbnail?
|
2025-07-10 09:46:07 +02:00
|
|
|
if (dimensions && !thumbnail) {
|
2025-06-09 09:44:37 +02:00
|
|
|
const w = dimensions.width;
|
|
|
|
|
const h = dimensions.height;
|
|
|
|
|
if (w > THUMBNAIL_MAX_WIDTH || h > THUMBNAIL_MAX_HEIGHT) {
|
|
|
|
|
var aspect = w / h;
|
|
|
|
|
var newWidth = parseInt((w > h ? THUMBNAIL_MAX_WIDTH : THUMBNAIL_MAX_HEIGHT * aspect).toFixed());
|
|
|
|
|
var newHeight = parseInt((w > h ? THUMBNAIL_MAX_WIDTH / aspect : THUMBNAIL_MAX_HEIGHT).toFixed());
|
|
|
|
|
const scaled = await imageResize(file, {
|
|
|
|
|
format: "webp",
|
|
|
|
|
width: newWidth,
|
|
|
|
|
height: newHeight,
|
|
|
|
|
outputType: "blob",
|
2025-08-20 15:12:04 +02:00
|
|
|
}).catch(() => {
|
|
|
|
|
return Promise.resolve(undefined);
|
|
|
|
|
});
|
2025-06-09 09:44:37 +02:00
|
|
|
if (scaled && file.size > scaled.size) {
|
|
|
|
|
thumbnailData = new Uint8Array(await scaled.arrayBuffer());
|
|
|
|
|
thumbnailInfo = {
|
|
|
|
|
mimetype: scaled.type,
|
|
|
|
|
size: scaled.size,
|
|
|
|
|
h: newHeight,
|
|
|
|
|
w: newWidth,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (file.type.startsWith("audio/")) {
|
|
|
|
|
msgtype = "m.audio";
|
|
|
|
|
} else if (file.type.startsWith("video/")) {
|
|
|
|
|
msgtype = "m.video";
|
|
|
|
|
}
|
2022-05-03 09:40:02 +00:00
|
|
|
|
2025-08-25 14:47:12 +02:00
|
|
|
let messageContent = {
|
2025-06-09 09:44:37 +02:00
|
|
|
body: description,
|
|
|
|
|
info: info,
|
|
|
|
|
msgtype: msgtype,
|
2025-03-31 16:33:54 +02:00
|
|
|
};
|
2022-05-03 09:40:02 +00:00
|
|
|
|
2025-09-12 17:05:22 +02:00
|
|
|
if (proofHintFlags) {
|
|
|
|
|
messageContent[CLIENT_EVENT_PROOF_HINT] = JSON.stringify(proofHintFlags);
|
|
|
|
|
}
|
2025-08-25 14:47:12 +02:00
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
// If thread root (an eventId) is set, add that here
|
|
|
|
|
if (threadRoot) {
|
|
|
|
|
messageContent["m.relates_to"] = {
|
|
|
|
|
rel_type: this.threadMessageType(),
|
|
|
|
|
event_id: threadRoot,
|
|
|
|
|
};
|
|
|
|
|
}
|
2020-11-25 14:42:50 +01:00
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
// Set filename for files
|
|
|
|
|
if (msgtype == "m.file") {
|
|
|
|
|
messageContent.filename = file.name;
|
|
|
|
|
}
|
2023-06-25 17:12:29 +03:00
|
|
|
|
2025-08-20 15:12:04 +02:00
|
|
|
let totalBytes = 0;
|
|
|
|
|
let thumbBytes = 0;
|
|
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
const useEncryption = matrixClient.isRoomEncrypted(roomId);
|
2020-12-03 10:00:23 +01:00
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
const dataUploadOpts = {
|
|
|
|
|
type: useEncryption ? "application/octet-stream" : file.type,
|
|
|
|
|
name: description,
|
2025-08-20 15:12:04 +02:00
|
|
|
progressHandler: ({ loaded, total }) => {
|
|
|
|
|
if (onUploadProgress) {
|
|
|
|
|
onUploadProgress({ loaded: loaded + thumbBytes, total: totalBytes });
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-06-09 09:44:37 +02:00
|
|
|
onlyContentUri: false,
|
|
|
|
|
};
|
2020-12-03 10:00:23 +01:00
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
if (useEncryption) {
|
|
|
|
|
const [encryptedBytes, encryptedFile] = await this.encryptFileAndGenerateInfo(data, file.type);
|
|
|
|
|
messageContent.file = encryptedFile;
|
|
|
|
|
data = encryptedBytes;
|
|
|
|
|
}
|
2020-12-03 10:00:23 +01:00
|
|
|
|
2025-08-20 15:12:04 +02:00
|
|
|
totalBytes = data.length;
|
|
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
if (thumbnailData) {
|
|
|
|
|
messageContent.thumbnail_info = thumbnailInfo;
|
|
|
|
|
if (useEncryption) {
|
|
|
|
|
const [encryptedBytes, encryptedFile] = await this.encryptFileAndGenerateInfo(thumbnailData, file.type);
|
|
|
|
|
messageContent.info.thumbnail_file = encryptedFile;
|
|
|
|
|
thumbnailData = encryptedBytes;
|
|
|
|
|
}
|
2025-08-20 15:12:04 +02:00
|
|
|
thumbBytes = thumbnailData.length;
|
|
|
|
|
totalBytes += thumbnailData.length;
|
2025-06-09 09:44:37 +02:00
|
|
|
const thumbnailUploadOpts = {
|
|
|
|
|
type: useEncryption ? "application/octet-stream" : file.type,
|
|
|
|
|
name: "thumb:" + description,
|
2025-08-20 15:12:04 +02:00
|
|
|
progressHandler: ({ loaded, total }) => {
|
|
|
|
|
if (onUploadProgress) {
|
|
|
|
|
onUploadProgress({ loaded: loaded, total: totalBytes });
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-06-09 09:44:37 +02:00
|
|
|
onlyContentUri: false,
|
|
|
|
|
};
|
|
|
|
|
const thumbUploadPromise = matrixClient.uploadContent(thumbnailData, thumbnailUploadOpts);
|
|
|
|
|
uploadPromise.onAbort = () => {
|
|
|
|
|
matrixClient.cancelUpload(thumbUploadPromise);
|
|
|
|
|
};
|
|
|
|
|
const thumbnailResponse = await thumbUploadPromise;
|
|
|
|
|
if (useEncryption) {
|
|
|
|
|
messageContent.info.thumbnail_file.url = thumbnailResponse.content_uri;
|
|
|
|
|
} else {
|
|
|
|
|
messageContent.info.thumbnail_url = thumbnailResponse.content_uri;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-06-28 12:14:44 +00:00
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
const dataUploadPromise = matrixClient.uploadContent(data, dataUploadOpts);
|
|
|
|
|
uploadPromise.onAbort = () => {
|
|
|
|
|
matrixClient.cancelUpload(dataUploadPromise);
|
|
|
|
|
};
|
|
|
|
|
const response = await dataUploadPromise;
|
|
|
|
|
if (useEncryption) {
|
|
|
|
|
messageContent.file.url = response.content_uri;
|
|
|
|
|
} else {
|
|
|
|
|
messageContent.url = response.content_uri;
|
|
|
|
|
}
|
2020-12-03 10:00:23 +01:00
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
// Generate audio waveforms
|
|
|
|
|
if (msgtype == "m.audio") {
|
2025-09-10 18:06:41 +02:00
|
|
|
await this.generateWaveform(fileContents, messageContent);
|
2025-06-09 09:44:37 +02:00
|
|
|
}
|
2020-11-25 14:42:50 +01:00
|
|
|
|
2025-06-09 09:44:37 +02:00
|
|
|
const result = await this.sendMessage(matrixClient, roomId, "m.room.message", messageContent);
|
|
|
|
|
resolve(result);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
reject(error);
|
|
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
};
|
|
|
|
|
reader.onerror = (err) => {
|
|
|
|
|
reject(err);
|
|
|
|
|
};
|
|
|
|
|
reader.readAsArrayBuffer(file);
|
|
|
|
|
});
|
|
|
|
|
return uploadPromise;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 18:06:41 +02:00
|
|
|
async generateWaveform(data, messageContent) {
|
2025-03-31 16:33:54 +02:00
|
|
|
if (!(window.AudioContext || window.webkitAudioContext)) {
|
|
|
|
|
return; // No support
|
2020-11-21 14:57:43 +01:00
|
|
|
}
|
2025-09-10 18:06:41 +02:00
|
|
|
try {
|
|
|
|
|
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
|
|
|
if (audioCtx) {
|
|
|
|
|
const audioBuffer = await audioCtx.decodeAudioData(data);
|
|
|
|
|
if (audioBuffer) {
|
|
|
|
|
const rawData = audioBuffer.getChannelData(0); // TODO - currently using only 1 channel
|
|
|
|
|
const samples = 1000; // Number of samples
|
|
|
|
|
const blockSize = Math.floor(rawData.length / samples);
|
|
|
|
|
let filteredData = [];
|
|
|
|
|
for (let i = 0; i < samples; i++) {
|
|
|
|
|
let blockStart = blockSize * i; // the location of the first sample in the block
|
|
|
|
|
let sum = 0;
|
|
|
|
|
for (let j = 0; j < blockSize; j++) {
|
|
|
|
|
sum = sum + Math.abs(rawData[blockStart + j]); // find the sum of all the samples in the block
|
|
|
|
|
}
|
|
|
|
|
filteredData.push(sum / blockSize); // divide the sum by the block size to get the average
|
2025-03-31 16:33:54 +02:00
|
|
|
}
|
2024-10-25 11:56:27 +02:00
|
|
|
|
2025-09-10 18:06:41 +02:00
|
|
|
// Normalize
|
|
|
|
|
const multiplier = Math.pow(Math.max(...filteredData), -1);
|
|
|
|
|
filteredData = filteredData.map((n) => n * multiplier);
|
|
|
|
|
|
|
|
|
|
// Integerize
|
|
|
|
|
filteredData = filteredData.map((n) => parseInt((n * 255).toFixed()));
|
|
|
|
|
|
|
|
|
|
// Generate SVG of waveform
|
|
|
|
|
let svg = `<svg viewBox="0 0 ${samples} 255" fill="none" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">`;
|
|
|
|
|
svg += `<path d="`;
|
|
|
|
|
filteredData.forEach((d, i) => {
|
|
|
|
|
const delta = d / 2;
|
|
|
|
|
svg += `M${i} ${128 - delta}V${128 + delta}`;
|
|
|
|
|
});
|
|
|
|
|
svg += `" style="fill:none;stroke:green;stroke-width:1" />`;
|
|
|
|
|
svg += "</svg>";
|
|
|
|
|
|
|
|
|
|
messageContent.format = "org.matrix.custom.html";
|
|
|
|
|
messageContent.formatted_body = svg;
|
|
|
|
|
|
|
|
|
|
// if duration is not set, do that here, since we have it
|
|
|
|
|
if (!messageContent.info.duration) {
|
|
|
|
|
messageContent.info.duration = parseInt((1000 * audioBuffer.duration).toFixed());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return;
|
2025-03-31 16:33:54 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return what "mode" to use for the given room.
|
|
|
|
|
*
|
|
|
|
|
* The default value is given by the room itself (as state events, see roomTypeMixin).
|
|
|
|
|
* This method just returns if the user has overridden this in room settings (this
|
|
|
|
|
* fact will be persisted as a user specific tag on the room). Note: currently override
|
|
|
|
|
* is disabled in the UI...
|
|
|
|
|
*/
|
|
|
|
|
roomDisplayTypeOverride(roomOrNull) {
|
|
|
|
|
if (roomOrNull) {
|
|
|
|
|
const room = roomOrNull;
|
|
|
|
|
|
|
|
|
|
// Have we changed our local view mode of this room?
|
|
|
|
|
const tags = room.tags;
|
|
|
|
|
if (tags && tags["ui_options"]) {
|
|
|
|
|
if (tags["ui_options"]["voice_mode"] === 1) {
|
|
|
|
|
return ROOM_TYPE_VOICE_MODE;
|
|
|
|
|
} else if (tags["ui_options"]["file_mode"] === 1) {
|
|
|
|
|
return ROOM_TYPE_FILE_MODE;
|
|
|
|
|
} else if (tags["ui_options"]["file_mode"] === 0 && tags["ui_options"]["file_mode"] === 0) {
|
|
|
|
|
// Explicitly set to "default"
|
|
|
|
|
return ROOM_TYPE_DEFAULT;
|
2022-07-01 08:55:26 +00:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
}
|
2020-12-10 15:12:40 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return the room type for the current room
|
|
|
|
|
* @param {*} roomOrNull
|
|
|
|
|
*/
|
|
|
|
|
roomDisplayTypeToQueryParam(roomOrNull, roomDisplayType) {
|
|
|
|
|
const roomType = this.roomDisplayTypeOverride(roomOrNull) || roomDisplayType;
|
|
|
|
|
if (roomType === ROOM_TYPE_FILE_MODE) {
|
|
|
|
|
// Send "file" here, so the receiver of the invite link knows to display the "file drop" join page
|
|
|
|
|
// instead of the standard one.
|
|
|
|
|
return "file";
|
|
|
|
|
} else if (roomType === ROOM_TYPE_VOICE_MODE) {
|
|
|
|
|
// No need to return "voice" here. The invite page looks the same for default and voice mode,
|
|
|
|
|
// so currently no point in cluttering the invite link with it. The corrent mode will be picked up from
|
|
|
|
|
// room creation flags once the user joins.
|
|
|
|
|
return undefined;
|
2020-12-14 16:11:38 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return undefined; // Default, just return undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Generate a random user name */
|
|
|
|
|
randomUser(prefix) {
|
|
|
|
|
var pfx = prefix ? prefix.replace(/[^0-9a-zA-Z\-_]/gi, "") : null;
|
|
|
|
|
if (!pfx || pfx.length == 0) {
|
|
|
|
|
pfx = "weblite-";
|
2020-12-10 15:12:40 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return pfx + this.randomString(12, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate random 12 char password
|
|
|
|
|
*/
|
|
|
|
|
randomPass() {
|
|
|
|
|
return this.randomString(12, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#_-*+");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// From here: https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript
|
|
|
|
|
randomString(length, characters) {
|
|
|
|
|
var result = "";
|
|
|
|
|
var charactersLength = characters.length;
|
|
|
|
|
for (var i = 0; i < length; i++) {
|
|
|
|
|
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
2021-01-12 11:26:01 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return result;
|
|
|
|
|
}
|
2021-01-14 16:17:05 +01:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
sanitizeRoomId(roomId) {
|
|
|
|
|
if (roomId && roomId.match(/^(!|#).+$/)) {
|
|
|
|
|
return roomId;
|
2022-12-12 16:10:53 +00:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return null;
|
|
|
|
|
}
|
2022-12-12 16:10:53 +00:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
sanitizeUserId(userId) {
|
|
|
|
|
if (userId && userId.match(/^([0-9a-z-.=_/]+|@[0-9a-z-.=_/]+:.+)$/)) {
|
|
|
|
|
return userId;
|
2023-10-17 11:01:49 +02:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return null;
|
|
|
|
|
}
|
2023-10-17 11:01:49 +02:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
invalidUserIdChars() {
|
|
|
|
|
return /[^0-9a-z-.=_/]+/g;
|
|
|
|
|
}
|
2021-03-03 12:29:55 +01:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
getFirstVisibleElement(parentNode, where) {
|
|
|
|
|
let visible = this.findVisibleElements(parentNode);
|
|
|
|
|
if (visible) {
|
|
|
|
|
visible = visible.filter(where);
|
2021-03-04 11:31:21 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
if (visible && visible.length > 0) {
|
|
|
|
|
return visible[0];
|
2021-03-03 12:29:55 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return null;
|
|
|
|
|
}
|
2021-01-14 16:17:05 +01:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
getLastVisibleElement(parentNode, where) {
|
|
|
|
|
let visible = this.findVisibleElements(parentNode);
|
|
|
|
|
if (visible) {
|
|
|
|
|
visible = visible.filter(where);
|
2021-03-04 11:31:21 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
if (visible && visible.length > 0) {
|
|
|
|
|
return visible[visible.length - 1];
|
2021-01-14 16:17:05 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
findVisibleElements(parentNode) {
|
|
|
|
|
const middle = this.findOneVisibleElement(parentNode);
|
|
|
|
|
if (middle) {
|
|
|
|
|
var nodes = [parentNode.children[middle]];
|
|
|
|
|
var i = middle - 1;
|
|
|
|
|
while (i >= 0 && this.isChildVisible(parentNode, parentNode.children[i])) {
|
|
|
|
|
nodes.splice(0, 0, parentNode.children[i]);
|
|
|
|
|
i -= 1;
|
|
|
|
|
}
|
|
|
|
|
i = middle + 1;
|
|
|
|
|
while (i < parentNode.children.length && this.isChildVisible(parentNode, parentNode.children[i])) {
|
|
|
|
|
nodes.push(parentNode.children[i]);
|
|
|
|
|
i += 1;
|
|
|
|
|
}
|
|
|
|
|
return nodes;
|
2021-01-20 09:42:13 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return null; // No visible found
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isChildVisible(parentNode, childNode) {
|
|
|
|
|
const rect1 = parentNode.getBoundingClientRect();
|
|
|
|
|
const rect2 = childNode.getBoundingClientRect();
|
|
|
|
|
var overlap = !(
|
|
|
|
|
rect1.right <= rect2.left ||
|
|
|
|
|
rect1.left >= rect2.right ||
|
|
|
|
|
rect1.bottom <= rect2.top ||
|
|
|
|
|
rect1.top >= rect2.bottom
|
|
|
|
|
);
|
|
|
|
|
return overlap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
findOneVisibleElement(parentNode) {
|
|
|
|
|
let start = 0;
|
|
|
|
|
let end = parentNode && parentNode.children ? parentNode.children.length - 1 : -1;
|
|
|
|
|
while (start <= end) {
|
|
|
|
|
let middle = Math.floor((start + end) / 2);
|
|
|
|
|
let childNode = parentNode.children[middle];
|
|
|
|
|
if (this.isChildVisible(parentNode, childNode)) {
|
|
|
|
|
// found the key
|
|
|
|
|
return middle;
|
|
|
|
|
} else if (childNode.getBoundingClientRect().top <= parentNode.getBoundingClientRect().top) {
|
|
|
|
|
// continue searching to the right
|
|
|
|
|
start = middle + 1;
|
|
|
|
|
} else {
|
|
|
|
|
// search searching to the left
|
|
|
|
|
end = middle - 1;
|
|
|
|
|
}
|
2021-01-20 09:42:13 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
// key wasn't found
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_importAll(r) {
|
|
|
|
|
var images = [];
|
|
|
|
|
r.keys().forEach((res) => {
|
|
|
|
|
console.log("Avatar", res);
|
|
|
|
|
// // Remove"./"
|
|
|
|
|
var name = res.split("_")[1];
|
|
|
|
|
name = name.slice(0, name.indexOf("."));
|
|
|
|
|
name = name.charAt(0).toUpperCase() + name.slice(1);
|
|
|
|
|
const image = r(res);
|
|
|
|
|
const randomNumber = parseInt(this.randomString(4, "0123456789")).toFixed();
|
|
|
|
|
images.push({ id: res, image: image, name: "Guest " + name + " " + randomNumber });
|
|
|
|
|
});
|
|
|
|
|
return images;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getDefaultAvatars() {
|
|
|
|
|
var images = [];
|
2025-05-12 17:15:11 +02:00
|
|
|
const modules = import.meta.glob("@/assets/avatars/*.{jpeg,jpg,png}", {
|
|
|
|
|
query: "?url",
|
|
|
|
|
import: "default",
|
|
|
|
|
eager: true,
|
2025-05-12 12:39:48 +02:00
|
|
|
});
|
2025-03-31 16:33:54 +02:00
|
|
|
Object.keys(modules).map((path) => {
|
|
|
|
|
var name = path.split("_")[1];
|
|
|
|
|
name = name.slice(0, name.indexOf("."));
|
|
|
|
|
name = name.charAt(0).toUpperCase() + name.slice(1);
|
2025-05-12 12:39:48 +02:00
|
|
|
const image = modules[path];
|
2025-03-31 16:33:54 +02:00
|
|
|
const randomNumber = parseInt(this.randomString(4, "0123456789")).toFixed();
|
2025-05-12 12:39:48 +02:00
|
|
|
const title = "Guest " + name + " " + randomNumber;
|
|
|
|
|
images.push({ id: path, image: image, name: title, title: title });
|
2025-03-31 16:33:54 +02:00
|
|
|
});
|
|
|
|
|
return images;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-16 12:45:50 +02:00
|
|
|
/**
|
|
|
|
|
* For one of the predefined avatars, make it system unique by adding a random PNG comment header.
|
|
|
|
|
*/
|
|
|
|
|
makeUniqueAvatar(image) {
|
|
|
|
|
var list = png.splitChunk(image);
|
|
|
|
|
|
|
|
|
|
// append
|
|
|
|
|
var iend = list.pop(); // remove IEND
|
|
|
|
|
var newchunk = png.createChunk("tEXt","Comment\0Keanu avatar - " + this.randomPass());
|
|
|
|
|
list.push(newchunk);
|
|
|
|
|
list.push(iend);
|
|
|
|
|
|
|
|
|
|
// join
|
|
|
|
|
var newpng = png.joinChunk(list);
|
|
|
|
|
return newpng;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setAvatar(matrix, file, onUploadProgress, makeUnique) {
|
2025-03-31 16:33:54 +02:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
axios
|
|
|
|
|
.get(file, { responseType: "arraybuffer" })
|
|
|
|
|
.then((response) => {
|
|
|
|
|
const opts = {
|
|
|
|
|
type: response.headers["content-type"].split(";")[0],
|
|
|
|
|
name: "Avatar",
|
|
|
|
|
progressHandler: onUploadProgress,
|
|
|
|
|
onlyContentUri: false,
|
|
|
|
|
};
|
2025-09-16 12:45:50 +02:00
|
|
|
let data = response.data;
|
|
|
|
|
|
|
|
|
|
// If making a system unique avatar, we add a random PNG header in here
|
|
|
|
|
if (makeUnique) {
|
|
|
|
|
data = this.makeUniqueAvatar(data);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
var avatarUri;
|
|
|
|
|
matrix.matrixClient
|
2025-09-16 12:45:50 +02:00
|
|
|
.uploadContent(data, opts)
|
2025-03-31 16:33:54 +02:00
|
|
|
.then((response) => {
|
|
|
|
|
avatarUri = response.content_uri;
|
|
|
|
|
return matrix.matrixClient.setAvatarUrl(avatarUri);
|
|
|
|
|
})
|
|
|
|
|
.then((result) => {
|
|
|
|
|
matrix.userAvatar = avatarUri;
|
|
|
|
|
resolve(result);
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
reject(err);
|
|
|
|
|
});
|
2021-01-20 09:42:13 +01:00
|
|
|
})
|
2025-03-31 16:33:54 +02:00
|
|
|
.catch((err) => {
|
|
|
|
|
reject(err);
|
2021-03-23 16:20:01 +01:00
|
|
|
});
|
2025-03-31 16:33:54 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setRoomAvatar(matrixClient, roomId, file, onUploadProgress) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
var reader = new FileReader();
|
|
|
|
|
reader.onload = (e) => {
|
|
|
|
|
const fileContents = e.target.result;
|
|
|
|
|
var data = new Uint8Array(fileContents);
|
|
|
|
|
|
|
|
|
|
const info = {
|
|
|
|
|
mimetype: file.type,
|
|
|
|
|
size: file.size,
|
|
|
|
|
};
|
2021-01-20 11:32:21 +01:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
const opts = {
|
|
|
|
|
type: file.type,
|
|
|
|
|
name: "Room Avatar",
|
|
|
|
|
progressHandler: onUploadProgress,
|
|
|
|
|
onlyContentUri: false,
|
|
|
|
|
};
|
2022-04-21 09:48:10 +02:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
var messageContent = {
|
|
|
|
|
body: file.name,
|
|
|
|
|
info: info,
|
|
|
|
|
};
|
2021-02-22 16:34:19 +01:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
matrixClient
|
|
|
|
|
.uploadContent(data, opts)
|
|
|
|
|
.then((response) => {
|
|
|
|
|
messageContent.url = response.content_uri;
|
|
|
|
|
return matrixClient.sendStateEvent(roomId, "m.room.avatar", messageContent);
|
|
|
|
|
})
|
|
|
|
|
.then((result) => {
|
2025-04-22 13:01:18 +02:00
|
|
|
resolve(matrixClient.mxcUrlToHttp(messageContent.url, 80, 80, "scale", undefined, undefined, true));
|
|
|
|
|
// resolve(result);
|
2025-03-31 16:33:54 +02:00
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
reject(err);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
reader.onerror = (err) => {
|
|
|
|
|
reject(err);
|
|
|
|
|
};
|
|
|
|
|
reader.readAsArrayBuffer(file);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loadAvatarFromFile(event, onLoad) {
|
|
|
|
|
if (event.target.files && event.target.files[0]) {
|
|
|
|
|
var reader = new FileReader();
|
|
|
|
|
reader.onload = (e) => {
|
|
|
|
|
const file = event.target.files[0];
|
|
|
|
|
if (file.type.startsWith("image/")) {
|
|
|
|
|
try {
|
|
|
|
|
var image = e.target.result;
|
|
|
|
|
|
2025-04-22 13:01:18 +02:00
|
|
|
const buffer = Uint8Array.from(window.atob(e.target.result.replace(/^data[^,]+,/, "")), (v) =>
|
|
|
|
|
v.charCodeAt(0)
|
|
|
|
|
);
|
|
|
|
|
var dimens = imageSize(buffer);
|
2025-03-31 16:33:54 +02:00
|
|
|
|
|
|
|
|
// Need to resize?
|
|
|
|
|
const w = dimens.width;
|
|
|
|
|
const h = dimens.height;
|
|
|
|
|
if (w > 640 || h > 640) {
|
|
|
|
|
var aspect = w / h;
|
|
|
|
|
var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed());
|
|
|
|
|
var newHeight = parseInt((w > h ? 640 / aspect : 640).toFixed());
|
2025-04-22 13:01:18 +02:00
|
|
|
imageResize(image, {
|
2025-03-31 16:33:54 +02:00
|
|
|
format: "png",
|
|
|
|
|
width: newWidth,
|
|
|
|
|
height: newHeight,
|
|
|
|
|
outputType: "blob",
|
2025-04-22 13:01:18 +02:00
|
|
|
})
|
2025-03-31 16:33:54 +02:00
|
|
|
.then((img) => {
|
|
|
|
|
var resizedImageFile = new File([img], file.name, {
|
|
|
|
|
type: img.type,
|
|
|
|
|
lastModified: Date.now(),
|
|
|
|
|
});
|
|
|
|
|
var reader2 = new FileReader();
|
|
|
|
|
reader2.onload = (e) => {
|
|
|
|
|
onLoad(e.target.result);
|
|
|
|
|
};
|
|
|
|
|
reader2.readAsDataURL(resizedImageFile);
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
console.error("Resize failed:", err);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
onLoad(image);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to get image dimensions: " + error);
|
|
|
|
|
}
|
2023-06-25 17:12:29 +03:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
};
|
|
|
|
|
reader.readAsDataURL(event.target.files[0]);
|
2023-06-25 17:12:29 +03:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return number of whole days between the timestamps, at end of that day.
|
|
|
|
|
* @param {*} ts1
|
|
|
|
|
* @param {*} ts2
|
|
|
|
|
*/
|
|
|
|
|
dayDiff(ts1, ts2) {
|
|
|
|
|
var t1 = dayjs(ts1).endOf("day");
|
|
|
|
|
var t2 = dayjs(ts2).endOf("day");
|
|
|
|
|
return t2.diff(t1, "day");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dayDiffToday(timestamp) {
|
|
|
|
|
var then = dayjs(timestamp).endOf("day");
|
|
|
|
|
var now = dayjs().endOf("day");
|
|
|
|
|
return now.diff(then, "day");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
formatDay(timestamp) {
|
|
|
|
|
var then = dayjs(timestamp).endOf("day");
|
|
|
|
|
return then.format("L");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
formatTime(time) {
|
|
|
|
|
const date = new Date();
|
|
|
|
|
date.setTime(time);
|
|
|
|
|
|
|
|
|
|
const today = new Date();
|
|
|
|
|
if (
|
|
|
|
|
date.getDate() == today.getDate() &&
|
|
|
|
|
date.getMonth() == today.getMonth() &&
|
|
|
|
|
date.getFullYear() == today.getFullYear()
|
|
|
|
|
) {
|
|
|
|
|
// For today, skip the date part
|
|
|
|
|
return date.toLocaleTimeString();
|
2021-02-22 16:34:19 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return date.toLocaleString();
|
|
|
|
|
}
|
2021-02-22 16:34:19 +01:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
formatRecordDuration(ms) {
|
|
|
|
|
return dayjs.duration(ms).format("HH:mm:ss");
|
|
|
|
|
}
|
2021-05-07 11:30:24 +02:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
formatDuration(ms) {
|
|
|
|
|
if (ms >= 60 * 60000) {
|
|
|
|
|
return dayjs.duration(ms).format("H:mm:ss");
|
2021-02-22 16:34:19 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return dayjs.duration(ms).format("m:ss");
|
|
|
|
|
}
|
2021-03-18 11:58:46 +01:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
formatRecordStartTime(timestamp) {
|
|
|
|
|
var then = dayjs(timestamp);
|
|
|
|
|
return then.format("lll");
|
|
|
|
|
}
|
2023-05-23 16:29:17 +02:00
|
|
|
|
2025-10-22 15:03:56 +02:00
|
|
|
parseExifDate(exifDateString) {
|
|
|
|
|
// Use a regular expression to match and capture the date/time components.
|
|
|
|
|
const regex = /(\d{4}):(\d{2}):(\d{2})\s(\d{2}):(\d{2}):(\d{2})/;
|
|
|
|
|
const match = exifDateString.match(regex);
|
|
|
|
|
|
|
|
|
|
if (match) {
|
|
|
|
|
// Extract components and convert to numbers. Note that months are 0-indexed.
|
|
|
|
|
const year = parseInt(match[1], 10);
|
|
|
|
|
const month = parseInt(match[2], 10) - 1;
|
|
|
|
|
const day = parseInt(match[3], 10);
|
|
|
|
|
const hour = parseInt(match[4], 10);
|
|
|
|
|
const minute = parseInt(match[5], 10);
|
|
|
|
|
const second = parseInt(match[6], 10);
|
|
|
|
|
return new Date(year, month, day, hour, minute, second);
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
browserCanRecordAudio() {
|
|
|
|
|
return _browserCanRecordAudio;
|
|
|
|
|
}
|
2023-08-07 15:47:52 +00:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
getRoomNameFromAlias(alias) {
|
|
|
|
|
if (alias && alias.startsWith("#") && alias.indexOf(":") > 0) {
|
|
|
|
|
return alias.slice(1).split(":")[0];
|
2023-11-06 15:28:26 +00:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getUniqueAliasForRoomName(matrixClient, roomName, defaultMatrixDomainPart, iterationCount) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
var preferredAlias = roomName.replace(/\s/g, "").toLowerCase();
|
|
|
|
|
var tryAlias = "#" + preferredAlias + ":" + defaultMatrixDomainPart;
|
|
|
|
|
matrixClient
|
|
|
|
|
.getRoomIdForAlias(tryAlias)
|
|
|
|
|
.then((ignoredid) => {
|
|
|
|
|
// We got a response, this means the tryAlias already exists.
|
|
|
|
|
// Try again, with appended random chars
|
|
|
|
|
if (iterationCount) {
|
|
|
|
|
// Not the first time around. Reached max tries?
|
|
|
|
|
if (iterationCount == 5) {
|
|
|
|
|
reject("Failed to get unique room alias");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Else, strip random chars from end so we can try again
|
|
|
|
|
roomName = roomName.substring(0, roomName.length - 5);
|
2024-07-30 18:44:46 +02:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
const randomChars = this.randomString(4, "abcdefghijklmnopqrstuvwxyz0123456789");
|
|
|
|
|
resolve(
|
|
|
|
|
this.getUniqueAliasForRoomName(
|
|
|
|
|
matrixClient,
|
|
|
|
|
roomName + "-" + randomChars,
|
|
|
|
|
defaultMatrixDomainPart,
|
|
|
|
|
(iterationCount || 0) + 1
|
|
|
|
|
)
|
|
|
|
|
);
|
2023-08-07 15:47:52 +00:00
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
2025-03-31 16:33:54 +02:00
|
|
|
if (err.errcode == "M_NOT_FOUND") {
|
|
|
|
|
resolve(preferredAlias);
|
|
|
|
|
} else {
|
|
|
|
|
reject(err);
|
|
|
|
|
}
|
2023-08-07 15:47:52 +00:00
|
|
|
});
|
2025-03-31 16:33:54 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
downloadableTypes() {
|
|
|
|
|
return ["m.video", "m.audio", "m.image", "m.file"];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
download(matrixClient, useAuthedMedia, event) {
|
|
|
|
|
console.log("DOWNLOAD");
|
|
|
|
|
this.getAttachment(matrixClient, useAuthedMedia, event)
|
|
|
|
|
.then((url) => {
|
|
|
|
|
const link = document.createElement("a");
|
|
|
|
|
link.href = url;
|
|
|
|
|
link.target = "_blank";
|
|
|
|
|
if (!this.isFileTypePDF(event)) {
|
|
|
|
|
// PDFs are shown inline, not downloaded
|
|
|
|
|
link.download = event.getContent().body || this.$t("fallbacks.download_name");
|
2023-12-01 08:20:03 +00:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
console.log("LINK", link);
|
|
|
|
|
document.body.appendChild(link);
|
|
|
|
|
link.click();
|
|
|
|
|
setTimeout(function () {
|
|
|
|
|
document.body.removeChild(link);
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
}, 200);
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
console.log("Failed to fetch attachment: ", err);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getMatrixBaseUrl(user, config) {
|
|
|
|
|
if (user) {
|
|
|
|
|
const domain = User.domainPart(user.user_id);
|
|
|
|
|
if (domain) {
|
|
|
|
|
const endpoint = config.getMatrixDomainPartMapping(domain);
|
|
|
|
|
if (endpoint) {
|
|
|
|
|
console.log("Mapped to", endpoint);
|
|
|
|
|
return Promise.resolve(endpoint);
|
2023-12-04 11:29:23 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return AutoDiscovery.findClientConfig(domain)
|
|
|
|
|
.then((clientConfig) => {
|
|
|
|
|
const hs = clientConfig["m.homeserver"];
|
|
|
|
|
if (hs && !hs.error && hs.base_url) {
|
|
|
|
|
console.log("Use home server returned from well-known", hs.base_url);
|
|
|
|
|
return hs.base_url;
|
|
|
|
|
}
|
|
|
|
|
console.log("Fallback to default server");
|
|
|
|
|
return config.defaultBaseUrl;
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
console.error("Failed well-known lookup", err);
|
|
|
|
|
return config.defaultBaseUrl;
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-12-04 11:29:23 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return Promise.resolve(config.defaultBaseUrl);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getMimeType(event) {
|
|
|
|
|
const content = event.getContent();
|
|
|
|
|
return content.info && content.info.mimetype
|
|
|
|
|
? content.info.mimetype
|
|
|
|
|
: content.file && content.file.mimetype
|
|
|
|
|
? content.file.mimetype
|
|
|
|
|
: "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getFileName(event) {
|
|
|
|
|
const content = event.getContent();
|
|
|
|
|
return (content.body || content.filename || "").toLowerCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getFileExtension(event) {
|
|
|
|
|
const fileName = this.getFileName(event);
|
|
|
|
|
const parts = fileName.split(".");
|
|
|
|
|
if (parts.length > 1) {
|
|
|
|
|
return "." + parts[parts.length - 1].toLowerCase();
|
2023-12-04 11:29:23 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return "";
|
|
|
|
|
}
|
2023-12-04 11:29:23 +01:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
getFileSize(event) {
|
|
|
|
|
const content = event.getContent();
|
|
|
|
|
if (content.info) {
|
|
|
|
|
return content.info.size;
|
2023-12-04 11:29:23 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return 0;
|
|
|
|
|
}
|
2023-12-04 11:29:23 +01:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
getFileSizeFormatted(event) {
|
2025-05-13 16:34:12 +02:00
|
|
|
const size = this.getFileSize(event);
|
|
|
|
|
const res = prettyBytes(size);
|
|
|
|
|
return res;
|
2025-03-31 16:33:54 +02:00
|
|
|
}
|
2024-01-14 13:06:37 +02:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
isFileTypeAPK(event) {
|
|
|
|
|
const mime = this.getMimeType(event);
|
|
|
|
|
if (mime === "application/vnd.android.package-archive" || this.getFileName(event).endsWith(".apk")) {
|
|
|
|
|
return true;
|
2023-12-04 11:29:23 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
2023-12-04 11:29:23 +01:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
isFileTypeIPA(event) {
|
|
|
|
|
if (this.getFileName(event).endsWith(".ipa")) {
|
|
|
|
|
return true;
|
2023-12-04 11:29:23 +01:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
2024-05-18 22:13:07 +03:00
|
|
|
|
2025-03-31 16:33:54 +02:00
|
|
|
isFileTypePDF(event) {
|
|
|
|
|
const mime = this.getMimeType(event);
|
|
|
|
|
if (mime === "application/pdf" || this.getFileName(event).endsWith(".pdf")) {
|
|
|
|
|
return true;
|
2024-05-18 22:13:07 +03:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isFileTypeZip(event) {
|
|
|
|
|
const mime = this.getMimeType(event);
|
|
|
|
|
if (
|
|
|
|
|
["application/zip", "application/x-zip-compressed", "multipart/x-zip"].includes(mime) ||
|
|
|
|
|
this.getFileName(event).endsWith(".zip")
|
|
|
|
|
) {
|
|
|
|
|
return true;
|
2024-05-18 22:13:07 +03:00
|
|
|
}
|
2025-03-31 16:33:54 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isMobileOrTabletBrowser() {
|
|
|
|
|
// Regular expression to match common mobile and tablet browser user agent strings
|
|
|
|
|
const mobileTabletPattern = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Tablet|Mobile|CriOS/i;
|
|
|
|
|
const userAgent = navigator.userAgent;
|
|
|
|
|
return mobileTabletPattern.test(userAgent);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-09 14:42:37 +02:00
|
|
|
/**
|
|
|
|
|
* Possibly convert numerals to local representation (currently only for "bo" locale)
|
|
|
|
|
* @param str String in which to convert numerals [0-9]
|
|
|
|
|
* @returns converted string
|
2025-05-12 17:15:11 +02:00
|
|
|
*/
|
2025-05-09 14:42:37 +02:00
|
|
|
toLocalNumbers = (str) => {
|
|
|
|
|
if (i18n.locale == "my") {
|
|
|
|
|
// Translate to burmese numerals
|
|
|
|
|
var result = "";
|
|
|
|
|
for (var i = 0; i < str.length; i++) {
|
|
|
|
|
var c = str.charCodeAt(i);
|
|
|
|
|
if (c >= 48 && c <= 57) {
|
|
|
|
|
result += String.fromCharCode(c + 0x1040 - 48);
|
|
|
|
|
} else {
|
|
|
|
|
result += String.fromCharCode(c);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
} else if (i18n.locale == "bo") {
|
|
|
|
|
// Translate to tibetan numerals
|
|
|
|
|
result = "";
|
|
|
|
|
for (i = 0; i < str.length; i++) {
|
|
|
|
|
c = str.charCodeAt(i);
|
|
|
|
|
if (c >= 48 && c <= 57) {
|
|
|
|
|
result += String.fromCharCode(c + 0x0f20 - 48);
|
|
|
|
|
} else {
|
|
|
|
|
result += String.fromCharCode(c);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return str;
|
|
|
|
|
};
|
2025-05-12 17:15:11 +02:00
|
|
|
|
|
|
|
|
deepToRaw(sourceObj) {
|
|
|
|
|
const objectIterator = (input) => {
|
|
|
|
|
if (Array.isArray(input)) {
|
|
|
|
|
return input.map((item) => objectIterator(item));
|
|
|
|
|
}
|
|
|
|
|
if (isRef(input) || isReactive(input) || isProxy(input)) {
|
|
|
|
|
return objectIterator(toRaw(input));
|
|
|
|
|
}
|
|
|
|
|
if (input && typeof input === "object") {
|
|
|
|
|
return Object.keys(input).reduce((acc, key) => {
|
|
|
|
|
acc[key] = objectIterator(input[key]);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {});
|
|
|
|
|
}
|
|
|
|
|
return input;
|
|
|
|
|
};
|
|
|
|
|
return objectIterator(sourceObj);
|
|
|
|
|
}
|
2025-06-09 09:44:37 +02:00
|
|
|
}
|
2020-11-19 17:08:58 +01:00
|
|
|
export default new Util();
|