import axios from "axios"; import * as ContentHelpers from "matrix-js-sdk/lib/content-helpers"; import imageResize from "image-resize"; import { AutoDiscovery } from "matrix-js-sdk"; import User from "../models/user"; import prettyBytes from "pretty-bytes"; import Hammer from "hammerjs"; import { Thread } from "matrix-js-sdk/lib/models/thread"; import { imageSize } from "image-size"; import dayjs from "dayjs"; import aesjs from "aes-js"; import localizedFormat from "dayjs/plugin/localizedFormat"; import duration from "dayjs/plugin/duration"; import i18n from "./lang"; import { toRaw, isRef, isReactive, isProxy, } from 'vue'; export const STATE_EVENT_ROOM_DELETION_NOTICE = "im.keanu.room_deletion_notice"; export const STATE_EVENT_ROOM_DELETED = "im.keanu.room_deleted"; export const ROOM_TYPE_DEFAULT = "im.keanu.room_type_default"; export const ROOM_TYPE_VOICE_MODE = "im.keanu.room_type_voice"; export const ROOM_TYPE_FILE_MODE = "im.keanu.room_type_file"; export const ROOM_TYPE_CHANNEL = "im.keanu.room_type_channel"; export const STATE_EVENT_ROOM_TYPE = "im.keanu.room_type"; // Install extended localized format dayjs.extend(localizedFormat); dayjs.extend(duration); // Store info about getUserMedia BEFORE we aply polyfill(s)! var _browserCanRecordAudioF = function () { var legacyGetUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; return ( legacyGetUserMedia !== undefined || (navigator.mediaDevices && navigator.mediaDevices.getUserMedia !== undefined) ); }; var _browserCanRecordAudio = _browserCanRecordAudioF(); class UploadPromise { aborted = false; onAbort = undefined; constructor(wrappedPromise) { this.wrappedPromise = wrappedPromise; } abort() { this.aborted = true; if (this.onAbort) { this.onAbort(); } } then(resolve, reject) { this.wrappedPromise = this.wrappedPromise.then(resolve, reject); return this; } catch(handler) { this.wrappedPromise = this.wrappedPromise.catch(handler); return this; } } class Util { threadMessageType() { return Thread.hasServerSideSupport ? "m.thread" : "io.element.thread"; } getAttachmentUrlAndDuration(event) { return new Promise((resolve, reject) => { const content = event.getContent(); if (content.url != null) { resolve([content.url, content.info.duration]); return; } if (content.file && content.file.url) { resolve([content.file.url, content.info.duration]); } else { reject("No url found!"); } }); } getAttachment(matrixClient, useAuthedMedia, event, progressCallback, asBlob = false, abortController = undefined) { return new Promise((resolve, reject) => { const content = event.getContent(); var url = null; var file = null; let decrypt = true; if (content.url != null) { url = matrixClient.mxcUrlToHttp( content.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia ); decrypt = false; } else if (content.file && content.file.url) { file = content.file; url = matrixClient.mxcUrlToHttp( file.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia ); } if (url == null) { reject("No url found!"); } 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) => { return decrypt ? this.decryptIfNeeded(file, response) : Promise.resolve({ buffer: response.data }); }) .then((bytes) => { if (asBlob) { resolve(new Blob([bytes.buffer], { type: file.mimetype })); } else { resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: file.mimetype }))); } }) .catch((err) => { console.log("Download error: ", err); reject(err); }) .finally(() => { if (progressCallback) { progressCallback(null); } }); }); } getThumbnail(matrixClient, useAuthedMedia, event, config, ignoredw, ignoredh) { return new Promise((resolve, reject) => { const content = event.getContent(); var url = null; var file = null; let decrypt = true; if (content.url != null) { url = matrixClient.mxcUrlToHttp( content.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia ); decrypt = false; } else if (content && content.info && content.info.thumbnail_file && content.info.thumbnail_file.url) { file = content.info.thumbnail_file; // var width = w; // var height = h; // if (content.info.w < w || content.info.h < h) { // width = content.info.w; // height = content.info.h; // } // url = matrixClient.mxcUrlToHttp( // file.url, // width, height, // "scale", // true // ); url = matrixClient.mxcUrlToHttp( file.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia ); } 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 ); } if (url == null) { reject("No url found!"); return; } axios .get(url, { responseType: "arraybuffer", headers: { Authorization: `Bearer ${matrixClient.getAccessToken()}`, }, }) .then((response) => { return decrypt ? this.decryptIfNeeded(file, response) : Promise.resolve({ buffer: response.data }); }) .then((bytes) => { resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: file.mimetype }))); }) .catch((err) => { console.log("Download error: ", err); reject(err); }); }); } b64toBuffer(val) { const baseValue = val.replaceAll("-", "+").replaceAll("_", "/"); return Buffer.from(baseValue, "base64"); } decryptIfNeeded(file, response) { return new Promise((resolve, reject) => { const key = this.b64toBuffer(file.key.k); const iv = this.b64toBuffer(file.iv); const originalHash = this.b64toBuffer(file.hashes.sha256); var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv)); const data = new Uint8Array(response.data); 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"); }); }); } sendTextMessage(matrixClient, roomId, text, editedEvent, replyToEvent) { var content = ContentHelpers.makeTextMessage(text); 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; } return this.sendMessage(matrixClient, roomId, "m.room.message", content); } 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); } sendMessage(matrixClient, roomId, eventType, content) { return new Promise((resolve, reject) => { matrixClient .sendEvent(roomId, eventType, content, undefined, undefined) .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)); } } } 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); }) .catch((err) => { // Still error, abort reject(err.toLocaleString()); }); }); } else { reject(err.toLocaleString()); } }); }); } sendFile(matrixClient, roomId, file, onUploadProgress, threadRoot) { const uploadPromise = new UploadPromise(undefined); uploadPromise.wrappedPromise = new Promise((resolve, reject) => { var reader = new FileReader(); reader.onload = (e) => { if (uploadPromise.aborted) { reject("Aborted"); return; } const fileContents = e.target.result; var data = new Uint8Array(fileContents); const info = { mimetype: file.type, size: file.size, }; // If audio, send duration in ms as well if (file.duration) { info.duration = file.duration; } var description = file.name; var msgtype = "m.file"; if (file.type.startsWith("image/")) { msgtype = "m.image"; } else if (file.type.startsWith("audio/")) { msgtype = "m.audio"; } else if (file.type.startsWith("video/")) { msgtype = "m.video"; } const opts = { type: file.type, name: description, progressHandler: onUploadProgress, onlyContentUri: false, }; var messageContent = { body: description, info: info, msgtype: msgtype, }; // If thread root (an eventId) is set, add that here if (threadRoot) { messageContent["m.relates_to"] = { rel_type: this.threadMessageType(), event_id: threadRoot, }; } // Set filename for files if (msgtype == "m.file") { messageContent.filename = file.name; } if (!matrixClient.isRoomEncrypted(roomId)) { // Not encrypted. const promise = matrixClient.uploadContent(data, opts); uploadPromise.onAbort = () => { matrixClient.cancelUpload(promise); }; promise .then((response) => { messageContent.url = response.content_uri; return msgtype == "m.audio" ? this.generateWaveform(fileContents, messageContent) : true; }) .then(() => { return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent); }) .then((result) => { resolve(result); }) .catch((err) => { reject(err); }); return; // Don't fall through } 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 var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv)); var encryptedBytes = aesCtr.encrypt(data); data = encryptedBytes; // Calculate sha256 var hash = new Uint8Array(sha256.create().update(data).arrayBuffer()); const jwk = { kty: "oct", key_ops: ["encrypt", "decrypt"], alg: "A256CTR", k: key.toString("base64").replaceAll(/\//g, "_").replaceAll(/\+/g, "-"), ext: true, }; const encryptedFile = { mimetype: file.type, key: jwk, iv: Buffer.from(iv).toString("base64").replace(/=/g, ""), hashes: { sha256: Buffer.from(hash).toString("base64").replace(/=/g, "") }, v: "v2", }; messageContent.file = encryptedFile; // Encrypted data sent as octet-stream! opts.type = "application/octet-stream"; const promise = matrixClient.uploadContent(data, opts); uploadPromise.onAbort = () => { matrixClient.cancelUpload(promise); }; promise .then((response) => { if (response.error) { return reject(response.error); } encryptedFile.url = response.content_uri; return msgtype == "m.audio" ? this.generateWaveform(fileContents, messageContent) : true; }) .then(() => { return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent); }) .then((result) => { resolve(result); }) .catch((err) => { reject(err); }); }; reader.onerror = (err) => { reject(err); }; reader.readAsArrayBuffer(file); }); return uploadPromise; } generateWaveform(data, messageContent) { if (!(window.AudioContext || window.webkitAudioContext)) { return; // No support } const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); if (audioCtx) { return audioCtx.decodeAudioData(data).then((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 } // 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 += ``; svg += ""; messageContent.format = "org.matrix.custom.html"; messageContent.formatted_body = svg; }); } } /** * 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"]) { console.error("We have a tag!"); 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; } } } 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; } 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-"; } 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)); } return result; } sanitizeRoomId(roomId) { if (roomId && roomId.match(/^(!|#).+$/)) { return roomId; } return null; } sanitizeUserId(userId) { if (userId && userId.match(/^([0-9a-z-.=_/]+|@[0-9a-z-.=_/]+:.+)$/)) { return userId; } return null; } invalidUserIdChars() { return /[^0-9a-z-.=_/]+/g; } getFirstVisibleElement(parentNode, where) { let visible = this.findVisibleElements(parentNode); if (visible) { visible = visible.filter(where); } if (visible && visible.length > 0) { return visible[0]; } return null; } getLastVisibleElement(parentNode, where) { let visible = this.findVisibleElements(parentNode); if (visible) { visible = visible.filter(where); } if (visible && visible.length > 0) { return visible[visible.length - 1]; } 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; } 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; } } // 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 = []; const modules = import.meta.glob("@/assets/avatars/*.{jpeg,jpg,png}", { query: "?url", import: "default", eager: true, }); Object.keys(modules).map((path) => { var name = path.split("_")[1]; name = name.slice(0, name.indexOf(".")); name = name.charAt(0).toUpperCase() + name.slice(1); const image = modules[path]; const randomNumber = parseInt(this.randomString(4, "0123456789")).toFixed(); const title = "Guest " + name + " " + randomNumber; images.push({ id: path, image: image, name: title, title: title }); }); return images; } setAvatar(matrix, file, onUploadProgress) { 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, }; var avatarUri; matrix.matrixClient .uploadContent(response.data, opts) .then((response) => { avatarUri = response.content_uri; return matrix.matrixClient.setAvatarUrl(avatarUri); }) .then((result) => { matrix.userAvatar = avatarUri; resolve(result); }) .catch((err) => { reject(err); }); }) .catch((err) => { reject(err); }); }); } 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, }; const opts = { type: file.type, name: "Room Avatar", progressHandler: onUploadProgress, onlyContentUri: false, }; var messageContent = { body: file.name, info: info, }; matrixClient .uploadContent(data, opts) .then((response) => { messageContent.url = response.content_uri; return matrixClient.sendStateEvent(roomId, "m.room.avatar", messageContent); }) .then((result) => { resolve(matrixClient.mxcUrlToHttp(messageContent.url, 80, 80, "scale", undefined, undefined, true)); // resolve(result); }) .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; const buffer = Uint8Array.from(window.atob(e.target.result.replace(/^data[^,]+,/, "")), (v) => v.charCodeAt(0) ); var dimens = imageSize(buffer); // 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()); imageResize(image, { format: "png", width: newWidth, height: newHeight, outputType: "blob", }) .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); } } }; reader.readAsDataURL(event.target.files[0]); } } /** * 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(); } return date.toLocaleString(); } formatRecordDuration(ms) { return dayjs.duration(ms).format("HH:mm:ss"); } formatDuration(ms) { if (ms >= 60 * 60000) { return dayjs.duration(ms).format("H:mm:ss"); } return dayjs.duration(ms).format("m:ss"); } formatRecordStartTime(timestamp) { var then = dayjs(timestamp); return then.format("lll"); } browserCanRecordAudio() { return _browserCanRecordAudio; } getRoomNameFromAlias(alias) { if (alias && alias.startsWith("#") && alias.indexOf(":") > 0) { return alias.slice(1).split(":")[0]; } 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); } const randomChars = this.randomString(4, "abcdefghijklmnopqrstuvwxyz0123456789"); resolve( this.getUniqueAliasForRoomName( matrixClient, roomName + "-" + randomChars, defaultMatrixDomainPart, (iterationCount || 0) + 1 ) ); }) .catch((err) => { if (err.errcode == "M_NOT_FOUND") { resolve(preferredAlias); } else { reject(err); } }); }); } 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"); } 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); } 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; }); } } 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(); } return ""; } getFileSize(event) { const content = event.getContent(); if (content.info) { return content.info.size; } return 0; } getFileSizeFormatted(event) { return prettyBytes(this.getFileSize(event)); } isFileTypeAPK(event) { const mime = this.getMimeType(event); if (mime === "application/vnd.android.package-archive" || this.getFileName(event).endsWith(".apk")) { return true; } return false; } isFileTypeIPA(event) { if (this.getFileName(event).endsWith(".ipa")) { return true; } return false; } isFileTypePDF(event) { const mime = this.getMimeType(event); if (mime === "application/pdf" || this.getFileName(event).endsWith(".pdf")) { return true; } 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; } 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); } singleOrDoubleTabRecognizer(element) { // reference: https://codepen.io/jtangelder/pen/xxYyJQ const hm = new Hammer.Manager(element); hm.add(new Hammer.Tap({ event: "doubletap", taps: 2 })); hm.add(new Hammer.Tap({ event: "singletap" })); hm.get("doubletap").recognizeWith("singletap"); hm.get("singletap").requireFailure("doubletap"); return hm; } /** * Possibly convert numerals to local representation (currently only for "bo" locale) * @param str String in which to convert numerals [0-9] * @returns converted string */ 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; }; 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); } }; export default new Util();