keanu-weblite/src/plugins/utils.js

1100 lines
40 KiB
JavaScript
Raw Normal View History

2020-11-19 17:08:58 +01:00
import axios from 'axios';
2020-11-21 14:57:43 +01:00
import * as ContentHelpers from "matrix-js-sdk/lib/content-helpers";
import dataUriToBuffer from "data-uri-to-buffer";
import ImageResize from "image-resize";
2023-12-01 08:20:03 +00:00
import { AutoDiscovery } from 'matrix-js-sdk';
import User from '../models/user';
2023-12-04 11:29:23 +01:00
const prettyBytes = require("pretty-bytes");
import Hammer from "hammerjs";
import { Thread } from 'matrix-js-sdk/lib/models/thread';
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";
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-08-07 14:13:35 +00:00
export const STATE_EVENT_ROOM_TYPE = "im.keanu.room_type";
const sizeOf = require("image-size");
2021-01-20 11:32:21 +01:00
var dayjs = require('dayjs');
2020-11-21 14:57:43 +01:00
var sha256 = require('js-sha256').sha256;
var aesjs = require('aes-js');
var base64Url = require('json-web-key/lib/base64url');
2020-11-19 17:08:58 +01:00
2021-01-20 11:32:21 +01:00
// Install extended localized format
var localizedFormat = require('dayjs/plugin/localizedFormat')
dayjs.extend(localizedFormat)
2021-02-22 16:34:19 +01:00
var duration = require('dayjs/plugin/duration')
dayjs.extend(duration);
2021-01-20 11:32:21 +01:00
// Store info about getUserMedia BEFORE we aply polyfill(s)!
2021-03-23 16:20:01 +01:00
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();
2023-01-09 21:12:17 +01:00
}
}
2023-01-09 21:12:17 +01:00
then(resolve, reject) {
this.wrappedPromise = this.wrappedPromise.then(resolve, reject);
return this;
}
2023-01-09 21:12:17 +01:00
catch(handler) {
this.wrappedPromise = this.wrappedPromise.catch(handler);
return this;
2023-01-09 21:12:17 +01:00
}
}
2020-11-19 17:08:58 +01:00
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!");
}
});
}
2023-06-25 17:12:29 +03:00
getAttachment(matrixClient, event, progressCallback, asBlob = false, abortController = undefined) {
return new Promise((resolve, reject) => {
const content = event.getContent();
if (content.url != null) {
// Unencrypted, just return!
resolve(matrixClient.mxcUrlToHttp(content.url));
return;
}
var url = null;
var file = null;
if (content.file && content.file.url) {
file = content.file;
url = matrixClient.mxcUrlToHttp(file.url);
}
if (url == null) {
reject("No url found!");
}
2021-03-23 16:20:01 +01:00
axios.get(url, {
signal: abortController ? abortController.signal : undefined,
2021-03-23 16:20:01 +01:00
responseType: 'arraybuffer', onDownloadProgress: progressEvent => {
let percentCompleted = Math.floor((progressEvent.loaded * 100) / progressEvent.total);
if (progressCallback) {
progressCallback(percentCompleted);
}
2021-03-23 16:20:01 +01:00
}
})
.then(response => {
return this.decryptIfNeeded(file, response);
})
.then(bytes => {
2022-05-23 15:19:55 +00:00
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);
}
2021-03-23 16:20:01 +01:00
});
});
}
getThumbnail(matrixClient, event, config, ignoredw, ignoredh) {
2020-11-19 17:08:58 +01:00
return new Promise((resolve, reject) => {
const content = event.getContent();
2020-11-21 14:57:43 +01:00
if (content.url != null) {
// Unencrypted, just return!
resolve(matrixClient.mxcUrlToHttp(content.url));
return;
}
2020-11-19 17:08:58 +01:00
var url = null;
var file = null;
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);
} else if (content.file && content.file.url && this.getFileSize(event) > 0 && this.getFileSize(event) < config.maxSizeAutoDownloads) {
2020-11-19 17:08:58 +01:00
// No thumb, use real url
file = content.file;
url = matrixClient.mxcUrlToHttp(file.url);
}
if (url == null) {
reject("No url found!");
return;
2020-11-19 17:08:58 +01:00
}
axios.get(url, { responseType: 'arraybuffer' })
.then(response => {
return this.decryptIfNeeded(file, response);
2020-11-19 17:08:58 +01:00
})
.then(bytes => {
2020-11-19 22:48:08 +01:00
resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: file.mimetype })));
2020-11-19 17:08:58 +01:00
})
.catch(err => {
console.log("Download error: ", err);
reject(err);
});
});
}
2020-11-21 14:57:43 +01:00
decryptIfNeeded(file, response) {
return new Promise((resolve, reject) => {
var key = base64Url.decode(file.key.k);
var iv = base64Url.decode(file.iv);
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
const data = new Uint8Array(response.data);
// Calculate sha256 and compare hashes
var hash = new Uint8Array(sha256.create().update(data).arrayBuffer());
const originalHash = base64Url.decode(file.hashes.sha256);
if (Buffer.compare(Buffer.from(hash), Buffer.from(originalHash.buffer)) != 0) {
reject("Hashes don't match!");
return;
}
var decryptedBytes = aesCtr.decrypt(data);
resolve(decryptedBytes);
});
}
2020-12-15 17:06:26 +01:00
sendTextMessage(matrixClient, roomId, text, editedEvent, replyToEvent) {
2020-12-14 16:30:27 +01:00
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
}
2020-12-15 17:06:26 +01:00
} else if (replyToEvent) {
content['m.relates_to'] = {
'm.in_reply_to': {
event_id: replyToEvent.getId()
}
}
let senderContent = replyToEvent.getContent()
2020-12-15 17:06:26 +01:00
2023-01-09 21:12:17 +01:00
const senderContentBody = Object.getOwnPropertyDescriptor(senderContent, 'body') ? senderContent.body : Object.values(senderContent)[0].question.body
2020-12-15 17:06:26 +01:00
// Prefix the content with reply info (seems to be a legacy thing)
const prefix = senderContentBody.split('\n').map((item, index) => {
2020-12-15 17:06:26 +01:00
return "> " + (index == 0 ? ("<" + replyToEvent.getSender() + "> ") : "") + item;
}).join('\n');
content.body = prefix + "\n\n" + content.body;
2020-12-14 16:30:27 +01:00
}
return this.sendMessage(matrixClient, roomId, "m.room.message", content);
2020-11-21 14:57:43 +01:00
}
sendQuickReaction(matrixClient, roomId, emoji, event, extraData = {}) {
2020-11-25 14:42:50 +01:00
const content = {
'm.relates_to': Object.assign(extraData, {
2020-11-25 14:42:50 +01:00
key: emoji,
rel_type: 'm.annotation',
event_id: event.getId()
})
};
2020-11-25 14:42:50 +01:00
return this.sendMessage(matrixClient, roomId, "m.reaction", content);
}
createPoll(matrixClient, roomId, question, answers, isDisclosed) {
var idx = 0;
let answerData = answers.map(a => {
idx++;
2023-01-09 21:12:17 +01:00
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);
}
2020-11-25 14:42:50 +01:00
sendMessage(matrixClient, roomId, eventType, content) {
2020-11-21 14:57:43 +01:00
return new Promise((resolve, reject) => {
2020-11-25 14:42:50 +01:00
matrixClient.sendEvent(roomId, eventType, content, undefined, undefined)
2020-11-21 14:57:43 +01:00
.then((result) => {
console.log("Message sent: ", result);
2023-06-28 12:14:44 +00:00
resolve(result.event_id);
2020-11-21 14:57:43 +01:00
})
.catch(err => {
console.log("Send error: ", err);
2020-11-21 14:57:43 +01:00
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];
2020-11-21 14:57:43 +01:00
if (!deviceInfo.known) {
setAsKnownPromises.push(
matrixClient.setDeviceKnown(
user,
deviceId,
true
)
);
}
}
}
Promise.all(setAsKnownPromises)
2020-11-21 14:57:43 +01:00
.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
});
2023-06-25 17:12:29 +03:00
}
}
matrixClient.resendEvent(event, matrixClient.getRoom(event.getRoomId()))
2020-11-21 14:57:43 +01:00
.then((result) => {
console.log("Message sent: ", result);
2023-06-28 12:14:44 +00:00
resolve(result.event_id);
2020-11-21 14:57:43 +01:00
})
.catch((err) => {
// Still error, abort
reject(err.toLocaleString());
});
});
}
else {
reject(err.toLocaleString());
}
});
});
}
2023-12-04 11:29:23 +01:00
sendFile(matrixClient, roomId, file, onUploadProgress, threadRoot) {
const uploadPromise = new UploadPromise(undefined);
uploadPromise.wrappedPromise = new Promise((resolve, reject) => {
2020-11-21 14:57:43 +01:00
var reader = new FileReader();
reader.onload = (e) => {
if (uploadPromise.aborted) {
2023-01-09 21:12:17 +01:00
reject("Aborted");
return;
}
2020-11-21 14:57:43 +01:00
const fileContents = e.target.result;
2020-11-25 14:42:50 +01:00
var data = new Uint8Array(fileContents);
const info = {
mimetype: file.type,
size: file.size
};
2023-06-25 17:12:29 +03:00
// If audio, send duration in ms as well
if (file.duration) {
info.duration = file.duration;
}
var description = file.name;
2023-12-04 11:29:23 +01:00
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';
}
2020-11-25 14:42:50 +01:00
const opts = {
type: file.type,
name: description,
2020-11-25 14:42:50 +01:00
progressHandler: onUploadProgress,
onlyContentUri: false
};
var messageContent = {
body: description,
info: info,
msgtype: msgtype
}
2023-06-28 12:14:44 +00:00
// If thread root (an eventId) is set, add that here
if (threadRoot) {
messageContent["m.relates_to"] = {
"rel_type": this.threadMessageType(),
2023-06-28 12:14:44 +00:00
"event_id": threadRoot
};
}
// Set filename for files
if (msgtype == 'm.file') {
messageContent.filename = file.name;
}
2020-11-25 14:42:50 +01:00
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);
});
2020-11-25 14:42:50 +01:00
return; // Don't fall through
}
2020-11-21 14:57:43 +01:00
const crypto = require('crypto');
let key = crypto.randomBytes(256 / 8);
let iv = Buffer.concat([crypto.randomBytes(8), Buffer.alloc(8)]); // Initialization vector.
2020-11-21 14:57:43 +01:00
// Encrypt
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
var encryptedBytes = aesCtr.encrypt(data);
2020-11-25 14:42:50 +01:00
data = encryptedBytes;
2020-11-21 14:57:43 +01:00
// Calculate sha256
2020-11-25 14:42:50 +01:00
var hash = new Uint8Array(sha256.create().update(data).arrayBuffer());
2020-11-21 14:57:43 +01:00
const jwk = {
kty: 'oct',
2021-03-10 13:40:32 +01:00
key_ops: ['encrypt', 'decrypt'],
2020-11-21 14:57:43 +01:00
alg: 'A256CTR',
k: base64Url.encode(key),
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, '') },
2020-11-21 14:57:43 +01:00
v: 'v2'
};
messageContent.file = encryptedFile;
2020-11-21 14:57:43 +01:00
// 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);
}
2020-11-25 14:42:50 +01:00
encryptedFile.url = response.content_uri;
return (msgtype == 'm.audio' ? this.generateWaveform(fileContents, messageContent) : true);
})
.then(() => {
2020-11-25 14:42:50 +01:00
return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent)
2020-11-21 14:57:43 +01:00
})
.then(result => {
resolve(result);
})
.catch(err => {
reject(err);
});
}
reader.onerror = (err) => {
reject(err);
}
reader.readAsArrayBuffer(file);
});
return uploadPromise;
2020-11-21 14:57:43 +01:00
}
generateWaveform(data, messageContent) {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx) {
return audioCtx.decodeAudioData(data)
.then((audioBuffer) => {
const rawData = audioBuffer.getChannelData(0); // We only need to work with one channel of data
const samples = 1000; // Number of samples we want to have in our final data set
const blockSize = Math.floor(rawData.length / samples); // the number of samples in each subdivision
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 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;
})
}
}
/**
2023-06-28 12:14:44 +00:00
* Return what "mode" to use for the given room.
*
2023-08-07 14:13:35 +00:00
* 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...
*/
2023-08-07 14:13:35 +00:00
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!");
2023-06-28 12:14:44 +00:00
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;
}
}
}
2023-08-07 14:13:35 +00:00
return null;
2023-06-28 12:14:44 +00:00
}
/**
* Return the room type for the current room
* @param {*} roomOrNull
2023-06-28 12:14:44 +00:00
*/
2023-08-07 14:13:35 +00:00
roomDisplayTypeToQueryParam(roomOrNull, roomDisplayType) {
const roomType = this.roomDisplayTypeOverride(roomOrNull) || roomDisplayType;
2023-06-28 12:14:44 +00:00
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 */
2022-07-01 08:55:26 +00:00
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;
2021-03-23 16:20:01 +01:00
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]);
2021-03-23 16:20:01 +01:00
i += 1;
}
return nodes;
}
return null; // No visible found
}
isChildVisible(parentNode, childNode) {
2021-03-04 13:11:17 +01:00
const rect1 = parentNode.getBoundingClientRect();
const rect2 = childNode.getBoundingClientRect();
2023-03-29 08:19:42 +00:00
var overlap = !(rect1.right <= rect2.left ||
rect1.left >= rect2.right ||
rect1.bottom <= rect2.top ||
rect1.top >= rect2.bottom)
2021-03-04 13:11:17 +01:00
return overlap;
}
findOneVisibleElement(parentNode) {
let start = 0;
2023-06-28 12:14:44 +00:00
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;
2023-03-29 08:19:42 +00:00
} 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;
}
2021-01-20 09:42:13 +01:00
_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);
2021-01-20 09:42:13 +01:00
const image = r(res);
const randomNumber = parseInt(this.randomString(4, "0123456789")).toFixed();
images.push({ id: res, image: image, name: "Guest " + name + " " + randomNumber});
2021-01-20 09:42:13 +01:00
});
return images;
}
getDefaultAvatars() {
return this._importAll(require.context('../assets/avatars/', true, /\.(jpeg|jpg|png)$/));
}
setAvatar(matrix, file, onUploadProgress) {
2021-01-20 09:42:13 +01: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
};
var avatarUri;
matrix.matrixClient.uploadContent(response.data, opts)
2021-01-20 09:42:13 +01:00
.then((response) => {
avatarUri = response.content_uri;
return matrix.matrixClient.setAvatarUrl(avatarUri);
2021-01-20 09:42:13 +01:00
})
.then(result => {
matrix.userAvatar = avatarUri;
2021-01-20 09:42:13 +01:00
resolve(result);
})
.catch(err => {
reject(err);
});
})
.catch(err => {
reject(err);
});
})
}
2021-01-20 11:32:21 +01:00
2021-03-23 16:20:01 +01: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
};
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(result);
})
.catch(err => {
reject(err);
});
}
reader.onerror = (err) => {
reject(err);
}
reader.readAsArrayBuffer(file);
});
}
loadAvatarFromFile(event, onLoad) {
if (event.target.files && event.target.files[0]) {
2023-01-09 21:12:17 +01:00
var reader = new FileReader();
reader.onload = (e) => {
const file = event.target.files[0];
if (file.type.startsWith("image/")) {
try {
var image = e.target.result;
var dimens = sizeOf(dataUriToBuffer(e.target.result));
// 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()
);
var imageResize = new ImageResize({
format: "png",
width: newWidth,
height: newHeight,
outputType: "blob",
});
imageResize
.play(event.target)
.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-01-09 21:12:17 +01:00
};
reader.readAsDataURL(event.target.files[0]);
}
2023-01-09 21:12:17 +01:00
}
2021-01-20 11:32:21 +01:00
/**
* Return number of whole days between the timestamps, at end of that day.
* @param {*} ts1
* @param {*} ts2
2021-01-20 11:32:21 +01:00
*/
dayDiff(ts1, ts2) {
var t1 = dayjs(ts1).endOf('day');
var t2 = dayjs(ts2).endOf('day');
return t2.diff(t1, 'day');
}
dayDiffToday(timestamp) {
2021-01-20 11:32:21 +01:00
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');
2021-01-20 11:32:21 +01:00
}
2021-02-22 16:34:19 +01:00
2023-06-25 17:12:29 +03:00
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();
}
2021-02-22 16:34:19 +01:00
formatRecordDuration(ms) {
return dayjs.duration(ms).format("HH:mm:ss");
}
2021-05-07 11:30:24 +02:00
formatDuration(ms) {
if (ms >= (60 * 60000)) {
return dayjs.duration(ms).format("H:mm:ss");
}
return dayjs.duration(ms).format("m:ss");
}
2021-02-22 16:34:19 +01:00
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;
}
2023-12-01 08:20:03 +00:00
getUniqueAliasForRoomName(matrixClient, roomName, defaultMatrixDomainPart, iterationCount) {
return new Promise((resolve, reject) => {
var preferredAlias = roomName.replace(/\s/g, "").toLowerCase();
2023-12-01 08:20:03 +00:00
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");
2023-12-01 08:20:03 +00:00
resolve(this.getUniqueAliasForRoomName(matrixClient, roomName + "-" + randomChars, defaultMatrixDomainPart, (iterationCount || 0) + 1))
})
.catch(err => {
if (err.errcode == 'M_NOT_FOUND') {
resolve(preferredAlias);
} else {
reject(err);
}
})
});
}
2023-08-07 15:47:52 +00:00
2023-11-06 15:28:26 +00:00
downloadableTypes() {
return ['m.video','m.audio','m.image','m.file'];
2023-11-06 15:28:26 +00:00
}
2023-08-07 15:47:52 +00:00
download(matrixClient, event) {
2024-10-17 11:33:46 +02:00
console.log("DOWNLOAD");
2023-08-07 15:47:52 +00:00
this
.getAttachment(matrixClient, event)
.then((url) => {
const link = document.createElement("a");
link.href = url;
link.target = "_blank";
2024-07-30 18:44:46 +02:00
if (!this.isFileTypePDF(event)) {
// PDFs are shown inline, not downloaded
link.download = event.getContent().body || this.$t("fallbacks.download_name");
}
2024-10-17 11:33:46 +02:00
console.log("LINK", link);
2023-08-07 15:47:52 +00:00
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);
});
}
2023-12-01 08:20:03 +00:00
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);
}
2024-01-14 13:06:37 +02:00
2023-12-04 11:29:23 +01:00
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;
}
2024-01-14 13:06:37 +02:00
2023-12-04 11:29:23 +01:00
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
}
2020-11-19 17:08:58 +01:00
}
export default new Util();