Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/querystring
Conflicts: package.json src/@types/global.d.ts src/components/views/elements/AppTile.js src/utils/HostingLink.js yarn.lock
This commit is contained in:
commit
5dbd79c729
1950 changed files with 174795 additions and 76807 deletions
32
src/utils/AnimationUtils.ts
Normal file
32
src/utils/AnimationUtils.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { clamp } from "lodash";
|
||||
|
||||
/**
|
||||
* This method linearly interpolates between two points (start, end). This is
|
||||
* most commonly used to find a point some fraction of the way along a line
|
||||
* between two endpoints (e.g. to move an object gradually between those
|
||||
* points).
|
||||
* @param {number} start the starting point
|
||||
* @param {number} end the ending point
|
||||
* @param {number} amt the interpolant
|
||||
* @returns
|
||||
*/
|
||||
export function lerp(start: number, end: number, amt: number) {
|
||||
amt = clamp(amt, 0, 1);
|
||||
return (1 - amt) * start + amt * end;
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,13 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {AutoDiscovery} from "matrix-js-sdk";
|
||||
import {_t, _td, newTranslatableError} from "../languageHandler";
|
||||
import {makeType} from "./TypeUtils";
|
||||
import React, { ReactNode } from 'react';
|
||||
import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery";
|
||||
import { _t, _td, newTranslatableError } from "../languageHandler";
|
||||
import { makeType } from "./TypeUtils";
|
||||
import SdkConfig from '../SdkConfig';
|
||||
|
||||
const LIVELINESS_DISCOVERY_ERRORS = [
|
||||
const LIVELINESS_DISCOVERY_ERRORS: string[] = [
|
||||
AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
];
|
||||
|
@ -34,21 +33,29 @@ export class ValidatedServerConfig {
|
|||
isUrl: string;
|
||||
|
||||
isDefault: boolean;
|
||||
// when the server config is based on static URLs the hsName is not resolvable and things may wish to use hsUrl
|
||||
isNameResolvable: boolean;
|
||||
|
||||
warning: string;
|
||||
}
|
||||
|
||||
export interface IAuthComponentState {
|
||||
serverIsAlive: boolean;
|
||||
serverErrorIsFatal: boolean;
|
||||
serverDeadError?: ReactNode;
|
||||
}
|
||||
|
||||
export default class AutoDiscoveryUtils {
|
||||
/**
|
||||
* Checks if a given error or error message is considered an error
|
||||
* relating to the liveliness of the server. Must be an error returned
|
||||
* from this AutoDiscoveryUtils class.
|
||||
* @param {string|Error} error The error to check
|
||||
* @param {string | Error} error The error to check
|
||||
* @returns {boolean} True if the error is a liveliness error.
|
||||
*/
|
||||
static isLivelinessError(error: string|Error): boolean {
|
||||
static isLivelinessError(error: string | Error): boolean {
|
||||
if (!error) return false;
|
||||
return !!LIVELINESS_DISCOVERY_ERRORS.find(e => e === error || e === error.message);
|
||||
return !!LIVELINESS_DISCOVERY_ERRORS.find(e => typeof error === "string" ? e === error : e === error.message);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,7 +66,7 @@ export default class AutoDiscoveryUtils {
|
|||
* implementation for known values.
|
||||
* @returns {*} The state for the component, given the error.
|
||||
*/
|
||||
static authComponentStateForError(err: string | Error | null, pageName = "login"): Object {
|
||||
static authComponentStateForError(err: string | Error | null, pageName = "login"): IAuthComponentState {
|
||||
if (!err) {
|
||||
return {
|
||||
serverIsAlive: true,
|
||||
|
@ -68,15 +75,19 @@ export default class AutoDiscoveryUtils {
|
|||
};
|
||||
}
|
||||
let title = _t("Cannot reach homeserver");
|
||||
let body = _t("Ensure you have a stable internet connection, or get in touch with the server admin");
|
||||
let body: ReactNode = _t("Ensure you have a stable internet connection, or get in touch with the server admin");
|
||||
if (!AutoDiscoveryUtils.isLivelinessError(err)) {
|
||||
title = _t("Your Riot is misconfigured");
|
||||
const brand = SdkConfig.get().brand;
|
||||
title = _t("Your %(brand)s is misconfigured", { brand });
|
||||
body = _t(
|
||||
"Ask your Riot admin to check <a>your config</a> for incorrect or duplicate entries.",
|
||||
{}, {
|
||||
"Ask your %(brand)s admin to check <a>your config</a> for incorrect or duplicate entries.",
|
||||
{
|
||||
brand,
|
||||
},
|
||||
{
|
||||
a: (sub) => {
|
||||
return <a
|
||||
href="https://github.com/vector-im/riot-web/blob/master/docs/config.md"
|
||||
href="https://github.com/vector-im/element-web/blob/master/docs/config.md"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>{sub}</a>;
|
||||
|
@ -86,7 +97,7 @@ export default class AutoDiscoveryUtils {
|
|||
}
|
||||
|
||||
let isFatalError = true;
|
||||
const errorMessage = err.message ? err.message : err;
|
||||
const errorMessage = typeof err === "string" ? err : err.message;
|
||||
if (errorMessage === AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER) {
|
||||
isFatalError = false;
|
||||
title = _t("Cannot reach identity server");
|
||||
|
@ -135,7 +146,10 @@ export default class AutoDiscoveryUtils {
|
|||
* @returns {Promise<ValidatedServerConfig>} Resolves to the validated configuration.
|
||||
*/
|
||||
static async validateServerConfigWithStaticUrls(
|
||||
homeserverUrl: string, identityUrl: string, syntaxOnly = false): ValidatedServerConfig {
|
||||
homeserverUrl: string,
|
||||
identityUrl?: string,
|
||||
syntaxOnly = false,
|
||||
): Promise<ValidatedServerConfig> {
|
||||
if (!homeserverUrl) {
|
||||
throw newTranslatableError(_td("No homeserver URL provided"));
|
||||
}
|
||||
|
@ -157,7 +171,7 @@ export default class AutoDiscoveryUtils {
|
|||
const url = new URL(homeserverUrl);
|
||||
const serverName = url.hostname;
|
||||
|
||||
return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result, syntaxOnly);
|
||||
return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result, syntaxOnly, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -165,7 +179,7 @@ export default class AutoDiscoveryUtils {
|
|||
* @param {string} serverName The homeserver domain name (eg: "matrix.org") to validate.
|
||||
* @returns {Promise<ValidatedServerConfig>} Resolves to the validated configuration.
|
||||
*/
|
||||
static async validateServerName(serverName: string): ValidatedServerConfig {
|
||||
static async validateServerName(serverName: string): Promise<ValidatedServerConfig> {
|
||||
const result = await AutoDiscovery.findClientConfig(serverName);
|
||||
return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result);
|
||||
}
|
||||
|
@ -175,12 +189,12 @@ export default class AutoDiscoveryUtils {
|
|||
* input.
|
||||
* @param {string} serverName The domain name the AutoDiscovery result is for.
|
||||
* @param {*} discoveryResult The AutoDiscovery result.
|
||||
* @param {boolean} syntaxOnly If true, errors relating to liveliness of the servers will
|
||||
* not be raised.
|
||||
* @param {boolean} syntaxOnly If true, errors relating to liveliness of the servers will not be raised.
|
||||
* @param {boolean} isSynthetic If true, then the discoveryResult was synthesised locally.
|
||||
* @returns {Promise<ValidatedServerConfig>} Resolves to the validated configuration.
|
||||
*/
|
||||
static buildValidatedConfigFromDiscovery(
|
||||
serverName: string, discoveryResult, syntaxOnly=false): ValidatedServerConfig {
|
||||
serverName: string, discoveryResult, syntaxOnly=false, isSynthetic=false): ValidatedServerConfig {
|
||||
if (!discoveryResult || !discoveryResult["m.homeserver"]) {
|
||||
// This shouldn't happen without major misconfiguration, so we'll log a bit of information
|
||||
// in the log so we can find this bit of codee but otherwise tell teh user "it broke".
|
||||
|
@ -199,7 +213,7 @@ export default class AutoDiscoveryUtils {
|
|||
// Note: In the cases where we rely on the default IS from the config (namely
|
||||
// lack of identity server provided by the discovery method), we intentionally do not
|
||||
// validate it. This has already been validated and this helps some off-the-grid usage
|
||||
// of Riot.
|
||||
// of Element.
|
||||
let preferredIdentityUrl = defaultConfig && defaultConfig['isUrl'];
|
||||
if (isResult && isResult.state === AutoDiscovery.SUCCESS) {
|
||||
preferredIdentityUrl = isResult["base_url"];
|
||||
|
@ -248,6 +262,7 @@ export default class AutoDiscoveryUtils {
|
|||
isUrl: preferredIdentityUrl,
|
||||
isDefault: false,
|
||||
warning: hsResult.error,
|
||||
isNameResolvable: !isSynthetic,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2016, 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,9 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import {Room} from "matrix-js-sdk/src/matrix";
|
||||
import { uniq } from "lodash";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
|
||||
/**
|
||||
* Class that takes a Matrix Client and flips the m.direct map
|
||||
|
@ -27,29 +28,38 @@ import {Room} from "matrix-js-sdk/src/matrix";
|
|||
* With 'start', this can also keep itself up to date over time.
|
||||
*/
|
||||
export default class DMRoomMap {
|
||||
constructor(matrixClient) {
|
||||
this.matrixClient = matrixClient;
|
||||
this.roomToUser = null;
|
||||
// see _onAccountData
|
||||
this._hasSentOutPatchDirectAccountDataPatch = false;
|
||||
private static sharedInstance: DMRoomMap;
|
||||
|
||||
// XXX: Force-bind the event handler method because it
|
||||
// doesn't call it with our object as the 'this'
|
||||
// (use a static property arrow function for this when we can)
|
||||
this._onAccountData = this._onAccountData.bind(this);
|
||||
// TODO: convert these to maps
|
||||
private roomToUser: {[key: string]: string} = null;
|
||||
private userToRooms: {[key: string]: string[]} = null;
|
||||
private hasSentOutPatchDirectAccountDataPatch: boolean;
|
||||
private mDirectEvent: object;
|
||||
|
||||
constructor(private readonly matrixClient: MatrixClient) {
|
||||
// see onAccountData
|
||||
this.hasSentOutPatchDirectAccountDataPatch = false;
|
||||
|
||||
const mDirectEvent = matrixClient.getAccountData('m.direct');
|
||||
this.mDirectEvent = mDirectEvent ? mDirectEvent.getContent() : {};
|
||||
this.userToRooms = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes and returns a new shared instance that can then be accessed
|
||||
* with shared(). This returned instance is not automatically started.
|
||||
*/
|
||||
static makeShared() {
|
||||
DMRoomMap._sharedInstance = new DMRoomMap(MatrixClientPeg.get());
|
||||
return DMRoomMap._sharedInstance;
|
||||
public static makeShared(): DMRoomMap {
|
||||
DMRoomMap.sharedInstance = new DMRoomMap(MatrixClientPeg.get());
|
||||
return DMRoomMap.sharedInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the shared instance to the instance supplied
|
||||
* Used by tests
|
||||
* @param inst the new shared instance
|
||||
*/
|
||||
public static setShared(inst: DMRoomMap) {
|
||||
DMRoomMap.sharedInstance = inst;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -57,32 +67,33 @@ export default class DMRoomMap {
|
|||
* that uses the singleton matrix client
|
||||
* The shared instance must be started before use.
|
||||
*/
|
||||
static shared() {
|
||||
return DMRoomMap._sharedInstance;
|
||||
public static shared(): DMRoomMap {
|
||||
return DMRoomMap.sharedInstance;
|
||||
}
|
||||
|
||||
start() {
|
||||
this._populateRoomToUser();
|
||||
this.matrixClient.on("accountData", this._onAccountData);
|
||||
public start() {
|
||||
this.populateRoomToUser();
|
||||
this.matrixClient.on("accountData", this.onAccountData);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.matrixClient.removeListener("accountData", this._onAccountData);
|
||||
public stop() {
|
||||
this.matrixClient.removeListener("accountData", this.onAccountData);
|
||||
}
|
||||
|
||||
_onAccountData(ev) {
|
||||
private onAccountData = (ev) => {
|
||||
if (ev.getType() == 'm.direct') {
|
||||
this.mDirectEvent = this.matrixClient.getAccountData('m.direct').getContent() || {};
|
||||
this.userToRooms = null;
|
||||
this.roomToUser = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* some client bug somewhere is causing some DMs to be marked
|
||||
* with ourself, not the other user. Fix it by guessing the other user and
|
||||
* modifying userToRooms
|
||||
*/
|
||||
_patchUpSelfDMs(userToRooms) {
|
||||
private patchUpSelfDMs(userToRooms) {
|
||||
const myUserId = this.matrixClient.getUserId();
|
||||
const selfRoomIds = userToRooms[myUserId];
|
||||
if (selfRoomIds) {
|
||||
|
@ -92,7 +103,7 @@ export default class DMRoomMap {
|
|||
if (room) {
|
||||
const userId = room.guessDMUserId();
|
||||
if (userId && userId !== myUserId) {
|
||||
return {userId, roomId};
|
||||
return { userId, roomId };
|
||||
}
|
||||
}
|
||||
}).filter((ids) => !!ids); //filter out
|
||||
|
@ -105,23 +116,23 @@ export default class DMRoomMap {
|
|||
return !guessedUserIdsThatChanged
|
||||
.some((ids) => ids.roomId === roomId);
|
||||
});
|
||||
guessedUserIdsThatChanged.forEach(({userId, roomId}) => {
|
||||
guessedUserIdsThatChanged.forEach(({ userId, roomId }) => {
|
||||
const roomIds = userToRooms[userId];
|
||||
if (!roomIds) {
|
||||
userToRooms[userId] = [roomId];
|
||||
} else {
|
||||
roomIds.push(roomId);
|
||||
userToRooms[userId] = _uniq(roomIds);
|
||||
userToRooms[userId] = uniq(roomIds);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
getDMRoomsForUserId(userId) {
|
||||
public getDMRoomsForUserId(userId): string[] {
|
||||
// Here, we return the empty list if there are no rooms,
|
||||
// since the number of conversations you have with this user is zero.
|
||||
return this._getUserToRooms()[userId] || [];
|
||||
return this.getUserToRooms()[userId] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -129,7 +140,7 @@ export default class DMRoomMap {
|
|||
* @param {string[]} ids The identifiers (user IDs and email addresses) to look for.
|
||||
* @returns {Room} The DM room which all IDs given share, or falsey if no common room.
|
||||
*/
|
||||
getDMRoomForIdentifiers(ids) {
|
||||
public getDMRoomForIdentifiers(ids: string[]): Room {
|
||||
// TODO: [Canonical DMs] Handle lookups for email addresses.
|
||||
// For now we'll pretend we only get user IDs and end up returning nothing for email addresses
|
||||
|
||||
|
@ -145,7 +156,7 @@ export default class DMRoomMap {
|
|||
return joinedRooms[0];
|
||||
}
|
||||
|
||||
getUserIdForRoomId(roomId) {
|
||||
public getUserIdForRoomId(roomId: string) {
|
||||
if (this.roomToUser == null) {
|
||||
// we lazily populate roomToUser so you can use
|
||||
// this class just to call getDMRoomsForUserId
|
||||
|
@ -153,7 +164,7 @@ export default class DMRoomMap {
|
|||
// convenient wrapper and there's no point
|
||||
// iterating through the map if getUserIdForRoomId()
|
||||
// is never called.
|
||||
this._populateRoomToUser();
|
||||
this.populateRoomToUser();
|
||||
}
|
||||
// Here, we return undefined if the room is not in the map:
|
||||
// the room ID you gave is not a DM room for any user.
|
||||
|
@ -167,28 +178,28 @@ export default class DMRoomMap {
|
|||
return this.roomToUser[roomId];
|
||||
}
|
||||
|
||||
getUniqueRoomsWithIndividuals(): {[userId: string]: Room} {
|
||||
public getUniqueRoomsWithIndividuals(): {[userId: string]: Room} {
|
||||
if (!this.roomToUser) return {}; // No rooms means no map.
|
||||
return Object.keys(this.roomToUser)
|
||||
.map(r => ({userId: this.getUserIdForRoomId(r), room: this.matrixClient.getRoom(r)}))
|
||||
.map(r => ({ userId: this.getUserIdForRoomId(r), room: this.matrixClient.getRoom(r) }))
|
||||
.filter(r => r.userId && r.room && r.room.getInvitedAndJoinedMemberCount() === 2)
|
||||
.reduce((obj, r) => (obj[r.userId] = r.room) && obj, {});
|
||||
}
|
||||
|
||||
_getUserToRooms() {
|
||||
private getUserToRooms(): {[key: string]: string[]} {
|
||||
if (!this.userToRooms) {
|
||||
const userToRooms = this.mDirectEvent;
|
||||
const userToRooms = this.mDirectEvent as {[key: string]: string[]};
|
||||
const myUserId = this.matrixClient.getUserId();
|
||||
const selfDMs = userToRooms[myUserId];
|
||||
if (selfDMs && selfDMs.length) {
|
||||
const neededPatching = this._patchUpSelfDMs(userToRooms);
|
||||
const neededPatching = this.patchUpSelfDMs(userToRooms);
|
||||
// to avoid multiple devices fighting to correct
|
||||
// the account data, only try to send the corrected
|
||||
// version once.
|
||||
console.warn(`Invalid m.direct account data detected ` +
|
||||
`(self-chats that shouldn't be), patching it up.`);
|
||||
if (neededPatching && !this._hasSentOutPatchDirectAccountDataPatch) {
|
||||
this._hasSentOutPatchDirectAccountDataPatch = true;
|
||||
if (neededPatching && !this.hasSentOutPatchDirectAccountDataPatch) {
|
||||
this.hasSentOutPatchDirectAccountDataPatch = true;
|
||||
this.matrixClient.setAccountData('m.direct', userToRooms);
|
||||
}
|
||||
}
|
||||
|
@ -197,9 +208,9 @@ export default class DMRoomMap {
|
|||
return this.userToRooms;
|
||||
}
|
||||
|
||||
_populateRoomToUser() {
|
||||
private populateRoomToUser() {
|
||||
this.roomToUser = {};
|
||||
for (const user of Object.keys(this._getUserToRooms())) {
|
||||
for (const user of Object.keys(this.getUserToRooms())) {
|
||||
for (const roomId of this.userToRooms[user]) {
|
||||
this.roomToUser[roomId] = user;
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Pull in the encryption lib so that we can decrypt attachments.
|
||||
import encrypt from 'browser-encrypt-attachment';
|
||||
// Grab the client so that we can turn mxc:// URLs into https:// URLS.
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
|
||||
// WARNING: We have to be very careful about what mime-types we allow into blobs,
|
||||
// as for performance reasons these are now rendered via URL.createObjectURL()
|
||||
// rather than by converting into data: URIs.
|
||||
//
|
||||
// This means that the content is rendered using the origin of the script which
|
||||
// called createObjectURL(), and so if the content contains any scripting then it
|
||||
// will pose a XSS vulnerability when the browser renders it. This is particularly
|
||||
// bad if the user right-clicks the URI and pastes it into a new window or tab,
|
||||
// as the blob will then execute with access to Riot's full JS environment(!)
|
||||
//
|
||||
// See https://github.com/matrix-org/matrix-react-sdk/pull/1820#issuecomment-385210647
|
||||
// for details.
|
||||
//
|
||||
// We mitigate this by only allowing mime-types into blobs which we know don't
|
||||
// contain any scripting, and instantiate all others as application/octet-stream
|
||||
// regardless of what mime-type the event claimed. Even if the payload itself
|
||||
// is some malicious HTML, the fact we instantiate it with a media mimetype or
|
||||
// application/octet-stream means the browser doesn't try to render it as such.
|
||||
//
|
||||
// One interesting edge case is image/svg+xml, which empirically *is* rendered
|
||||
// correctly if the blob is set to the src attribute of an img tag (for thumbnails)
|
||||
// *even if the mimetype is application/octet-stream*. However, empirically JS
|
||||
// in the SVG isn't executed in this scenario, so we seem to be okay.
|
||||
//
|
||||
// Tested on Chrome 65 and Firefox 60
|
||||
//
|
||||
// The list below is taken mainly from
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
|
||||
// N.B. Matrix doesn't currently specify which mimetypes are valid in given
|
||||
// events, so we pick the ones which HTML5 browsers should be able to display
|
||||
//
|
||||
// For the record, mime-types which must NEVER enter this list below include:
|
||||
// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar.
|
||||
|
||||
const ALLOWED_BLOB_MIMETYPES = {
|
||||
'image/jpeg': true,
|
||||
'image/gif': true,
|
||||
'image/png': true,
|
||||
|
||||
'video/mp4': true,
|
||||
'video/webm': true,
|
||||
'video/ogg': true,
|
||||
|
||||
'audio/mp4': true,
|
||||
'audio/webm': true,
|
||||
'audio/aac': true,
|
||||
'audio/mpeg': true,
|
||||
'audio/ogg': true,
|
||||
'audio/wave': true,
|
||||
'audio/wav': true,
|
||||
'audio/x-wav': true,
|
||||
'audio/x-pn-wav': true,
|
||||
'audio/flac': true,
|
||||
'audio/x-flac': true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Decrypt a file attached to a matrix event.
|
||||
* @param file {Object} The json taken from the matrix event.
|
||||
* This passed to [link]{@link https://github.com/matrix-org/browser-encrypt-attachments}
|
||||
* as the encryption info object, so will also have the those keys in addition to
|
||||
* the keys below.
|
||||
* @param file.url {string} An mxc:// URL for the encrypted file.
|
||||
* @param file.mimetype {string} The MIME-type of the plaintext file.
|
||||
*/
|
||||
export function decryptFile(file) {
|
||||
const url = MatrixClientPeg.get().mxcUrlToHttp(file.url);
|
||||
// Download the encrypted file as an array buffer.
|
||||
return Promise.resolve(fetch(url)).then(function(response) {
|
||||
return response.arrayBuffer();
|
||||
}).then(function(responseData) {
|
||||
// Decrypt the array buffer using the information taken from
|
||||
// the event content.
|
||||
return encrypt.decryptAttachment(responseData, file);
|
||||
}).then(function(dataArray) {
|
||||
// Turn the array into a Blob and give it the correct MIME-type.
|
||||
|
||||
// IMPORTANT: we must not allow scriptable mime-types into Blobs otherwise
|
||||
// they introduce XSS attacks if the Blob URI is viewed directly in the
|
||||
// browser (e.g. by copying the URI into a new tab or window.)
|
||||
// See warning at top of file.
|
||||
let mimetype = file.mimetype ? file.mimetype.split(";")[0].trim() : '';
|
||||
if (!ALLOWED_BLOB_MIMETYPES[mimetype]) {
|
||||
mimetype = 'application/octet-stream';
|
||||
}
|
||||
|
||||
const blob = new Blob([dataArray], {type: mimetype});
|
||||
return blob;
|
||||
});
|
||||
}
|
52
src/utils/DecryptFile.ts
Normal file
52
src/utils/DecryptFile.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
Copyright 2016, 2018, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Pull in the encryption lib so that we can decrypt attachments.
|
||||
import encrypt from 'browser-encrypt-attachment';
|
||||
import { mediaFromContent } from "../customisations/Media";
|
||||
import { IEncryptedFile } from "../customisations/models/IMediaEventContent";
|
||||
import { getBlobSafeMimeType } from "./blobs";
|
||||
|
||||
/**
|
||||
* Decrypt a file attached to a matrix event.
|
||||
* @param {IEncryptedFile} file The json taken from the matrix event.
|
||||
* This passed to [link]{@link https://github.com/matrix-org/browser-encrypt-attachments}
|
||||
* as the encryption info object, so will also have the those keys in addition to
|
||||
* the keys below.
|
||||
* @returns {Promise<Blob>} Resolves to a Blob of the file.
|
||||
*/
|
||||
export function decryptFile(file: IEncryptedFile): Promise<Blob> {
|
||||
const media = mediaFromContent({ file });
|
||||
// Download the encrypted file as an array buffer.
|
||||
return media.downloadSource().then((response) => {
|
||||
return response.arrayBuffer();
|
||||
}).then((responseData) => {
|
||||
// Decrypt the array buffer using the information taken from
|
||||
// the event content.
|
||||
return encrypt.decryptAttachment(responseData, file);
|
||||
}).then((dataArray) => {
|
||||
// Turn the array into a Blob and give it the correct MIME-type.
|
||||
|
||||
// IMPORTANT: we must not allow scriptable mime-types into Blobs otherwise
|
||||
// they introduce XSS attacks if the Blob URI is viewed directly in the
|
||||
// browser (e.g. by copying the URI into a new tab or window.)
|
||||
// See warning at top of file.
|
||||
let mimetype = file.mimetype ? file.mimetype.split(";")[0].trim() : '';
|
||||
mimetype = getBlobSafeMimeType(mimetype);
|
||||
|
||||
return new Blob([dataArray], { type: mimetype });
|
||||
});
|
||||
}
|
|
@ -1,23 +1,39 @@
|
|||
/*
|
||||
Copyright 2018 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Find a protocol 'instance' with a given instance_id
|
||||
// in the supplied protocols dict
|
||||
export function instanceForInstanceId(protocols, instance_id) {
|
||||
if (!instance_id) return null;
|
||||
export function instanceForInstanceId(protocols, instanceId) {
|
||||
if (!instanceId) return null;
|
||||
for (const proto of Object.keys(protocols)) {
|
||||
if (!protocols[proto].instances && protocols[proto].instances instanceof Array) continue;
|
||||
for (const instance of protocols[proto].instances) {
|
||||
if (instance.instance_id == instance_id) return instance;
|
||||
if (instance.instance_id == instanceId) return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// given an instance_id, return the name of the protocol for
|
||||
// that instance ID in the supplied protocols dict
|
||||
export function protocolNameForInstanceId(protocols, instance_id) {
|
||||
if (!instance_id) return null;
|
||||
export function protocolNameForInstanceId(protocols, instanceId) {
|
||||
if (!instanceId) return null;
|
||||
for (const proto of Object.keys(protocols)) {
|
||||
if (!protocols[proto].instances && protocols[proto].instances instanceof Array) continue;
|
||||
for (const instance of protocols[proto].instances) {
|
||||
if (instance.instance_id == instance_id) return proto;
|
||||
if (instance.instance_id == instanceId) return proto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Used while editing, to pass the event, and to preserve editor state
|
||||
* from one editor instance to another when remounting the editor
|
||||
* upon receiving the remote echo for an unsent event.
|
||||
*/
|
||||
export default class EditorStateTransfer {
|
||||
constructor(event) {
|
||||
this._event = event;
|
||||
this._serializedParts = null;
|
||||
this.caret = null;
|
||||
}
|
||||
|
||||
setEditorState(caret, serializedParts) {
|
||||
this._caret = caret;
|
||||
this._serializedParts = serializedParts;
|
||||
}
|
||||
|
||||
hasEditorState() {
|
||||
return !!this._serializedParts;
|
||||
}
|
||||
|
||||
getSerializedParts() {
|
||||
return this._serializedParts;
|
||||
}
|
||||
|
||||
getCaret() {
|
||||
return this._caret;
|
||||
}
|
||||
|
||||
getEvent() {
|
||||
return this._event;
|
||||
}
|
||||
}
|
53
src/utils/EditorStateTransfer.ts
Normal file
53
src/utils/EditorStateTransfer.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { SerializedPart } from "../editor/parts";
|
||||
import DocumentOffset from "../editor/offset";
|
||||
|
||||
/**
|
||||
* Used while editing, to pass the event, and to preserve editor state
|
||||
* from one editor instance to another when remounting the editor
|
||||
* upon receiving the remote echo for an unsent event.
|
||||
*/
|
||||
export default class EditorStateTransfer {
|
||||
private serializedParts: SerializedPart[] = null;
|
||||
private caret: DocumentOffset = null;
|
||||
|
||||
constructor(private readonly event: MatrixEvent) {}
|
||||
|
||||
public setEditorState(caret: DocumentOffset, serializedParts: SerializedPart[]) {
|
||||
this.caret = caret;
|
||||
this.serializedParts = serializedParts;
|
||||
}
|
||||
|
||||
public hasEditorState(): boolean {
|
||||
return !!this.serializedParts;
|
||||
}
|
||||
|
||||
public getSerializedParts(): SerializedPart[] {
|
||||
return this.serializedParts;
|
||||
}
|
||||
|
||||
public getCaret(): DocumentOffset {
|
||||
return this.caret;
|
||||
}
|
||||
|
||||
public getEvent(): MatrixEvent {
|
||||
return this.event;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,7 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { _t, _td } from '../languageHandler';
|
||||
import React, { ReactNode } from "react";
|
||||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
|
||||
import { _t, _td, Tags, TranslatedString } from '../languageHandler';
|
||||
|
||||
/**
|
||||
* Produce a translated error message for a
|
||||
|
@ -30,7 +33,12 @@ import { _t, _td } from '../languageHandler';
|
|||
* for any tags in the strings apart from 'a'
|
||||
* @returns {*} Translated string or react component
|
||||
*/
|
||||
export function messageForResourceLimitError(limitType, adminContact, strings, extraTranslations) {
|
||||
export function messageForResourceLimitError(
|
||||
limitType: string,
|
||||
adminContact: string,
|
||||
strings: Record<string, string>,
|
||||
extraTranslations?: Tags,
|
||||
): TranslatedString {
|
||||
let errString = strings[limitType];
|
||||
if (errString === undefined) errString = strings[''];
|
||||
|
||||
|
@ -49,19 +57,14 @@ export function messageForResourceLimitError(limitType, adminContact, strings, e
|
|||
}
|
||||
}
|
||||
|
||||
export function messageForSendError(errorData) {
|
||||
if (errorData.errcode === "M_TOO_LARGE") {
|
||||
return _t("The message you are trying to send is too large.");
|
||||
}
|
||||
}
|
||||
|
||||
export function messageForSyncError(err) {
|
||||
export function messageForSyncError(err: MatrixError | Error): ReactNode {
|
||||
if (err.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const limitError = messageForResourceLimitError(
|
||||
err.data.limit_type,
|
||||
err.data.admin_contact,
|
||||
{
|
||||
'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."),
|
||||
'hs_blocked': _td("This homeserver has been blocked by its administrator."),
|
||||
'': _td("This homeserver has exceeded one of its resource limits."),
|
||||
},
|
||||
);
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,9 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventStatus } from 'matrix-js-sdk';
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import shouldHideEvent from "../shouldHideEvent";
|
||||
import { getHandlerTile, haveTileForEvent } from "../components/views/rooms/EventTile";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
/**
|
||||
* Returns whether an event should allow actions like reply, reactions, edit, etc.
|
||||
* which effectively checks whether it's a regular message that has been sent and that we
|
||||
|
@ -25,13 +31,13 @@ import shouldHideEvent from "../shouldHideEvent";
|
|||
* @param {MatrixEvent} mxEvent The event to check
|
||||
* @returns {boolean} true if actionable
|
||||
*/
|
||||
export function isContentActionable(mxEvent) {
|
||||
export function isContentActionable(mxEvent: MatrixEvent): boolean {
|
||||
const { status: eventStatus } = mxEvent;
|
||||
|
||||
// status is SENT before remote-echo, null after
|
||||
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
||||
|
||||
if (isSent) {
|
||||
if (isSent && !mxEvent.isRedacted()) {
|
||||
if (mxEvent.getType() === 'm.room.message') {
|
||||
const content = mxEvent.getContent();
|
||||
if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) {
|
||||
|
@ -45,18 +51,18 @@ export function isContentActionable(mxEvent) {
|
|||
return false;
|
||||
}
|
||||
|
||||
export function canEditContent(mxEvent) {
|
||||
export function canEditContent(mxEvent: MatrixEvent): boolean {
|
||||
if (mxEvent.status === EventStatus.CANCELLED || mxEvent.getType() !== "m.room.message" || mxEvent.isRedacted()) {
|
||||
return false;
|
||||
}
|
||||
const content = mxEvent.getOriginalContent();
|
||||
const {msgtype} = content;
|
||||
const { msgtype } = content;
|
||||
return (msgtype === "m.text" || msgtype === "m.emote") &&
|
||||
content.body && typeof content.body === 'string' &&
|
||||
mxEvent.getSender() === MatrixClientPeg.get().getUserId();
|
||||
}
|
||||
|
||||
export function canEditOwnEvent(mxEvent) {
|
||||
export function canEditOwnEvent(mxEvent: MatrixEvent): boolean {
|
||||
// for now we only allow editing
|
||||
// your own events. So this just call through
|
||||
// In the future though, moderators will be able to
|
||||
|
@ -67,7 +73,7 @@ export function canEditOwnEvent(mxEvent) {
|
|||
}
|
||||
|
||||
const MAX_JUMP_DISTANCE = 100;
|
||||
export function findEditableEvent(room, isForward, fromEventId = undefined) {
|
||||
export function findEditableEvent(room: Room, isForward: boolean, fromEventId: string = undefined): MatrixEvent {
|
||||
const liveTimeline = room.getLiveTimeline();
|
||||
const events = liveTimeline.getEvents().concat(room.getPendingEvents());
|
||||
const maxIdx = events.length - 1;
|
||||
|
@ -93,3 +99,38 @@ export function findEditableEvent(room, isForward, fromEventId = undefined) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getEventDisplayInfo(mxEvent: MatrixEvent): {
|
||||
isInfoMessage: boolean;
|
||||
tileHandler: string;
|
||||
isBubbleMessage: boolean;
|
||||
} {
|
||||
const content = mxEvent.getContent();
|
||||
const msgtype = content.msgtype;
|
||||
const eventType = mxEvent.getType();
|
||||
|
||||
let tileHandler = getHandlerTile(mxEvent);
|
||||
|
||||
// Info messages are basically information about commands processed on a room
|
||||
let isBubbleMessage = eventType.startsWith("m.key.verification") ||
|
||||
(eventType === EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification")) ||
|
||||
(eventType === EventType.RoomCreate) ||
|
||||
(eventType === EventType.RoomEncryption) ||
|
||||
(tileHandler === "messages.MJitsiWidgetEvent");
|
||||
let isInfoMessage = (
|
||||
!isBubbleMessage && eventType !== EventType.RoomMessage &&
|
||||
eventType !== EventType.Sticker && eventType !== EventType.RoomCreate
|
||||
);
|
||||
|
||||
// If we're showing hidden events in the timeline, we should use the
|
||||
// source tile when there's no regular tile for an event and also for
|
||||
// replace relations (which otherwise would display as a confusing
|
||||
// duplicate of the thing they are replacing).
|
||||
if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(mxEvent)) {
|
||||
tileHandler = "messages.ViewSourceEvent";
|
||||
isBubbleMessage = false;
|
||||
// Reuse info message avatar and sender profile styling
|
||||
isInfoMessage = true;
|
||||
}
|
||||
|
||||
return { tileHandler, isInfoMessage, isBubbleMessage };
|
||||
}
|
54
src/utils/FixedRollingArray.ts
Normal file
54
src/utils/FixedRollingArray.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { arrayFastClone, arraySeed } from "./arrays";
|
||||
|
||||
/**
|
||||
* An array which is of fixed length and accepts rolling values. Values will
|
||||
* be inserted on the left, falling off the right.
|
||||
*/
|
||||
export class FixedRollingArray<T> {
|
||||
private samples: T[] = [];
|
||||
|
||||
/**
|
||||
* Creates a new fixed rolling array.
|
||||
* @param width The width of the array.
|
||||
* @param padValue The value to seed the array with.
|
||||
*/
|
||||
constructor(private width: number, padValue: T) {
|
||||
this.samples = arraySeed(padValue, this.width);
|
||||
}
|
||||
|
||||
/**
|
||||
* The array, as a fixed length.
|
||||
*/
|
||||
public get value(): T[] {
|
||||
return this.samples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a value to the array.
|
||||
* @param value The value to push.
|
||||
*/
|
||||
public pushValue(value: T) {
|
||||
let swap = arrayFastClone(this.samples);
|
||||
swap.splice(0, 0, value);
|
||||
if (swap.length > this.width) {
|
||||
swap = swap.slice(0, this.width);
|
||||
}
|
||||
this.samples = swap;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -21,7 +21,7 @@ limitations under the License.
|
|||
* MIT license
|
||||
*/
|
||||
|
||||
function safariVersionCheck(ua) {
|
||||
function safariVersionCheck(ua: string): boolean {
|
||||
console.log("Browser is Safari - checking version for COLR support");
|
||||
try {
|
||||
const safariVersionMatch = ua.match(/Mac OS X ([\d|_]+).*Version\/([\d|.]+).*Safari/);
|
||||
|
@ -44,10 +44,10 @@ function safariVersionCheck(ua) {
|
|||
return false;
|
||||
}
|
||||
|
||||
async function isColrFontSupported() {
|
||||
async function isColrFontSupported(): Promise<boolean> {
|
||||
console.log("Checking for COLR support");
|
||||
|
||||
const {userAgent} = navigator;
|
||||
const { userAgent } = navigator;
|
||||
// Firefox has supported COLR fonts since version 26
|
||||
// but doesn't support the check below without
|
||||
// "Extract canvas data" permissions
|
||||
|
@ -101,7 +101,7 @@ async function isColrFontSupported() {
|
|||
}
|
||||
|
||||
let colrFontCheckStarted = false;
|
||||
export async function fixupColorFonts() {
|
||||
export async function fixupColorFonts(): Promise<void> {
|
||||
if (colrFontCheckStarted) {
|
||||
return;
|
||||
}
|
||||
|
@ -112,14 +112,14 @@ export async function fixupColorFonts() {
|
|||
document.fonts.add(new FontFace("Twemoji", path, {}));
|
||||
// For at least Chrome on Windows 10, we have to explictly add extra
|
||||
// weights for the emoji to appear in bold messages, etc.
|
||||
document.fonts.add(new FontFace("Twemoji", path, { weight: 600 }));
|
||||
document.fonts.add(new FontFace("Twemoji", path, { weight: 700 }));
|
||||
document.fonts.add(new FontFace("Twemoji", path, { weight: "600" }));
|
||||
document.fonts.add(new FontFace("Twemoji", path, { weight: "700" }));
|
||||
} else {
|
||||
// fall back to SBIX, generated via https://github.com/matrix-org/twemoji-colr/tree/matthew/sbix
|
||||
const path = `url('${require("../../res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2")}')`;
|
||||
document.fonts.add(new FontFace("Twemoji", path, {}));
|
||||
document.fonts.add(new FontFace("Twemoji", path, { weight: 600 }));
|
||||
document.fonts.add(new FontFace("Twemoji", path, { weight: 700 }));
|
||||
document.fonts.add(new FontFace("Twemoji", path, { weight: "600" }));
|
||||
document.fonts.add(new FontFace("Twemoji", path, { weight: "700" }));
|
||||
}
|
||||
// ...and if SBIX is not supported, the browser will fall back to one of the native fonts specified.
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -21,29 +21,29 @@ import { _t } from '../languageHandler';
|
|||
* formats numbers to fit into ~3 characters, suitable for badge counts
|
||||
* e.g: 999, 9.9K, 99K, 0.9M, 9.9M, 99M, 0.9B, 9.9B
|
||||
*/
|
||||
export function formatCount(count) {
|
||||
if (count < 1000) return count;
|
||||
if (count < 10000) return (count / 1000).toFixed(1) + "K";
|
||||
if (count < 100000) return (count / 1000).toFixed(0) + "K";
|
||||
if (count < 10000000) return (count / 1000000).toFixed(1) + "M";
|
||||
if (count < 100000000) return (count / 1000000).toFixed(0) + "M";
|
||||
return (count / 1000000000).toFixed(1) + "B"; // 10B is enough for anyone, right? :S
|
||||
export function formatCount(count: number): string {
|
||||
if (count < 1000) return count.toString();
|
||||
if (count < 10000) return (count / 1000).toFixed(1) + "K";
|
||||
if (count < 100000) return (count / 1000).toFixed(0) + "K";
|
||||
if (count < 10000000) return (count / 1000000).toFixed(1) + "M";
|
||||
if (count < 100000000) return (count / 1000000).toFixed(0) + "M";
|
||||
return (count / 1000000000).toFixed(1) + "B"; // 10B is enough for anyone, right? :S
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a count showing the whole number but making it a bit more readable.
|
||||
* e.g: 1000 => 1,000
|
||||
*/
|
||||
export function formatCountLong(count) {
|
||||
export function formatCountLong(count: number): string {
|
||||
const formatter = new Intl.NumberFormat();
|
||||
return formatter.format(count)
|
||||
return formatter.format(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* format a size in bytes into a human readable form
|
||||
* e.g: 1024 -> 1.00 KB
|
||||
*/
|
||||
export function formatBytes(bytes, decimals = 2) {
|
||||
export function formatBytes(bytes: number, decimals = 2): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
|
@ -62,7 +62,7 @@ export function formatBytes(bytes, decimals = 2) {
|
|||
*
|
||||
* @return {string}
|
||||
*/
|
||||
export function formatCryptoKey(key) {
|
||||
export function formatCryptoKey(key: string): string {
|
||||
return key.match(/.{1,4}/g).join(" ");
|
||||
}
|
||||
/**
|
||||
|
@ -72,7 +72,7 @@ export function formatCryptoKey(key) {
|
|||
*
|
||||
* @return {number}
|
||||
*/
|
||||
export function hashCode(str) {
|
||||
export function hashCode(str: string): number {
|
||||
let hash = 0;
|
||||
let i;
|
||||
let chr;
|
||||
|
@ -87,7 +87,7 @@ export function hashCode(str) {
|
|||
return Math.abs(hash);
|
||||
}
|
||||
|
||||
export function getUserNameColorClass(userId) {
|
||||
export function getUserNameColorClass(userId: string): string {
|
||||
const colorNumber = (hashCode(userId) % 8) + 1;
|
||||
return `mx_Username_color${colorNumber}`;
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ export function getUserNameColorClass(userId) {
|
|||
* @returns {string} a string constructed by joining `items` with a comma
|
||||
* between each item, but with the last item appended as " and [lastItem]".
|
||||
*/
|
||||
export function formatCommaSeparatedList(items, itemLimit) {
|
||||
export function formatCommaSeparatedList(items: string[], itemLimit?: number): string {
|
||||
const remaining = itemLimit === undefined ? 0 : Math.max(
|
||||
items.length - itemLimit, 0,
|
||||
);
|
|
@ -17,8 +17,8 @@ limitations under the License.
|
|||
import url from 'url';
|
||||
|
||||
import SdkConfig from '../SdkConfig';
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import {urlSearchParamsToObject} from "./UrlUtils";
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import { urlSearchParamsToObject } from "./UrlUtils";
|
||||
|
||||
export function getHostingLink(campaign) {
|
||||
const hostingLink = SdkConfig.get().hosting_signup_link;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,15 +14,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Automatically focuses the captured reference when receiving a non-null
|
||||
* object. Useful in scenarios where componentDidMount does not have a
|
||||
* useful reference to an element, but one needs to focus the element on
|
||||
* first render. Example usage: ref={focusCapturedRef}
|
||||
* @param {function} ref The React reference to focus on, if not null
|
||||
*/
|
||||
export function focusCapturedRef(ref) {
|
||||
if (ref) {
|
||||
ref.focus();
|
||||
}
|
||||
export interface IDestroyable {
|
||||
destroy(): void;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,15 +14,16 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { SERVICE_TYPES } from 'matrix-js-sdk';
|
||||
import SdkConfig from '../SdkConfig';
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
|
||||
|
||||
export function getDefaultIdentityServerUrl() {
|
||||
import SdkConfig from '../SdkConfig';
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
|
||||
export function getDefaultIdentityServerUrl(): string {
|
||||
return SdkConfig.get()['validated_server_config']['isUrl'];
|
||||
}
|
||||
|
||||
export function useDefaultIdentityServer() {
|
||||
export function useDefaultIdentityServer(): void {
|
||||
const url = getDefaultIdentityServerUrl();
|
||||
// Account data change will update localstorage, client, etc through dispatcher
|
||||
MatrixClientPeg.get().setAccountData("m.identity_server", {
|
||||
|
@ -30,7 +31,7 @@ export function useDefaultIdentityServer() {
|
|||
});
|
||||
}
|
||||
|
||||
export async function doesIdentityServerHaveTerms(fullUrl) {
|
||||
export async function doesIdentityServerHaveTerms(fullUrl: string): Promise<boolean> {
|
||||
let terms;
|
||||
try {
|
||||
terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl);
|
||||
|
@ -46,7 +47,7 @@ export async function doesIdentityServerHaveTerms(fullUrl) {
|
|||
return terms && terms["policies"] && (Object.keys(terms["policies"]).length > 0);
|
||||
}
|
||||
|
||||
export function doesAccountDataHaveIdentityServer() {
|
||||
export function doesAccountDataHaveIdentityServer(): boolean {
|
||||
const event = MatrixClientPeg.get().getAccountData("m.identity_server");
|
||||
return event && event.getContent() && event.getContent()['base_url'];
|
||||
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import { _t } from '../languageHandler';
|
||||
|
||||
export function getNameForEventRoom(userId, roomId) {
|
||||
|
@ -27,7 +27,7 @@ export function getNameForEventRoom(userId, roomId) {
|
|||
export function userLabelForEventRoom(userId, roomId) {
|
||||
const name = getNameForEventRoom(userId, roomId);
|
||||
if (name !== userId) {
|
||||
return _t("%(name)s (%(userId)s)", {name, userId});
|
||||
return _t("%(name)s (%(userId)s)", { name, userId });
|
||||
} else {
|
||||
return userId;
|
||||
}
|
||||
|
|
59
src/utils/MarkedExecution.ts
Normal file
59
src/utils/MarkedExecution.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A utility to ensure that a function is only called once triggered with
|
||||
* a mark applied. Multiple marks can be applied to the function, however
|
||||
* the function will only be called once upon trigger().
|
||||
*
|
||||
* The function starts unmarked.
|
||||
*/
|
||||
export class MarkedExecution {
|
||||
private marked = false;
|
||||
|
||||
/**
|
||||
* Creates a MarkedExecution for the provided function.
|
||||
* @param {Function} fn The function to be called upon trigger if marked.
|
||||
* @param {Function} onMarkCallback A function that is called when a new mark is made. Not
|
||||
* called if a mark is already flagged.
|
||||
*/
|
||||
constructor(private fn: () => void, private onMarkCallback?: () => void) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the mark without calling the function.
|
||||
*/
|
||||
public reset() {
|
||||
this.marked = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the function to be called upon trigger().
|
||||
*/
|
||||
public mark() {
|
||||
if (!this.marked) this.onMarkCallback?.();
|
||||
this.marked = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If marked, the function will be called, otherwise this does nothing.
|
||||
*/
|
||||
public trigger() {
|
||||
if (!this.marked) return;
|
||||
this.reset(); // reset first just in case the fn() causes a trigger()
|
||||
this.fn();
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,21 +15,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// polyfill textencoder if necessary
|
||||
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
|
||||
let TextEncoder = window.TextEncoder;
|
||||
if (!TextEncoder) {
|
||||
TextEncoder = TextEncodingUtf8.TextEncoder;
|
||||
}
|
||||
let TextDecoder = window.TextDecoder;
|
||||
if (!TextDecoder) {
|
||||
TextDecoder = TextEncodingUtf8.TextDecoder;
|
||||
}
|
||||
|
||||
import { _t } from '../languageHandler';
|
||||
|
||||
import SdkConfig from '../SdkConfig';
|
||||
|
||||
const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle;
|
||||
|
||||
|
@ -61,23 +49,24 @@ function cryptoFailMsg() {
|
|||
*/
|
||||
export async function decryptMegolmKeyFile(data, password) {
|
||||
const body = unpackMegolmKeyFile(data);
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
// check we have a version byte
|
||||
if (body.length < 1) {
|
||||
throw friendlyError('Invalid file: too short',
|
||||
_t('Not a valid Riot keyfile'));
|
||||
_t('Not a valid %(brand)s keyfile', { brand }));
|
||||
}
|
||||
|
||||
const version = body[0];
|
||||
if (version !== 1) {
|
||||
throw friendlyError('Unsupported version',
|
||||
_t('Not a valid Riot keyfile'));
|
||||
_t('Not a valid %(brand)s keyfile', { brand }));
|
||||
}
|
||||
|
||||
const ciphertextLength = body.length-(1+16+16+4+32);
|
||||
if (ciphertextLength < 0) {
|
||||
throw friendlyError('Invalid file: too short',
|
||||
_t('Not a valid Riot keyfile'));
|
||||
_t('Not a valid %(brand)s keyfile', { brand }));
|
||||
}
|
||||
|
||||
const salt = body.subarray(1, 1+16);
|
||||
|
@ -92,7 +81,7 @@ export async function decryptMegolmKeyFile(data, password) {
|
|||
let isValid;
|
||||
try {
|
||||
isValid = await subtleCrypto.verify(
|
||||
{name: 'HMAC'},
|
||||
{ name: 'HMAC' },
|
||||
hmacKey,
|
||||
hmac,
|
||||
toVerify,
|
||||
|
@ -123,7 +112,6 @@ export async function decryptMegolmKeyFile(data, password) {
|
|||
return new TextDecoder().decode(new Uint8Array(plaintext));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt a megolm key file
|
||||
*
|
||||
|
@ -185,7 +173,7 @@ export async function encryptMegolmKeyFile(data, password, options) {
|
|||
let hmac;
|
||||
try {
|
||||
hmac = await subtleCrypto.sign(
|
||||
{name: 'HMAC'},
|
||||
{ name: 'HMAC' },
|
||||
hmacKey,
|
||||
toSign,
|
||||
);
|
||||
|
@ -193,7 +181,6 @@ export async function encryptMegolmKeyFile(data, password, options) {
|
|||
throw friendlyError('subtleCrypto.sign failed: ' + e, cryptoFailMsg());
|
||||
}
|
||||
|
||||
|
||||
const hmacArray = new Uint8Array(hmac);
|
||||
resultBuffer.set(hmacArray, idx);
|
||||
return packMegolmKeyFile(resultBuffer);
|
||||
|
@ -215,7 +202,7 @@ async function deriveKeys(salt, iterations, password) {
|
|||
key = await subtleCrypto.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(password),
|
||||
{name: 'PBKDF2'},
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveBits'],
|
||||
);
|
||||
|
@ -248,7 +235,7 @@ async function deriveKeys(salt, iterations, password) {
|
|||
const aesProm = subtleCrypto.importKey(
|
||||
'raw',
|
||||
aesKey,
|
||||
{name: 'AES-CTR'},
|
||||
{ name: 'AES-CTR' },
|
||||
false,
|
||||
['encrypt', 'decrypt'],
|
||||
).catch((e) => {
|
||||
|
@ -260,7 +247,7 @@ async function deriveKeys(salt, iterations, password) {
|
|||
hmacKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: {name: 'SHA-256'},
|
||||
hash: { name: 'SHA-256' },
|
||||
},
|
||||
false,
|
||||
['sign', 'verify'],
|
||||
|
@ -310,8 +297,7 @@ function unpackMegolmKeyFile(data) {
|
|||
// look for the end line
|
||||
while (1) {
|
||||
const lineEnd = fileStr.indexOf('\n', lineStart);
|
||||
const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd)
|
||||
.trim();
|
||||
const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd).trim();
|
||||
if (line === TRAILER_LINE) {
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,31 +14,33 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import DiffMatchPatch from 'diff-match-patch';
|
||||
import {DiffDOM} from "diff-dom";
|
||||
import { checkBlockNode, bodyToHtml } from "../HtmlUtils";
|
||||
import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch';
|
||||
import { DiffDOM, IDiff } from "diff-dom";
|
||||
import { IContent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { bodyToHtml, checkBlockNode, IOptsReturnString } from "../HtmlUtils";
|
||||
|
||||
const decodeEntities = (function() {
|
||||
let textarea = null;
|
||||
return function(string) {
|
||||
return function(str: string): string {
|
||||
if (!textarea) {
|
||||
textarea = document.createElement("textarea");
|
||||
}
|
||||
textarea.innerHTML = string;
|
||||
textarea.innerHTML = str;
|
||||
return textarea.value;
|
||||
};
|
||||
})();
|
||||
|
||||
function textToHtml(text) {
|
||||
function textToHtml(text: string): string {
|
||||
const container = document.createElement("div");
|
||||
container.textContent = text;
|
||||
return container.innerHTML;
|
||||
}
|
||||
|
||||
function getSanitizedHtmlBody(content) {
|
||||
const opts = {
|
||||
function getSanitizedHtmlBody(content: IContent): string {
|
||||
const opts: IOptsReturnString = {
|
||||
stripReplyFallback: true,
|
||||
returnString: true,
|
||||
};
|
||||
|
@ -57,31 +59,29 @@ function getSanitizedHtmlBody(content) {
|
|||
}
|
||||
}
|
||||
|
||||
function wrapInsertion(child) {
|
||||
function wrapInsertion(child: Node): HTMLElement {
|
||||
const wrapper = document.createElement(checkBlockNode(child) ? "div" : "span");
|
||||
wrapper.className = "mx_EditHistoryMessage_insertion";
|
||||
wrapper.appendChild(child);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function wrapDeletion(child) {
|
||||
function wrapDeletion(child: Node): HTMLElement {
|
||||
const wrapper = document.createElement(checkBlockNode(child) ? "div" : "span");
|
||||
wrapper.className = "mx_EditHistoryMessage_deletion";
|
||||
wrapper.appendChild(child);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function findRefNodes(root, route, isAddition) {
|
||||
function findRefNodes(root: Node, route: number[], isAddition = false) {
|
||||
let refNode = root;
|
||||
let refParentNode;
|
||||
const end = isAddition ? route.length - 1 : route.length;
|
||||
for (let i = 0; i < end; ++i) {
|
||||
refParentNode = refNode;
|
||||
// Lists don't have appropriate child nodes we can use.
|
||||
if (!refNode.childNodes[route[i]]) continue;
|
||||
refNode = refNode.childNodes[route[i]];
|
||||
}
|
||||
return {refNode, refParentNode};
|
||||
return { refNode, refParentNode };
|
||||
}
|
||||
|
||||
function diffTreeToDOM(desc) {
|
||||
|
@ -103,7 +103,7 @@ function diffTreeToDOM(desc) {
|
|||
}
|
||||
}
|
||||
|
||||
function insertBefore(parent, nextSibling, child) {
|
||||
function insertBefore(parent: Node, nextSibling: Node | null, child: Node): void {
|
||||
if (nextSibling) {
|
||||
parent.insertBefore(child, nextSibling);
|
||||
} else {
|
||||
|
@ -111,7 +111,7 @@ function insertBefore(parent, nextSibling, child) {
|
|||
}
|
||||
}
|
||||
|
||||
function isRouteOfNextSibling(route1, route2) {
|
||||
function isRouteOfNextSibling(route1: number[], route2: number[]): boolean {
|
||||
// routes are arrays with indices,
|
||||
// to be interpreted as a path in the dom tree
|
||||
|
||||
|
@ -129,7 +129,7 @@ function isRouteOfNextSibling(route1, route2) {
|
|||
return route2[lastD1Idx] >= route1[lastD1Idx];
|
||||
}
|
||||
|
||||
function adjustRoutes(diff, remainingDiffs) {
|
||||
function adjustRoutes(diff: IDiff, remainingDiffs: IDiff[]): void {
|
||||
if (diff.action === "removeTextElement" || diff.action === "removeElement") {
|
||||
// as removed text is not removed from the html, but marked as deleted,
|
||||
// we need to readjust indices that assume the current node has been removed.
|
||||
|
@ -142,12 +142,12 @@ function adjustRoutes(diff, remainingDiffs) {
|
|||
}
|
||||
}
|
||||
|
||||
function stringAsTextNode(string) {
|
||||
function stringAsTextNode(string: string): Text {
|
||||
return document.createTextNode(decodeEntities(string));
|
||||
}
|
||||
|
||||
function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) {
|
||||
const {refNode, refParentNode} = findRefNodes(originalRootNode, diff.route);
|
||||
function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatch: DiffMatchPatch): void {
|
||||
const { refNode, refParentNode } = findRefNodes(originalRootNode, diff.route);
|
||||
switch (diff.action) {
|
||||
case "replaceElement": {
|
||||
const container = document.createElement("span");
|
||||
|
@ -173,7 +173,7 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) {
|
|||
diffMathPatch.diff_cleanupSemantic(textDiffs);
|
||||
const container = document.createElement("span");
|
||||
for (const [modifier, text] of textDiffs) {
|
||||
let textDiffNode = stringAsTextNode(text);
|
||||
let textDiffNode: Node = stringAsTextNode(text);
|
||||
if (modifier < 0) {
|
||||
textDiffNode = wrapDeletion(textDiffNode);
|
||||
} else if (modifier > 0) {
|
||||
|
@ -190,10 +190,11 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) {
|
|||
break;
|
||||
}
|
||||
case "addTextElement": {
|
||||
if (diff.value !== "\n") {
|
||||
const insNode = wrapInsertion(stringAsTextNode(diff.value));
|
||||
insertBefore(refParentNode, refNode, insNode);
|
||||
}
|
||||
// XXX: sometimes diffDOM says insert a newline when there shouldn't be one
|
||||
// but we must insert the node anyway so that we don't break the route child IDs.
|
||||
// See https://github.com/fiduswriter/diffDOM/issues/100
|
||||
const insNode = wrapInsertion(stringAsTextNode(diff.value !== "\n" ? diff.value : ""));
|
||||
insertBefore(refParentNode, refNode, insNode);
|
||||
break;
|
||||
}
|
||||
// e.g. when changing a the href of a link,
|
||||
|
@ -202,7 +203,7 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) {
|
|||
case "addAttribute":
|
||||
case "modifyAttribute": {
|
||||
const delNode = wrapDeletion(refNode.cloneNode(true));
|
||||
const updatedNode = refNode.cloneNode(true);
|
||||
const updatedNode = refNode.cloneNode(true) as HTMLElement;
|
||||
if (diff.action === "addAttribute" || diff.action === "modifyAttribute") {
|
||||
updatedNode.setAttribute(diff.name, diff.newValue);
|
||||
} else {
|
||||
|
@ -221,12 +222,12 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) {
|
|||
}
|
||||
}
|
||||
|
||||
function routeIsEqual(r1, r2) {
|
||||
function routeIsEqual(r1: number[], r2: number[]): boolean {
|
||||
return r1.length === r2.length && !r1.some((e, i) => e !== r2[i]);
|
||||
}
|
||||
|
||||
// workaround for https://github.com/fiduswriter/diffDOM/issues/90
|
||||
function filterCancelingOutDiffs(originalDiffActions) {
|
||||
function filterCancelingOutDiffs(originalDiffActions: IDiff[]): IDiff[] {
|
||||
const diffActions = originalDiffActions.slice();
|
||||
|
||||
for (let i = 0; i < diffActions.length; ++i) {
|
||||
|
@ -253,7 +254,7 @@ function filterCancelingOutDiffs(originalDiffActions) {
|
|||
* @param {object} editContent the content for the edit message
|
||||
* @return {object} a react element similar to what `bodyToHtml` returns
|
||||
*/
|
||||
export function editBodyDiffToHtml(originalContent, editContent) {
|
||||
export function editBodyDiffToHtml(originalContent: IContent, editContent: IContent): ReactNode {
|
||||
// wrap the body in a div, DiffDOM needs a root element
|
||||
const originalBody = `<div>${getSanitizedHtmlBody(originalContent)}</div>`;
|
||||
const editBody = `<div>${getSanitizedHtmlBody(editContent)}</div>`;
|
50
src/utils/Mouse.ts
Normal file
50
src/utils/Mouse.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Different browsers use different deltaModes. This causes different behaviour.
|
||||
* To avoid that we use this function to convert any event to pixels.
|
||||
* @param {WheelEvent} event to normalize
|
||||
* @returns {WheelEvent} normalized event event
|
||||
*/
|
||||
export function normalizeWheelEvent(event: WheelEvent): WheelEvent {
|
||||
const LINE_HEIGHT = 18;
|
||||
|
||||
let deltaX;
|
||||
let deltaY;
|
||||
let deltaZ;
|
||||
|
||||
if (event.deltaMode === 1) { // Units are lines
|
||||
deltaX = (event.deltaX * LINE_HEIGHT);
|
||||
deltaY = (event.deltaY * LINE_HEIGHT);
|
||||
deltaZ = (event.deltaZ * LINE_HEIGHT);
|
||||
} else {
|
||||
deltaX = event.deltaX;
|
||||
deltaY = event.deltaY;
|
||||
deltaZ = event.deltaZ;
|
||||
}
|
||||
|
||||
return new WheelEvent(
|
||||
"syntheticWheel",
|
||||
{
|
||||
deltaMode: 0,
|
||||
deltaY: deltaY,
|
||||
deltaX: deltaX,
|
||||
deltaZ: deltaZ,
|
||||
...event,
|
||||
},
|
||||
);
|
||||
}
|
|
@ -1,268 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import {getAddressType} from '../UserAddress';
|
||||
import GroupStore from '../stores/GroupStore';
|
||||
import {_t} from "../languageHandler";
|
||||
import * as sdk from "../index";
|
||||
import Modal from "../Modal";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import {defer} from "./promise";
|
||||
|
||||
/**
|
||||
* Invites multiple addresses to a room or group, handling rate limiting from the server
|
||||
*/
|
||||
export default class MultiInviter {
|
||||
/**
|
||||
* @param {string} targetId The ID of the room or group to invite to
|
||||
*/
|
||||
constructor(targetId) {
|
||||
if (targetId[0] === '+') {
|
||||
this.roomId = null;
|
||||
this.groupId = targetId;
|
||||
} else {
|
||||
this.roomId = targetId;
|
||||
this.groupId = null;
|
||||
}
|
||||
|
||||
this.canceled = false;
|
||||
this.addrs = [];
|
||||
this.busy = false;
|
||||
this.completionStates = {}; // State of each address (invited or error)
|
||||
this.errors = {}; // { address: {errorText, errcode} }
|
||||
this.deferred = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite users to this room. This may only be called once per
|
||||
* instance of the class.
|
||||
*
|
||||
* @param {array} addrs Array of addresses to invite
|
||||
* @returns {Promise} Resolved when all invitations in the queue are complete
|
||||
*/
|
||||
invite(addrs) {
|
||||
if (this.addrs.length > 0) {
|
||||
throw new Error("Already inviting/invited");
|
||||
}
|
||||
this.addrs.push(...addrs);
|
||||
|
||||
for (const addr of this.addrs) {
|
||||
if (getAddressType(addr) === null) {
|
||||
this.completionStates[addr] = 'error';
|
||||
this.errors[addr] = {
|
||||
errcode: 'M_INVALID',
|
||||
errorText: _t('Unrecognised address'),
|
||||
};
|
||||
}
|
||||
}
|
||||
this.deferred = defer();
|
||||
this._inviteMore(0);
|
||||
|
||||
return this.deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops inviting. Causes promises returned by invite() to be rejected.
|
||||
*/
|
||||
cancel() {
|
||||
if (!this.busy) return;
|
||||
|
||||
this._canceled = true;
|
||||
this.deferred.reject(new Error('canceled'));
|
||||
}
|
||||
|
||||
getCompletionState(addr) {
|
||||
return this.completionStates[addr];
|
||||
}
|
||||
|
||||
getErrorText(addr) {
|
||||
return this.errors[addr] ? this.errors[addr].errorText : null;
|
||||
}
|
||||
|
||||
async _inviteToRoom(roomId, addr, ignoreProfile) {
|
||||
const addrType = getAddressType(addr);
|
||||
|
||||
if (addrType === 'email') {
|
||||
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
||||
} else if (addrType === 'mx-user-id') {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) throw new Error("Room not found");
|
||||
|
||||
const member = room.getMember(addr);
|
||||
if (member && ['join', 'invite'].includes(member.membership)) {
|
||||
throw {errcode: "RIOT.ALREADY_IN_ROOM", error: "Member already invited"};
|
||||
}
|
||||
|
||||
if (!ignoreProfile && SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) {
|
||||
try {
|
||||
const profile = await MatrixClientPeg.get().getProfileInfo(addr);
|
||||
if (!profile) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("User has no profile");
|
||||
}
|
||||
} catch (e) {
|
||||
throw {
|
||||
errcode: "RIOT.USER_NOT_FOUND",
|
||||
error: "User does not have a profile or does not exist."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return MatrixClientPeg.get().invite(roomId, addr);
|
||||
} else {
|
||||
throw new Error('Unsupported address');
|
||||
}
|
||||
}
|
||||
|
||||
_doInvite(address, ignoreProfile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`Inviting ${address}`);
|
||||
|
||||
let doInvite;
|
||||
if (this.groupId !== null) {
|
||||
doInvite = GroupStore.inviteUserToGroup(this.groupId, address);
|
||||
} else {
|
||||
doInvite = this._inviteToRoom(this.roomId, address, ignoreProfile);
|
||||
}
|
||||
|
||||
doInvite.then(() => {
|
||||
if (this._canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.completionStates[address] = 'invited';
|
||||
delete this.errors[address];
|
||||
|
||||
resolve();
|
||||
}).catch((err) => {
|
||||
if (this._canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
|
||||
let errorText;
|
||||
let fatal = false;
|
||||
if (err.errcode === 'M_FORBIDDEN') {
|
||||
fatal = true;
|
||||
errorText = _t('You do not have permission to invite people to this room.');
|
||||
} else if (err.errcode === "RIOT.ALREADY_IN_ROOM") {
|
||||
errorText = _t("User %(userId)s is already in the room", {userId: address});
|
||||
} else if (err.errcode === 'M_LIMIT_EXCEEDED') {
|
||||
// we're being throttled so wait a bit & try again
|
||||
setTimeout(() => {
|
||||
this._doInvite(address, ignoreProfile).then(resolve, reject);
|
||||
}, 5000);
|
||||
return;
|
||||
} else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'RIOT.USER_NOT_FOUND'].includes(err.errcode)) {
|
||||
errorText = _t("User %(user_id)s does not exist", {user_id: address});
|
||||
} else if (err.errcode === 'M_PROFILE_UNDISCLOSED') {
|
||||
errorText = _t("User %(user_id)s may or may not exist", {user_id: address});
|
||||
} else if (err.errcode === 'M_PROFILE_NOT_FOUND' && !ignoreProfile) {
|
||||
// Invite without the profile check
|
||||
console.warn(`User ${address} does not have a profile - inviting anyways automatically`);
|
||||
this._doInvite(address, true).then(resolve, reject);
|
||||
} else if (err.errcode === "M_BAD_STATE") {
|
||||
errorText = _t("The user must be unbanned before they can be invited.");
|
||||
} else if (err.errcode === "M_UNSUPPORTED_ROOM_VERSION") {
|
||||
errorText = _t("The user's homeserver does not support the version of the room.");
|
||||
} else {
|
||||
errorText = _t('Unknown server error');
|
||||
}
|
||||
|
||||
this.completionStates[address] = 'error';
|
||||
this.errors[address] = {errorText, errcode: err.errcode};
|
||||
|
||||
this.busy = !fatal;
|
||||
this.fatal = fatal;
|
||||
|
||||
if (fatal) {
|
||||
reject();
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_inviteMore(nextIndex, ignoreProfile) {
|
||||
if (this._canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextIndex === this.addrs.length) {
|
||||
this.busy = false;
|
||||
if (Object.keys(this.errors).length > 0 && !this.groupId) {
|
||||
// There were problems inviting some people - see if we can invite them
|
||||
// without caring if they exist or not.
|
||||
const unknownProfileErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND', 'RIOT.USER_NOT_FOUND'];
|
||||
const unknownProfileUsers = Object.keys(this.errors).filter(a => unknownProfileErrors.includes(this.errors[a].errcode));
|
||||
|
||||
if (unknownProfileUsers.length > 0) {
|
||||
const inviteUnknowns = () => {
|
||||
const promises = unknownProfileUsers.map(u => this._doInvite(u, true));
|
||||
Promise.all(promises).then(() => this.deferred.resolve(this.completionStates));
|
||||
};
|
||||
|
||||
if (!SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) {
|
||||
inviteUnknowns();
|
||||
return;
|
||||
}
|
||||
|
||||
const AskInviteAnywayDialog = sdk.getComponent("dialogs.AskInviteAnywayDialog");
|
||||
console.log("Showing failed to invite dialog...");
|
||||
Modal.createTrackedDialog('Failed to invite the following users to the room', '', AskInviteAnywayDialog, {
|
||||
unknownProfileUsers: unknownProfileUsers.map(u => {return {userId: u, errorText: this.errors[u].errorText};}),
|
||||
onInviteAnyways: () => inviteUnknowns(),
|
||||
onGiveUp: () => {
|
||||
// Fake all the completion states because we already warned the user
|
||||
for (const addr of unknownProfileUsers) {
|
||||
this.completionStates[addr] = 'invited';
|
||||
}
|
||||
this.deferred.resolve(this.completionStates);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.deferred.resolve(this.completionStates);
|
||||
return;
|
||||
}
|
||||
|
||||
const addr = this.addrs[nextIndex];
|
||||
|
||||
// don't try to invite it if it's an invalid address
|
||||
// (it will already be marked as an error though,
|
||||
// so no need to do so again)
|
||||
if (getAddressType(addr) === null) {
|
||||
this._inviteMore(nextIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// don't re-invite (there's no way in the UI to do this, but
|
||||
// for sanity's sake)
|
||||
if (this.completionStates[addr] === 'invited') {
|
||||
this._inviteMore(nextIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
this._doInvite(addr, ignoreProfile).then(() => {
|
||||
this._inviteMore(nextIndex + 1, ignoreProfile);
|
||||
}).catch(() => this.deferred.resolve(this.completionStates));
|
||||
}
|
||||
}
|
317
src/utils/MultiInviter.ts
Normal file
317
src/utils/MultiInviter.ts
Normal file
|
@ -0,0 +1,317 @@
|
|||
/*
|
||||
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
import { defer, IDeferred } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import { AddressType, getAddressType } from '../UserAddress';
|
||||
import GroupStore from '../stores/GroupStore';
|
||||
import { _t } from "../languageHandler";
|
||||
import Modal from "../Modal";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import AskInviteAnywayDialog from "../components/views/dialogs/AskInviteAnywayDialog";
|
||||
|
||||
export enum InviteState {
|
||||
Invited = "invited",
|
||||
Error = "error",
|
||||
}
|
||||
|
||||
interface IError {
|
||||
errorText: string;
|
||||
errcode: string;
|
||||
}
|
||||
|
||||
const UNKNOWN_PROFILE_ERRORS = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND'];
|
||||
|
||||
export type CompletionStates = Record<string, InviteState>;
|
||||
|
||||
const USER_ALREADY_JOINED = "IO.ELEMENT.ALREADY_JOINED";
|
||||
const USER_ALREADY_INVITED = "IO.ELEMENT.ALREADY_INVITED";
|
||||
|
||||
/**
|
||||
* Invites multiple addresses to a room or group, handling rate limiting from the server
|
||||
*/
|
||||
export default class MultiInviter {
|
||||
private readonly roomId?: string;
|
||||
private readonly groupId?: string;
|
||||
|
||||
private canceled = false;
|
||||
private addresses: string[] = [];
|
||||
private busy = false;
|
||||
private _fatal = false;
|
||||
private completionStates: CompletionStates = {}; // State of each address (invited or error)
|
||||
private errors: Record<string, IError> = {}; // { address: {errorText, errcode} }
|
||||
private deferred: IDeferred<CompletionStates> = null;
|
||||
private reason: string = null;
|
||||
|
||||
/**
|
||||
* @param {string} targetId The ID of the room or group to invite to
|
||||
*/
|
||||
constructor(targetId: string) {
|
||||
if (targetId[0] === '+') {
|
||||
this.roomId = null;
|
||||
this.groupId = targetId;
|
||||
} else {
|
||||
this.roomId = targetId;
|
||||
this.groupId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public get fatal() {
|
||||
return this._fatal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite users to this room. This may only be called once per
|
||||
* instance of the class.
|
||||
*
|
||||
* @param {array} addresses Array of addresses to invite
|
||||
* @param {string} reason Reason for inviting (optional)
|
||||
* @returns {Promise} Resolved when all invitations in the queue are complete
|
||||
*/
|
||||
public invite(addresses, reason?: string): Promise<CompletionStates> {
|
||||
if (this.addresses.length > 0) {
|
||||
throw new Error("Already inviting/invited");
|
||||
}
|
||||
this.addresses.push(...addresses);
|
||||
this.reason = reason;
|
||||
|
||||
for (const addr of this.addresses) {
|
||||
if (getAddressType(addr) === null) {
|
||||
this.completionStates[addr] = InviteState.Error;
|
||||
this.errors[addr] = {
|
||||
errcode: 'M_INVALID',
|
||||
errorText: _t('Unrecognised address'),
|
||||
};
|
||||
}
|
||||
}
|
||||
this.deferred = defer<CompletionStates>();
|
||||
this.inviteMore(0);
|
||||
|
||||
return this.deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops inviting. Causes promises returned by invite() to be rejected.
|
||||
*/
|
||||
public cancel(): void {
|
||||
if (!this.busy) return;
|
||||
|
||||
this.canceled = true;
|
||||
this.deferred.reject(new Error('canceled'));
|
||||
}
|
||||
|
||||
public getCompletionState(addr: string): InviteState {
|
||||
return this.completionStates[addr];
|
||||
}
|
||||
|
||||
public getErrorText(addr: string): string {
|
||||
return this.errors[addr] ? this.errors[addr].errorText : null;
|
||||
}
|
||||
|
||||
private async inviteToRoom(roomId: string, addr: string, ignoreProfile = false): Promise<{}> {
|
||||
const addrType = getAddressType(addr);
|
||||
|
||||
if (addrType === AddressType.Email) {
|
||||
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
||||
} else if (addrType === AddressType.MatrixUserId) {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) throw new Error("Room not found");
|
||||
|
||||
const member = room.getMember(addr);
|
||||
if (member?.membership === "join") {
|
||||
throw new MatrixError({
|
||||
errcode: USER_ALREADY_JOINED,
|
||||
error: "Member already joined",
|
||||
});
|
||||
} else if (member?.membership === "invite") {
|
||||
throw new MatrixError({
|
||||
errcode: USER_ALREADY_INVITED,
|
||||
error: "Member already invited",
|
||||
});
|
||||
}
|
||||
|
||||
if (!ignoreProfile && SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) {
|
||||
const profile = await MatrixClientPeg.get().getProfileInfo(addr);
|
||||
if (!profile) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("User has no profile");
|
||||
}
|
||||
}
|
||||
|
||||
return MatrixClientPeg.get().invite(roomId, addr, undefined, this.reason);
|
||||
} else {
|
||||
throw new Error('Unsupported address');
|
||||
}
|
||||
}
|
||||
|
||||
private doInvite(address: string, ignoreProfile = false): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
console.log(`Inviting ${address}`);
|
||||
|
||||
let doInvite;
|
||||
if (this.groupId !== null) {
|
||||
doInvite = GroupStore.inviteUserToGroup(this.groupId, address);
|
||||
} else {
|
||||
doInvite = this.inviteToRoom(this.roomId, address, ignoreProfile);
|
||||
}
|
||||
|
||||
doInvite.then(() => {
|
||||
if (this.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.completionStates[address] = InviteState.Invited;
|
||||
delete this.errors[address];
|
||||
|
||||
resolve();
|
||||
}).catch((err) => {
|
||||
if (this.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
|
||||
let errorText;
|
||||
let fatal = false;
|
||||
switch (err.errcode) {
|
||||
case "M_FORBIDDEN":
|
||||
errorText = _t('You do not have permission to invite people to this room.');
|
||||
fatal = true;
|
||||
break;
|
||||
case USER_ALREADY_INVITED:
|
||||
errorText = _t("User %(userId)s is already invited to the room", { userId: address });
|
||||
break;
|
||||
case USER_ALREADY_JOINED:
|
||||
errorText = _t("User %(userId)s is already in the room", { userId: address });
|
||||
break;
|
||||
case "M_LIMIT_EXCEEDED":
|
||||
// we're being throttled so wait a bit & try again
|
||||
setTimeout(() => {
|
||||
this.doInvite(address, ignoreProfile).then(resolve, reject);
|
||||
}, 5000);
|
||||
return;
|
||||
case "M_NOT_FOUND":
|
||||
case "M_USER_NOT_FOUND":
|
||||
errorText = _t("User %(user_id)s does not exist", { user_id: address });
|
||||
break;
|
||||
case "M_PROFILE_UNDISCLOSED":
|
||||
errorText = _t("User %(user_id)s may or may not exist", { user_id: address });
|
||||
break;
|
||||
case "M_PROFILE_NOT_FOUND":
|
||||
if (!ignoreProfile) {
|
||||
// Invite without the profile check
|
||||
console.warn(`User ${address} does not have a profile - inviting anyways automatically`);
|
||||
this.doInvite(address, true).then(resolve, reject);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "M_BAD_STATE":
|
||||
errorText = _t("The user must be unbanned before they can be invited.");
|
||||
break;
|
||||
case "M_UNSUPPORTED_ROOM_VERSION":
|
||||
errorText = _t("The user's homeserver does not support the version of the room.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!errorText) {
|
||||
errorText = _t('Unknown server error');
|
||||
}
|
||||
|
||||
this.completionStates[address] = InviteState.Error;
|
||||
this.errors[address] = { errorText, errcode: err.errcode };
|
||||
|
||||
this.busy = !fatal;
|
||||
this._fatal = fatal;
|
||||
|
||||
if (fatal) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private inviteMore(nextIndex: number, ignoreProfile = false): void {
|
||||
if (this.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextIndex === this.addresses.length) {
|
||||
this.busy = false;
|
||||
if (Object.keys(this.errors).length > 0 && !this.groupId) {
|
||||
// There were problems inviting some people - see if we can invite them
|
||||
// without caring if they exist or not.
|
||||
const unknownProfileUsers = Object.keys(this.errors)
|
||||
.filter(a => UNKNOWN_PROFILE_ERRORS.includes(this.errors[a].errcode));
|
||||
|
||||
if (unknownProfileUsers.length > 0) {
|
||||
const inviteUnknowns = () => {
|
||||
const promises = unknownProfileUsers.map(u => this.doInvite(u, true));
|
||||
Promise.all(promises).then(() => this.deferred.resolve(this.completionStates));
|
||||
};
|
||||
|
||||
if (!SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) {
|
||||
inviteUnknowns();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Showing failed to invite dialog...");
|
||||
Modal.createTrackedDialog('Failed to invite', '', AskInviteAnywayDialog, {
|
||||
unknownProfileUsers: unknownProfileUsers.map(u => ({
|
||||
userId: u,
|
||||
errorText: this.errors[u].errorText,
|
||||
})),
|
||||
onInviteAnyways: () => inviteUnknowns(),
|
||||
onGiveUp: () => {
|
||||
// Fake all the completion states because we already warned the user
|
||||
for (const addr of unknownProfileUsers) {
|
||||
this.completionStates[addr] = InviteState.Invited;
|
||||
}
|
||||
this.deferred.resolve(this.completionStates);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.deferred.resolve(this.completionStates);
|
||||
return;
|
||||
}
|
||||
|
||||
const addr = this.addresses[nextIndex];
|
||||
|
||||
// don't try to invite it if it's an invalid address
|
||||
// (it will already be marked as an error though,
|
||||
// so no need to do so again)
|
||||
if (getAddressType(addr) === null) {
|
||||
this.inviteMore(nextIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// don't re-invite (there's no way in the UI to do this, but
|
||||
// for sanity's sake)
|
||||
if (this.completionStates[addr] === InviteState.Invited) {
|
||||
this.inviteMore(nextIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
this.doInvite(addr, ignoreProfile).then(() => {
|
||||
this.inviteMore(nextIndex + 1, ignoreProfile);
|
||||
}).catch(() => this.deferred.resolve(this.completionStates));
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import zxcvbn from 'zxcvbn';
|
||||
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import { _t, _td } from '../languageHandler';
|
||||
|
||||
const ZXCVBN_USER_INPUTS = [
|
||||
|
@ -63,7 +63,7 @@ _td("Short keyboard patterns are easy to guess");
|
|||
* @param {string} password Password to score
|
||||
* @returns {object} Score result with `score` and `feedback` properties
|
||||
*/
|
||||
export function scorePassword(password) {
|
||||
export function scorePassword(password: string) {
|
||||
if (password.length === 0) return null;
|
||||
|
||||
const userInputs = ZXCVBN_USER_INPUTS.slice();
|
|
@ -14,13 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
export default class PinningUtils {
|
||||
/**
|
||||
* Determines if the given event may be pinned.
|
||||
* @param {MatrixEvent} event The event to check.
|
||||
* @return {boolean} True if the event may be pinned, false otherwise.
|
||||
*/
|
||||
static isPinnable(event) {
|
||||
static isPinnable(event: MatrixEvent): boolean {
|
||||
if (!event) return false;
|
||||
if (event.getType() !== "m.room.message") return false;
|
||||
if (event.isRedacted()) return false;
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,12 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
/**
|
||||
* Given MatrixEvent containing receipts, return the first
|
||||
* read receipt from the given user ID, or null if no such
|
||||
* receipt exists.
|
||||
*
|
||||
* @param {Object} receiptEvent A Matrix Event
|
||||
* @param {string} userId A user ID
|
||||
* @returns {Object} Read receipt
|
||||
*/
|
||||
export function findReadReceiptFromUserId(receiptEvent, userId) {
|
||||
export function findReadReceiptFromUserId(receiptEvent: MatrixEvent, userId: string): object | null {
|
||||
const receiptKeys = Object.keys(receiptEvent.getContent());
|
||||
for (let i = 0; i < receiptKeys.length; ++i) {
|
||||
const rcpt = receiptEvent.getContent()[receiptKeys[i]];
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when the middle panel has been resized.
|
||||
* @event module:utils~ResizeNotifier#"middlePanelResized"
|
||||
*/
|
||||
import { EventEmitter } from "events";
|
||||
import { throttle } from "lodash";
|
||||
|
||||
export default class ResizeNotifier extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
// with default options, will call fn once at first call, and then every x ms
|
||||
// if there was another call in that timespan
|
||||
this._throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200);
|
||||
}
|
||||
|
||||
notifyBannersChanged() {
|
||||
this.emit("leftPanelResized");
|
||||
this.emit("middlePanelResized");
|
||||
}
|
||||
|
||||
// can be called in quick succession
|
||||
notifyLeftHandleResized() {
|
||||
// don't emit event for own region
|
||||
this._throttledMiddlePanel();
|
||||
}
|
||||
|
||||
// can be called in quick succession
|
||||
notifyRightHandleResized() {
|
||||
this._throttledMiddlePanel();
|
||||
}
|
||||
|
||||
// can be called in quick succession
|
||||
notifyWindowResized() {
|
||||
// no need to throttle this one,
|
||||
// also it could make scrollbars appear for
|
||||
// a split second when the room list manual layout is now
|
||||
// taller than the available space
|
||||
this.emit("leftPanelResized");
|
||||
|
||||
this._throttledMiddlePanel();
|
||||
}
|
||||
}
|
||||
|
79
src/utils/ResizeNotifier.ts
Normal file
79
src/utils/ResizeNotifier.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when the middle panel has been resized (throttled).
|
||||
* @event module:utils~ResizeNotifier#"middlePanelResized"
|
||||
*/
|
||||
/**
|
||||
* Fires when the middle panel has been resized by a pixel.
|
||||
* @event module:utils~ResizeNotifier#"middlePanelResizedNoisy"
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { throttle } from "lodash";
|
||||
|
||||
export default class ResizeNotifier extends EventEmitter {
|
||||
private _isResizing = false;
|
||||
|
||||
// with default options, will call fn once at first call, and then every x ms
|
||||
// if there was another call in that timespan
|
||||
private throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200);
|
||||
|
||||
public get isResizing() {
|
||||
return this._isResizing;
|
||||
}
|
||||
|
||||
public startResizing() {
|
||||
this._isResizing = true;
|
||||
this.emit("isResizing", true);
|
||||
}
|
||||
|
||||
public stopResizing() {
|
||||
this._isResizing = false;
|
||||
this.emit("isResizing", false);
|
||||
}
|
||||
|
||||
private noisyMiddlePanel() {
|
||||
this.emit("middlePanelResizedNoisy");
|
||||
}
|
||||
|
||||
private updateMiddlePanel() {
|
||||
this.throttledMiddlePanel();
|
||||
this.noisyMiddlePanel();
|
||||
}
|
||||
|
||||
// can be called in quick succession
|
||||
public notifyLeftHandleResized() {
|
||||
// don't emit event for own region
|
||||
this.updateMiddlePanel();
|
||||
}
|
||||
|
||||
// can be called in quick succession
|
||||
public notifyRightHandleResized() {
|
||||
this.updateMiddlePanel();
|
||||
}
|
||||
|
||||
public notifyTimelineHeightChanged() {
|
||||
this.updateMiddlePanel();
|
||||
}
|
||||
|
||||
// can be called in quick succession
|
||||
public notifyWindowResized() {
|
||||
this.updateMiddlePanel();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,32 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import DMRoomMap from './DMRoomMap';
|
||||
|
||||
/* For now, a cut-down type spec for the client */
|
||||
interface Client {
|
||||
getUserId: () => string;
|
||||
checkUserTrust: (userId: string) => {
|
||||
isCrossSigningVerified: () => boolean
|
||||
wasCrossSigningVerified: () => boolean
|
||||
};
|
||||
getStoredDevicesForUser: (userId: string) => Promise<[{ deviceId: string }]>;
|
||||
checkDeviceTrust: (userId: string, deviceId: string) => {
|
||||
isVerified: () => boolean
|
||||
}
|
||||
export enum E2EStatus {
|
||||
Warning = "warning",
|
||||
Verified = "verified",
|
||||
Normal = "normal"
|
||||
}
|
||||
|
||||
interface Room {
|
||||
getEncryptionTargetMembers: () => Promise<[{userId: string}]>;
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
export async function shieldStatusForRoom(client: Client, room: Room): Promise<string> {
|
||||
const members = (await room.getEncryptionTargetMembers()).map(({userId}) => userId);
|
||||
export async function shieldStatusForRoom(client: MatrixClient, room: Room): Promise<E2EStatus> {
|
||||
const members = (await room.getEncryptionTargetMembers()).map(({ userId }) => userId);
|
||||
const inDMMap = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
|
||||
const verified: string[] = [];
|
||||
|
@ -27,13 +34,13 @@ export async function shieldStatusForRoom(client: Client, room: Room): Promise<s
|
|||
members.filter((userId) => userId !== client.getUserId())
|
||||
.forEach((userId) => {
|
||||
(client.checkUserTrust(userId).isCrossSigningVerified() ?
|
||||
verified : unverified).push(userId);
|
||||
verified : unverified).push(userId);
|
||||
});
|
||||
|
||||
/* Alarm if any unverified users were verified before. */
|
||||
for (const userId of unverified) {
|
||||
if (client.checkUserTrust(userId).wasCrossSigningVerified()) {
|
||||
return "warning";
|
||||
return E2EStatus.Warning;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,14 +52,14 @@ export async function shieldStatusForRoom(client: Client, room: Room): Promise<s
|
|||
(members.length === 1); // Do alarm for self if we're alone in a room
|
||||
const targets = includeUser ? [...verified, client.getUserId()] : verified;
|
||||
for (const userId of targets) {
|
||||
const devices = await client.getStoredDevicesForUser(userId);
|
||||
const anyDeviceNotVerified = devices.some(({deviceId}) => {
|
||||
const devices = client.getStoredDevicesForUser(userId);
|
||||
const anyDeviceNotVerified = devices.some(({ deviceId }) => {
|
||||
return !client.checkDeviceTrust(userId, deviceId).isVerified();
|
||||
});
|
||||
if (anyDeviceNotVerified) {
|
||||
return "warning";
|
||||
return E2EStatus.Warning;
|
||||
}
|
||||
}
|
||||
|
||||
return unverified.length === 0 ? "verified" : "normal";
|
||||
return unverified.length === 0 ? E2EStatus.Verified : E2EStatus.Normal;
|
||||
}
|
||||
|
|
126
src/utils/Singleflight.ts
Normal file
126
src/utils/Singleflight.ts
Normal file
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EnhancedMap } from "./maps";
|
||||
|
||||
// Inspired by https://pkg.go.dev/golang.org/x/sync/singleflight
|
||||
|
||||
const keyMap = new EnhancedMap<Object, EnhancedMap<string, unknown>>();
|
||||
|
||||
/**
|
||||
* Access class to get a singleflight context. Singleflights execute a
|
||||
* function exactly once, unless instructed to forget about a result.
|
||||
*
|
||||
* Typically this is used to de-duplicate an action, such as a save button
|
||||
* being pressed, without having to track state internally for an operation
|
||||
* already being in progress. This doesn't expose a flag which can be used
|
||||
* to disable a button, however it would be capable of returning a Promise
|
||||
* from the first call.
|
||||
*
|
||||
* The result of the function call is cached indefinitely, just in case a
|
||||
* second call comes through late. There are various functions named "forget"
|
||||
* to have the cache be cleared of a result.
|
||||
*
|
||||
* Singleflights in our usecase are tied to an instance of something, combined
|
||||
* with a string key to differentiate between multiple possible actions. This
|
||||
* means that a "save" key will be scoped to the instance which defined it and
|
||||
* not leak between other instances. This is done to avoid having to concatenate
|
||||
* variables to strings to essentially namespace the field, for most cases.
|
||||
*/
|
||||
export class Singleflight {
|
||||
private constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* A void marker to help with returning a value in a singleflight context.
|
||||
* If your code doesn't return anything, return this instead.
|
||||
*/
|
||||
public static Void = Symbol("void");
|
||||
|
||||
/**
|
||||
* Acquire a singleflight context.
|
||||
* @param {Object} instance An instance to associate the context with. Can be any object.
|
||||
* @param {string} key A string key relevant to that instance to namespace under.
|
||||
* @returns {SingleflightContext} Returns the context to execute the function.
|
||||
*/
|
||||
public static for(instance: Object, key: string): SingleflightContext {
|
||||
if (!instance || !key) throw new Error("An instance and key must be supplied");
|
||||
return new SingleflightContext(instance, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forgets all results for a given instance.
|
||||
* @param {Object} instance The instance to forget about.
|
||||
*/
|
||||
public static forgetAllFor(instance: Object) {
|
||||
keyMap.delete(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forgets all cached results for all instances. Intended for use by tests.
|
||||
*/
|
||||
public static forgetAll() {
|
||||
for (const k of keyMap.keys()) {
|
||||
keyMap.remove(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SingleflightContext {
|
||||
public constructor(private instance: Object, private key: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget this particular instance and key combination, discarding the result.
|
||||
*/
|
||||
public forget() {
|
||||
const map = keyMap.get(this.instance);
|
||||
if (!map) return;
|
||||
map.remove(this.key);
|
||||
if (!map.size) keyMap.remove(this.instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a function. If a result is already known, that will be returned instead
|
||||
* of executing the provided function. However, if no result is known then the function
|
||||
* will be called, with its return value cached. The function must return a value
|
||||
* other than `undefined` - take a look at Singleflight.Void if you don't have a return
|
||||
* to make.
|
||||
*
|
||||
* Note that this technically allows the caller to provide a different function each time:
|
||||
* this is largely considered a bad idea and should not be done. Singleflights work off the
|
||||
* premise that something needs to happen once, so duplicate executions will be ignored.
|
||||
*
|
||||
* For ideal performance and behaviour, functions which return promises are preferred. If
|
||||
* a function is not returning a promise, it should return as soon as possible to avoid a
|
||||
* second call potentially racing it. The promise returned by this function will be that
|
||||
* of the first execution of the function, even on duplicate calls.
|
||||
* @param {Function} fn The function to execute.
|
||||
* @returns The recorded value.
|
||||
*/
|
||||
public do<T>(fn: () => T): T {
|
||||
const map = keyMap.getOrCreate(this.instance, new EnhancedMap<string, unknown>());
|
||||
|
||||
// We have to manually getOrCreate() because we need to execute the fn
|
||||
let val = <T>map.get(this.key);
|
||||
if (val === undefined) {
|
||||
val = fn();
|
||||
map.set(this.key, val);
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import {LocalStorageCryptoStore} from 'matrix-js-sdk/src/crypto/store/localStorage-crypto-store';
|
||||
import { LocalStorageCryptoStore } from 'matrix-js-sdk/src/crypto/store/localStorage-crypto-store';
|
||||
import Analytics from '../Analytics';
|
||||
import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb";
|
||||
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
|
||||
|
||||
const localStorage = window.localStorage;
|
||||
|
||||
|
@ -31,15 +32,15 @@ try {
|
|||
const SYNC_STORE_NAME = "riot-web-sync";
|
||||
const CRYPTO_STORE_NAME = "matrix-js-sdk:crypto";
|
||||
|
||||
function log(msg) {
|
||||
function log(msg: string) {
|
||||
console.log(`StorageManager: ${msg}`);
|
||||
}
|
||||
|
||||
function error(msg) {
|
||||
console.error(`StorageManager: ${msg}`);
|
||||
function error(msg: string, ...args: string[]) {
|
||||
console.error(`StorageManager: ${msg}`, ...args);
|
||||
}
|
||||
|
||||
function track(action) {
|
||||
function track(action: string) {
|
||||
Analytics.trackEvent("StorageManager", action);
|
||||
}
|
||||
|
||||
|
@ -72,7 +73,7 @@ export async function checkConsistency() {
|
|||
dataInLocalStorage = localStorage.length > 0;
|
||||
log(`Local storage contains data? ${dataInLocalStorage}`);
|
||||
|
||||
cryptoInited = localStorage.getItem("mx_crypto_initialised");
|
||||
cryptoInited = !!localStorage.getItem("mx_crypto_initialised");
|
||||
log(`Crypto initialised? ${cryptoInited}`);
|
||||
} else {
|
||||
healthy = false;
|
||||
|
@ -132,7 +133,7 @@ export async function checkConsistency() {
|
|||
async function checkSyncStore() {
|
||||
let exists = false;
|
||||
try {
|
||||
exists = await Matrix.IndexedDBStore.exists(
|
||||
exists = await IndexedDBStore.exists(
|
||||
indexedDB, SYNC_STORE_NAME,
|
||||
);
|
||||
log(`Sync store using IndexedDB contains data? ${exists}`);
|
||||
|
@ -148,7 +149,7 @@ async function checkSyncStore() {
|
|||
async function checkCryptoStore() {
|
||||
let exists = false;
|
||||
try {
|
||||
exists = await Matrix.IndexedDBCryptoStore.exists(
|
||||
exists = await IndexedDBCryptoStore.exists(
|
||||
indexedDB, CRYPTO_STORE_NAME,
|
||||
);
|
||||
log(`Crypto store using IndexedDB contains data? ${exists}`);
|
||||
|
@ -190,3 +191,79 @@ export function trackStores(client) {
|
|||
export function setCryptoInitialised(cryptoInited) {
|
||||
localStorage.setItem("mx_crypto_initialised", cryptoInited);
|
||||
}
|
||||
|
||||
/* Simple wrapper functions around IndexedDB.
|
||||
*/
|
||||
|
||||
let idb = null;
|
||||
|
||||
async function idbInit(): Promise<void> {
|
||||
if (!indexedDB) {
|
||||
throw new Error("IndexedDB not available");
|
||||
}
|
||||
idb = await new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open("matrix-react-sdk", 1);
|
||||
request.onerror = reject;
|
||||
request.onsuccess = (event) => { resolve(request.result); };
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = request.result;
|
||||
db.createObjectStore("pickleKey");
|
||||
db.createObjectStore("account");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function idbLoad(
|
||||
table: string,
|
||||
key: string | string[],
|
||||
): Promise<any> {
|
||||
if (!idb) {
|
||||
await idbInit();
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const txn = idb.transaction([table], "readonly");
|
||||
txn.onerror = reject;
|
||||
|
||||
const objectStore = txn.objectStore(table);
|
||||
const request = objectStore.get(key);
|
||||
request.onerror = reject;
|
||||
request.onsuccess = (event) => { resolve(request.result); };
|
||||
});
|
||||
}
|
||||
|
||||
export async function idbSave(
|
||||
table: string,
|
||||
key: string | string[],
|
||||
data: any,
|
||||
): Promise<void> {
|
||||
if (!idb) {
|
||||
await idbInit();
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const txn = idb.transaction([table], "readwrite");
|
||||
txn.onerror = reject;
|
||||
|
||||
const objectStore = txn.objectStore(table);
|
||||
const request = objectStore.put(data, key);
|
||||
request.onerror = reject;
|
||||
request.onsuccess = (event) => { resolve(); };
|
||||
});
|
||||
}
|
||||
|
||||
export async function idbDelete(
|
||||
table: string,
|
||||
key: string | string[],
|
||||
): Promise<void> {
|
||||
if (!idb) {
|
||||
await idbInit();
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const txn = idb.transaction([table], "readwrite");
|
||||
txn.onerror = reject;
|
||||
|
||||
const objectStore = txn.objectStore(table);
|
||||
const request = objectStore.delete(key);
|
||||
request.onerror = reject;
|
||||
request.onsuccess = (event) => { resolve(); };
|
||||
});
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -26,44 +26,48 @@ Once a timer is finished or aborted, it can't be started again
|
|||
a new one through `clone()` or `cloneIfRun()`.
|
||||
*/
|
||||
export default class Timer {
|
||||
constructor(timeout) {
|
||||
this._timeout = timeout;
|
||||
this._onTimeout = this._onTimeout.bind(this);
|
||||
this._setNotStarted();
|
||||
private timerHandle: number;
|
||||
private startTs: number;
|
||||
private promise: Promise<void>;
|
||||
private resolve: () => void;
|
||||
private reject: (Error) => void;
|
||||
|
||||
constructor(private timeout: number) {
|
||||
this.setNotStarted();
|
||||
}
|
||||
|
||||
_setNotStarted() {
|
||||
this._timerHandle = null;
|
||||
this._startTs = null;
|
||||
this._promise = new Promise((resolve, reject) => {
|
||||
this._resolve = resolve;
|
||||
this._reject = reject;
|
||||
private setNotStarted() {
|
||||
this.timerHandle = null;
|
||||
this.startTs = null;
|
||||
this.promise = new Promise<void>((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
}).finally(() => {
|
||||
this._timerHandle = null;
|
||||
this.timerHandle = null;
|
||||
});
|
||||
}
|
||||
|
||||
_onTimeout() {
|
||||
private onTimeout = () => {
|
||||
const now = Date.now();
|
||||
const elapsed = now - this._startTs;
|
||||
if (elapsed >= this._timeout) {
|
||||
this._resolve();
|
||||
this._setNotStarted();
|
||||
const elapsed = now - this.startTs;
|
||||
if (elapsed >= this.timeout) {
|
||||
this.resolve();
|
||||
this.setNotStarted();
|
||||
} else {
|
||||
const delta = this._timeout - elapsed;
|
||||
this._timerHandle = setTimeout(this._onTimeout, delta);
|
||||
const delta = this.timeout - elapsed;
|
||||
this.timerHandle = setTimeout(this.onTimeout, delta);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
changeTimeout(timeout) {
|
||||
if (timeout === this._timeout) {
|
||||
changeTimeout(timeout: number) {
|
||||
if (timeout === this.timeout) {
|
||||
return;
|
||||
}
|
||||
const isSmallerTimeout = timeout < this._timeout;
|
||||
this._timeout = timeout;
|
||||
const isSmallerTimeout = timeout < this.timeout;
|
||||
this.timeout = timeout;
|
||||
if (this.isRunning() && isSmallerTimeout) {
|
||||
clearTimeout(this._timerHandle);
|
||||
this._onTimeout();
|
||||
clearTimeout(this.timerHandle);
|
||||
this.onTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,8 +77,8 @@ export default class Timer {
|
|||
*/
|
||||
start() {
|
||||
if (!this.isRunning()) {
|
||||
this._startTs = Date.now();
|
||||
this._timerHandle = setTimeout(this._onTimeout, this._timeout);
|
||||
this.startTs = Date.now();
|
||||
this.timerHandle = setTimeout(this.onTimeout, this.timeout);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
@ -89,7 +93,7 @@ export default class Timer {
|
|||
// can be called in fast succession,
|
||||
// instead just take note and compare
|
||||
// when the already running timeout expires
|
||||
this._startTs = Date.now();
|
||||
this.startTs = Date.now();
|
||||
return this;
|
||||
} else {
|
||||
return this.start();
|
||||
|
@ -103,9 +107,9 @@ export default class Timer {
|
|||
*/
|
||||
abort() {
|
||||
if (this.isRunning()) {
|
||||
clearTimeout(this._timerHandle);
|
||||
this._reject(new Error("Timer was aborted."));
|
||||
this._setNotStarted();
|
||||
clearTimeout(this.timerHandle);
|
||||
this.reject(new Error("Timer was aborted."));
|
||||
this.setNotStarted();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
@ -116,10 +120,10 @@ export default class Timer {
|
|||
*@return {Promise}
|
||||
*/
|
||||
finished() {
|
||||
return this._promise;
|
||||
return this.promise;
|
||||
}
|
||||
|
||||
isRunning() {
|
||||
return this._timerHandle !== null;
|
||||
return this.timerHandle !== null;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -26,7 +26,7 @@ export function urlSearchParamsToObject<T extends {}>(params: URLSearchParams) {
|
|||
* @param {string} u The url to be abbreviated
|
||||
* @returns {string} The abbreviated url
|
||||
*/
|
||||
export function abbreviateUrl(u) {
|
||||
export function abbreviateUrl(u: string): string {
|
||||
if (!u) return '';
|
||||
|
||||
const parsedUrl = url.parse(u);
|
||||
|
@ -41,7 +41,7 @@ export function abbreviateUrl(u) {
|
|||
return u;
|
||||
}
|
||||
|
||||
export function unabbreviateUrl(u) {
|
||||
export function unabbreviateUrl(u: string): string {
|
||||
if (!u) return '';
|
||||
|
||||
let longUrl = u;
|
||||
|
|
78
src/utils/WellKnownUtils.ts
Normal file
78
src/utils/WellKnownUtils.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
|
||||
const CALL_BEHAVIOUR_WK_KEY = "io.element.call_behaviour";
|
||||
const E2EE_WK_KEY = "io.element.e2ee";
|
||||
const E2EE_WK_KEY_DEPRECATED = "im.vector.riot.e2ee";
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface ICallBehaviourWellKnown {
|
||||
widget_build_url?: string;
|
||||
}
|
||||
|
||||
export interface IE2EEWellKnown {
|
||||
default?: boolean;
|
||||
secure_backup_required?: boolean;
|
||||
secure_backup_setup_methods?: SecureBackupSetupMethod[];
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
export function getCallBehaviourWellKnown(): ICallBehaviourWellKnown {
|
||||
const clientWellKnown = MatrixClientPeg.get().getClientWellKnown();
|
||||
return clientWellKnown?.[CALL_BEHAVIOUR_WK_KEY];
|
||||
}
|
||||
|
||||
export function getE2EEWellKnown(): IE2EEWellKnown {
|
||||
const clientWellKnown = MatrixClientPeg.get().getClientWellKnown();
|
||||
if (clientWellKnown && clientWellKnown[E2EE_WK_KEY]) {
|
||||
return clientWellKnown[E2EE_WK_KEY];
|
||||
}
|
||||
if (clientWellKnown && clientWellKnown[E2EE_WK_KEY_DEPRECATED]) {
|
||||
return clientWellKnown[E2EE_WK_KEY_DEPRECATED];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isSecureBackupRequired(): boolean {
|
||||
const wellKnown = getE2EEWellKnown();
|
||||
return wellKnown && wellKnown["secure_backup_required"] === true;
|
||||
}
|
||||
|
||||
export enum SecureBackupSetupMethod {
|
||||
Key = "key",
|
||||
Passphrase = "passphrase",
|
||||
}
|
||||
|
||||
export function getSecureBackupSetupMethods(): SecureBackupSetupMethod[] {
|
||||
const wellKnown = getE2EEWellKnown();
|
||||
if (
|
||||
!wellKnown ||
|
||||
!wellKnown["secure_backup_setup_methods"] ||
|
||||
!wellKnown["secure_backup_setup_methods"].length ||
|
||||
!(
|
||||
wellKnown["secure_backup_setup_methods"].includes(SecureBackupSetupMethod.Key) ||
|
||||
wellKnown["secure_backup_setup_methods"].includes(SecureBackupSetupMethod.Passphrase)
|
||||
)
|
||||
) {
|
||||
return [
|
||||
SecureBackupSetupMethod.Key,
|
||||
SecureBackupSetupMethod.Passphrase,
|
||||
];
|
||||
}
|
||||
return wellKnown["secure_backup_setup_methods"];
|
||||
}
|
85
src/utils/Whenable.ts
Normal file
85
src/utils/Whenable.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IDestroyable } from "./IDestroyable";
|
||||
import { arrayFastClone } from "./arrays";
|
||||
|
||||
export type WhenFn<T> = (w: Whenable<T>) => void;
|
||||
|
||||
/**
|
||||
* Whenables are a cheap way to have Observable patterns mixed with typical
|
||||
* usage of Promises, without having to tear down listeners or calls. Whenables
|
||||
* are intended to be used when a condition will be met multiple times and
|
||||
* the consumer needs to know *when* that happens.
|
||||
*/
|
||||
export abstract class Whenable<T> implements IDestroyable {
|
||||
private listeners: {condition: T | null, fn: WhenFn<T>}[] = [];
|
||||
|
||||
/**
|
||||
* Sets up a call to `fn` *when* the `condition` is met.
|
||||
* @param condition The condition to match.
|
||||
* @param fn The function to call.
|
||||
* @returns This.
|
||||
*/
|
||||
public when(condition: T, fn: WhenFn<T>): Whenable<T> {
|
||||
this.listeners.push({ condition, fn });
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a call to `fn` *when* any of the `conditions` are met.
|
||||
* @param conditions The conditions to match.
|
||||
* @param fn The function to call.
|
||||
* @returns This.
|
||||
*/
|
||||
public whenAnyOf(conditions: T[], fn: WhenFn<T>): Whenable<T> {
|
||||
for (const condition of conditions) {
|
||||
this.when(condition, fn);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a call to `fn` *when* any condition is met.
|
||||
* @param fn The function to call.
|
||||
* @returns This.
|
||||
*/
|
||||
public whenAnything(fn: WhenFn<T>): Whenable<T> {
|
||||
this.listeners.push({ condition: null, fn });
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies all the listeners of a given condition.
|
||||
* @param condition The new condition that has been met.
|
||||
*/
|
||||
protected notifyCondition(condition: T) {
|
||||
const listeners = arrayFastClone(this.listeners); // clone just in case the handler modifies us
|
||||
for (const listener of listeners) {
|
||||
if (listener.condition === null || listener.condition === condition) {
|
||||
try {
|
||||
listener.fn(this);
|
||||
} catch (e) {
|
||||
console.error(`Error calling whenable listener for ${condition}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.listeners = [];
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Travis Ralston
|
||||
Copyright 2017 - 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,19 +15,34 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import dis from '../dispatcher';
|
||||
import * as url from "url";
|
||||
import { Capability, IWidget, IWidgetData, MatrixCapabilities } from "matrix-widget-api";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import dis from '../dispatcher/dispatcher';
|
||||
import WidgetEchoStore from '../stores/WidgetEchoStore';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { IntegrationManagers } from "../integrations/IntegrationManagers";
|
||||
import { WidgetType } from "../widgets/WidgetType";
|
||||
import { objectClone } from "./objects";
|
||||
import { _t } from "../languageHandler";
|
||||
import { IApp } from "../stores/WidgetStore";
|
||||
|
||||
// How long we wait for the state event echo to come back from the server
|
||||
// before waitFor[Room/User]Widget rejects its promise
|
||||
const WIDGET_WAIT_TIME = 20000;
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
||||
import {IntegrationManagers} from "../integrations/IntegrationManagers";
|
||||
import {Capability} from "../widgets/WidgetApi";
|
||||
|
||||
export interface IWidgetEvent {
|
||||
id: string;
|
||||
type: string;
|
||||
sender: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
state_key: string;
|
||||
content: Partial<IApp>;
|
||||
}
|
||||
|
||||
export default class WidgetUtils {
|
||||
/* Returns true if user is able to send state events to modify widgets in this room
|
||||
|
@ -37,7 +51,7 @@ export default class WidgetUtils {
|
|||
* @return Boolean -- true if the user can modify widgets in this room
|
||||
* @throws Error -- specifies the error reason
|
||||
*/
|
||||
static canUserModifyWidgets(roomId) {
|
||||
static canUserModifyWidgets(roomId: string): boolean {
|
||||
if (!roomId) {
|
||||
console.warn('No room ID specified');
|
||||
return false;
|
||||
|
@ -66,15 +80,17 @@ export default class WidgetUtils {
|
|||
return false;
|
||||
}
|
||||
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
return room.currentState.maySendStateEvent('im.vector.modular.widgets', me);
|
||||
}
|
||||
|
||||
// TODO: Generify the name of this function. It's not just scalar.
|
||||
/**
|
||||
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
|
||||
* @param {[type]} testUrlString URL to check
|
||||
* @return {Boolean} True if specified URL is a scalar URL
|
||||
*/
|
||||
static isScalarUrl(testUrlString) {
|
||||
static isScalarUrl(testUrlString: string): boolean {
|
||||
if (!testUrlString) {
|
||||
console.error('Scalar URL check failed. No URL specified');
|
||||
return false;
|
||||
|
@ -117,7 +133,7 @@ export default class WidgetUtils {
|
|||
* @returns {Promise} that resolves when the widget is in the
|
||||
* requested state according to the `add` param
|
||||
*/
|
||||
static waitForUserWidget(widgetId, add) {
|
||||
static waitForUserWidget(widgetId: string, add: boolean): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Tests an account data event, returning true if it's in the state
|
||||
// we're waiting for it to be in
|
||||
|
@ -164,7 +180,7 @@ export default class WidgetUtils {
|
|||
* @returns {Promise} that resolves when the widget is in the
|
||||
* requested state according to the `add` param
|
||||
*/
|
||||
static waitForRoomWidget(widgetId, roomId, add) {
|
||||
static waitForRoomWidget(widgetId: string, roomId: string, add: boolean): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Tests a list of state events, returning true if it's in the state
|
||||
// we're waiting for it to be in
|
||||
|
@ -180,6 +196,7 @@ export default class WidgetUtils {
|
|||
}
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
const startingWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||
if (eventsInIntendedState(startingWidgetEvents)) {
|
||||
resolve();
|
||||
|
@ -189,6 +206,7 @@ export default class WidgetUtils {
|
|||
function onRoomStateEvents(ev) {
|
||||
if (ev.getRoomId() !== roomId) return;
|
||||
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
const currentWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||
|
||||
if (eventsInIntendedState(currentWidgetEvents)) {
|
||||
|
@ -205,9 +223,15 @@ export default class WidgetUtils {
|
|||
});
|
||||
}
|
||||
|
||||
static setUserWidget(widgetId, widgetType, widgetUrl, widgetName, widgetData) {
|
||||
static setUserWidget(
|
||||
widgetId: string,
|
||||
widgetType: WidgetType,
|
||||
widgetUrl: string,
|
||||
widgetName: string,
|
||||
widgetData: IWidgetData,
|
||||
) {
|
||||
const content = {
|
||||
type: widgetType,
|
||||
type: widgetType.preferred,
|
||||
url: widgetUrl,
|
||||
name: widgetName,
|
||||
data: widgetData,
|
||||
|
@ -216,7 +240,7 @@ export default class WidgetUtils {
|
|||
const client = MatrixClientPeg.get();
|
||||
// Get the current widgets and clone them before we modify them, otherwise
|
||||
// we'll modify the content of the old event.
|
||||
const userWidgets = JSON.parse(JSON.stringify(WidgetUtils.getUserWidgets()));
|
||||
const userWidgets = objectClone(WidgetUtils.getUserWidgets());
|
||||
|
||||
// Delete existing widget with ID
|
||||
try {
|
||||
|
@ -249,14 +273,23 @@ export default class WidgetUtils {
|
|||
});
|
||||
}
|
||||
|
||||
static setRoomWidget(roomId, widgetId, widgetType, widgetUrl, widgetName, widgetData) {
|
||||
static setRoomWidget(
|
||||
roomId: string,
|
||||
widgetId: string,
|
||||
widgetType?: WidgetType,
|
||||
widgetUrl?: string,
|
||||
widgetName?: string,
|
||||
widgetData?: object,
|
||||
) {
|
||||
let content;
|
||||
|
||||
const addingWidget = Boolean(widgetUrl);
|
||||
|
||||
if (addingWidget) {
|
||||
content = {
|
||||
type: widgetType,
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
// For now we'll send the legacy event type for compatibility with older apps/elements
|
||||
type: widgetType.legacy,
|
||||
url: widgetUrl,
|
||||
name: widgetName,
|
||||
data: widgetData,
|
||||
|
@ -265,11 +298,20 @@ export default class WidgetUtils {
|
|||
content = {};
|
||||
}
|
||||
|
||||
return WidgetUtils.setRoomWidgetContent(roomId, widgetId, content);
|
||||
}
|
||||
|
||||
static setRoomWidgetContent(
|
||||
roomId: string,
|
||||
widgetId: string,
|
||||
content: IWidget,
|
||||
) {
|
||||
const addingWidget = !!content.url;
|
||||
|
||||
WidgetEchoStore.setRoomWidgetEcho(roomId, widgetId, content);
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
// TODO - Room widgets need to be moved to 'm.widget' state events
|
||||
// https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
return client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).then(() => {
|
||||
return WidgetUtils.waitForRoomWidget(widgetId, roomId, addingWidget);
|
||||
}).finally(() => {
|
||||
|
@ -279,10 +321,11 @@ export default class WidgetUtils {
|
|||
|
||||
/**
|
||||
* Get room specific widgets
|
||||
* @param {object} room The room to get widgets force
|
||||
* @param {Room} room The room to get widgets force
|
||||
* @return {[object]} Array containing current / active room widgets
|
||||
*/
|
||||
static getRoomWidgets(room) {
|
||||
static getRoomWidgets(room: Room) {
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||
if (!appsStateEvents) {
|
||||
return [];
|
||||
|
@ -297,7 +340,7 @@ export default class WidgetUtils {
|
|||
* Get user specific widgets (not linked to a specific room)
|
||||
* @return {object} Event content object containing current / active user widgets
|
||||
*/
|
||||
static getUserWidgets() {
|
||||
static getUserWidgets(): Record<string, IWidgetEvent> {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
throw new Error('User not logged in');
|
||||
|
@ -313,7 +356,7 @@ export default class WidgetUtils {
|
|||
* Get user specific widgets (not linked to a specific room) as an array
|
||||
* @return {[object]} Array containing current / active user widgets
|
||||
*/
|
||||
static getUserWidgetsArray() {
|
||||
static getUserWidgetsArray(): IWidgetEvent[] {
|
||||
return Object.values(WidgetUtils.getUserWidgets());
|
||||
}
|
||||
|
||||
|
@ -321,7 +364,7 @@ export default class WidgetUtils {
|
|||
* Get active stickerpicker widgets (stickerpickers are user widgets by nature)
|
||||
* @return {[object]} Array containing current / active stickerpicker widgets
|
||||
*/
|
||||
static getStickerpickerWidgets() {
|
||||
static getStickerpickerWidgets(): IWidgetEvent[] {
|
||||
const widgets = WidgetUtils.getUserWidgetsArray();
|
||||
return widgets.filter((widget) => widget.content && widget.content.type === "m.stickerpicker");
|
||||
}
|
||||
|
@ -330,34 +373,42 @@ export default class WidgetUtils {
|
|||
* Get all integration manager widgets for this user.
|
||||
* @returns {Object[]} An array of integration manager user widgets.
|
||||
*/
|
||||
static getIntegrationManagerWidgets() {
|
||||
static getIntegrationManagerWidgets(): IWidgetEvent[] {
|
||||
const widgets = WidgetUtils.getUserWidgetsArray();
|
||||
return widgets.filter(w => w.content && w.content.type === "m.integration_manager");
|
||||
}
|
||||
|
||||
static removeIntegrationManagerWidgets() {
|
||||
static getRoomWidgetsOfType(room: Room, type: WidgetType): MatrixEvent[] {
|
||||
const widgets = WidgetUtils.getRoomWidgets(room) || [];
|
||||
return widgets.filter(w => {
|
||||
const content = w.getContent();
|
||||
return content.url && type.matches(content.type);
|
||||
});
|
||||
}
|
||||
|
||||
static async removeIntegrationManagerWidgets(): Promise<void> {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
throw new Error('User not logged in');
|
||||
}
|
||||
const widgets = client.getAccountData('m.widgets');
|
||||
if (!widgets) return;
|
||||
const userWidgets = widgets.getContent() || {};
|
||||
const userWidgets: Record<string, IWidgetEvent> = widgets.getContent() || {};
|
||||
Object.entries(userWidgets).forEach(([key, widget]) => {
|
||||
if (widget.content && widget.content.type === "m.integration_manager") {
|
||||
delete userWidgets[key];
|
||||
}
|
||||
});
|
||||
return client.setAccountData('m.widgets', userWidgets);
|
||||
await client.setAccountData('m.widgets', userWidgets);
|
||||
}
|
||||
|
||||
static addIntegrationManagerWidget(name: string, uiUrl: string, apiUrl: string) {
|
||||
static addIntegrationManagerWidget(name: string, uiUrl: string, apiUrl: string): Promise<void> {
|
||||
return WidgetUtils.setUserWidget(
|
||||
"integration_manager_" + (new Date().getTime()),
|
||||
"m.integration_manager",
|
||||
WidgetType.INTEGRATION_MANAGER,
|
||||
uiUrl,
|
||||
"Integration Manager: " + name,
|
||||
{"api_url": apiUrl},
|
||||
"Integration manager: " + name,
|
||||
{ "api_url": apiUrl },
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -365,92 +416,114 @@ export default class WidgetUtils {
|
|||
* Remove all stickerpicker widgets (stickerpickers are user widgets by nature)
|
||||
* @return {Promise} Resolves on account data updated
|
||||
*/
|
||||
static removeStickerpickerWidgets() {
|
||||
static async removeStickerpickerWidgets(): Promise<void> {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
throw new Error('User not logged in');
|
||||
}
|
||||
const widgets = client.getAccountData('m.widgets');
|
||||
if (!widgets) return;
|
||||
const userWidgets = widgets.getContent() || {};
|
||||
const userWidgets: Record<string, IWidgetEvent> = widgets.getContent() || {};
|
||||
Object.entries(userWidgets).forEach(([key, widget]) => {
|
||||
if (widget.content && widget.content.type === 'm.stickerpicker') {
|
||||
delete userWidgets[key];
|
||||
}
|
||||
});
|
||||
return client.setAccountData('m.widgets', userWidgets);
|
||||
await client.setAccountData('m.widgets', userWidgets);
|
||||
}
|
||||
|
||||
static makeAppConfig(appId, app, senderUserId, roomId, eventId) {
|
||||
static makeAppConfig(
|
||||
appId: string,
|
||||
app: Partial<IApp>,
|
||||
senderUserId: string,
|
||||
roomId: string | null,
|
||||
eventId: string,
|
||||
): IApp {
|
||||
if (!senderUserId) {
|
||||
throw new Error("Widgets must be created by someone - provide a senderUserId");
|
||||
}
|
||||
app.creatorUserId = senderUserId;
|
||||
|
||||
app.id = appId;
|
||||
app.roomId = roomId;
|
||||
app.eventId = eventId;
|
||||
app.name = app.name || app.type;
|
||||
|
||||
return app;
|
||||
return app as IApp;
|
||||
}
|
||||
|
||||
static getCapWhitelistForAppTypeInRoomId(appType, roomId) {
|
||||
static getCapWhitelistForAppTypeInRoomId(appType: string, roomId: string): Capability[] {
|
||||
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId);
|
||||
|
||||
const capWhitelist = enableScreenshots ? [Capability.Screenshot] : [];
|
||||
const capWhitelist = enableScreenshots ? [MatrixCapabilities.Screenshots] : [];
|
||||
|
||||
// Obviously anyone that can add a widget can claim it's a jitsi widget,
|
||||
// so this doesn't really offer much over the set of domains we load
|
||||
// widgets from at all, but it probably makes sense for sanity.
|
||||
if (appType === 'jitsi') {
|
||||
capWhitelist.push(Capability.AlwaysOnScreen);
|
||||
if (WidgetType.JITSI.matches(appType)) {
|
||||
capWhitelist.push(MatrixCapabilities.AlwaysOnScreen);
|
||||
}
|
||||
|
||||
return capWhitelist;
|
||||
}
|
||||
|
||||
static getWidgetSecurityKey(widgetId, widgetUrl, isUserWidget) {
|
||||
let widgetLocation = ActiveWidgetStore.getRoomId(widgetId);
|
||||
|
||||
if (isUserWidget) {
|
||||
const userWidget = WidgetUtils.getUserWidgetsArray()
|
||||
.find((w) => w.id === widgetId && w.content && w.content.url === widgetUrl);
|
||||
|
||||
if (!userWidget) {
|
||||
throw new Error("No matching user widget to form security key");
|
||||
}
|
||||
|
||||
widgetLocation = userWidget.sender;
|
||||
}
|
||||
|
||||
if (!widgetLocation) {
|
||||
throw new Error("Failed to locate where the widget resides");
|
||||
}
|
||||
|
||||
return encodeURIComponent(`${widgetLocation}::${widgetUrl}`);
|
||||
}
|
||||
|
||||
static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean}={}) {
|
||||
static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean, auth?: string} = {}) {
|
||||
// NB. we can't just encodeURIComponent all of these because the $ signs need to be there
|
||||
const queryString = [
|
||||
const queryStringParts = [
|
||||
'conferenceDomain=$domain',
|
||||
'conferenceId=$conferenceId',
|
||||
'isAudioOnly=$isAudioOnly',
|
||||
'displayName=$matrix_display_name',
|
||||
'avatarUrl=$matrix_avatar_url',
|
||||
'userId=$matrix_user_id',
|
||||
].join('&');
|
||||
'roomId=$matrix_room_id',
|
||||
'theme=$theme',
|
||||
'roomName=$roomName',
|
||||
];
|
||||
if (opts.auth) {
|
||||
queryStringParts.push(`auth=${opts.auth}`);
|
||||
}
|
||||
const queryString = queryStringParts.join('&');
|
||||
|
||||
let baseUrl = window.location;
|
||||
let baseUrl = window.location.href;
|
||||
if (window.location.protocol !== "https:" && !opts.forLocalRender) {
|
||||
// Use an external wrapper if we're not locally rendering the widget. This is usually
|
||||
// the URL that will end up in the widget event, so we want to make sure it's relatively
|
||||
// safe to send.
|
||||
// We'll end up using a local render URL when we see a Jitsi widget anyways, so this is
|
||||
// really just for backwards compatibility and to appease the spec.
|
||||
baseUrl = "https://riot.im/app/";
|
||||
baseUrl = "https://app.element.io/";
|
||||
}
|
||||
const url = new URL("jitsi.html#" + queryString, baseUrl); // this strips hash fragment from baseUrl
|
||||
return url.href;
|
||||
}
|
||||
|
||||
static getWidgetName(app?: IApp): string {
|
||||
return app?.name?.trim() || _t("Unknown App");
|
||||
}
|
||||
|
||||
static getWidgetDataTitle(app?: IApp): string {
|
||||
return app?.data?.title?.trim() || "";
|
||||
}
|
||||
|
||||
static editWidget(room: Room, app: IApp): void {
|
||||
// TODO: Open the right manager for the widget
|
||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||
IntegrationManagers.sharedInstance().openAll(room, 'type_' + app.type, app.id);
|
||||
} else {
|
||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(room, 'type_' + app.type, app.id);
|
||||
}
|
||||
}
|
||||
|
||||
static isManagedByManager(app) {
|
||||
if (WidgetUtils.isScalarUrl(app.url)) {
|
||||
const managers = IntegrationManagers.sharedInstance();
|
||||
if (managers.hasManager()) {
|
||||
// TODO: Pick the right manager for the widget
|
||||
const defaultManager = managers.getPrimaryManager();
|
||||
return WidgetUtils.isScalarUrl(defaultManager.apiUrl);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
304
src/utils/arrays.ts
Normal file
304
src/utils/arrays.ts
Normal file
|
@ -0,0 +1,304 @@
|
|||
/*
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { percentageOf, percentageWithin } from "./numbers";
|
||||
|
||||
/**
|
||||
* Quickly resample an array to have less/more data points. If an input which is larger
|
||||
* than the desired size is provided, it will be downsampled. Similarly, if the input
|
||||
* is smaller than the desired size then it will be upsampled.
|
||||
* @param {number[]} input The input array to resample.
|
||||
* @param {number} points The number of samples to end up with.
|
||||
* @returns {number[]} The resampled array.
|
||||
*/
|
||||
export function arrayFastResample(input: number[], points: number): number[] {
|
||||
if (input.length === points) return input; // short-circuit a complicated call
|
||||
|
||||
// Heavily inspired by matrix-media-repo (used with permission)
|
||||
// https://github.com/turt2live/matrix-media-repo/blob/abe72c87d2e29/util/util_audio/fastsample.go#L10
|
||||
const samples: number[] = [];
|
||||
if (input.length > points) {
|
||||
// Danger: this loop can cause out of memory conditions if the input is too small.
|
||||
const everyNth = Math.round(input.length / points);
|
||||
for (let i = 0; i < input.length; i += everyNth) {
|
||||
samples.push(input[i]);
|
||||
}
|
||||
} else {
|
||||
// Smaller inputs mean we have to spread the values over the desired length. We
|
||||
// end up overshooting the target length in doing this, but we're not looking to
|
||||
// be super accurate so we'll let the sanity trims do their job.
|
||||
const spreadFactor = Math.ceil(points / input.length);
|
||||
for (const val of input) {
|
||||
samples.push(...arraySeed(val, spreadFactor));
|
||||
}
|
||||
}
|
||||
|
||||
// Trim to size & return
|
||||
return arrayTrimFill(samples, points, arraySeed(input[input.length - 1], points));
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts a smooth resample of the given array. This is functionally similar to arrayFastResample
|
||||
* though can take longer due to the smoothing of data.
|
||||
* @param {number[]} input The input array to resample.
|
||||
* @param {number} points The number of samples to end up with.
|
||||
* @returns {number[]} The resampled array.
|
||||
*/
|
||||
export function arraySmoothingResample(input: number[], points: number): number[] {
|
||||
if (input.length === points) return input; // short-circuit a complicated call
|
||||
|
||||
let samples: number[] = [];
|
||||
if (input.length > points) {
|
||||
// We're downsampling. To preserve the curve we'll actually reduce our sample
|
||||
// selection and average some points between them.
|
||||
|
||||
// All we're doing here is repeatedly averaging the waveform down to near our
|
||||
// target value. We don't average down to exactly our target as the loop might
|
||||
// never end, and we can over-average the data. Instead, we'll get as far as
|
||||
// we can and do a followup fast resample (the neighbouring points will be close
|
||||
// to the actual waveform, so we can get away with this safely).
|
||||
while (samples.length > (points * 2) || samples.length === 0) {
|
||||
samples = [];
|
||||
for (let i = 1; i < input.length - 1; i += 2) {
|
||||
const prevPoint = input[i - 1];
|
||||
const nextPoint = input[i + 1];
|
||||
const currPoint = input[i];
|
||||
const average = (prevPoint + nextPoint + currPoint) / 3;
|
||||
samples.push(average);
|
||||
}
|
||||
input = samples;
|
||||
}
|
||||
|
||||
return arrayFastResample(samples, points);
|
||||
} else {
|
||||
// In practice there's not much purpose in burning CPU for short arrays only to
|
||||
// end up with a result that can't possibly look much different than the fast
|
||||
// resample, so just skip ahead to the fast resample.
|
||||
return arrayFastResample(input, points);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rescales the input array to have values that are inclusively within the provided
|
||||
* minimum and maximum.
|
||||
* @param {number[]} input The array to rescale.
|
||||
* @param {number} newMin The minimum value to scale to.
|
||||
* @param {number} newMax The maximum value to scale to.
|
||||
* @returns {number[]} The rescaled array.
|
||||
*/
|
||||
export function arrayRescale(input: number[], newMin: number, newMax: number): number[] {
|
||||
const min: number = Math.min(...input);
|
||||
const max: number = Math.max(...input);
|
||||
return input.map(v => percentageWithin(percentageOf(v, min, max), newMin, newMax));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an array of the given length, seeded with the given value.
|
||||
* @param {T} val The value to seed the array with.
|
||||
* @param {number} length The length of the array to create.
|
||||
* @returns {T[]} The array.
|
||||
*/
|
||||
export function arraySeed<T>(val: T, length: number): T[] {
|
||||
// Size the array up front for performance, and use `fill` to let the browser
|
||||
// optimize the operation better than we can with a `for` loop, if it wants.
|
||||
return new Array<T>(length).fill(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims or fills the array to ensure it meets the desired length. The seed array
|
||||
* given is pulled from to fill any missing slots - it is recommended that this be
|
||||
* at least `len` long. The resulting array will be exactly `len` long, either
|
||||
* trimmed from the source or filled with the some/all of the seed array.
|
||||
* @param {T[]} a The array to trim/fill.
|
||||
* @param {number} len The length to trim or fill to, as needed.
|
||||
* @param {T[]} seed Values to pull from if the array needs filling.
|
||||
* @returns {T[]} The resulting array of `len` length.
|
||||
*/
|
||||
export function arrayTrimFill<T>(a: T[], len: number, seed: T[]): T[] {
|
||||
// Dev note: we do length checks because the spread operator can result in some
|
||||
// performance penalties in more critical code paths. As a utility, it should be
|
||||
// as fast as possible to not cause a problem for the call stack, no matter how
|
||||
// critical that stack is.
|
||||
if (a.length === len) return a;
|
||||
if (a.length > len) return a.slice(0, len);
|
||||
return a.concat(seed.slice(0, len - a.length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones an array as fast as possible, retaining references of the array's values.
|
||||
* @param a The array to clone. Must be defined.
|
||||
* @returns A copy of the array.
|
||||
*/
|
||||
export function arrayFastClone<T>(a: T[]): T[] {
|
||||
return a.slice(0, a.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the two arrays are different either in length, contents,
|
||||
* or order of those contents.
|
||||
* @param a The first array. Must be defined.
|
||||
* @param b The second array. Must be defined.
|
||||
* @returns True if they are different, false otherwise.
|
||||
*/
|
||||
export function arrayHasOrderChange(a: any[], b: any[]): boolean {
|
||||
if (a.length === b.length) {
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return true;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return true; // like arrayHasDiff, a difference in length is a natural change
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if two arrays are different through a shallow comparison.
|
||||
* @param a The first array. Must be defined.
|
||||
* @param b The second array. Must be defined.
|
||||
* @returns True if they are different, false otherwise.
|
||||
*/
|
||||
export function arrayHasDiff(a: any[], b: any[]): boolean {
|
||||
if (a.length === b.length) {
|
||||
// When the lengths are equal, check to see if either array is missing
|
||||
// an element from the other.
|
||||
if (b.some(i => !a.includes(i))) return true;
|
||||
if (a.some(i => !b.includes(i))) return true;
|
||||
|
||||
// if all the keys are common, say so
|
||||
return false;
|
||||
} else {
|
||||
return true; // different lengths means they are naturally diverged
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a diff on two arrays. The result is what is different with the
|
||||
* first array (`added` in the returned object means objects in B that aren't
|
||||
* in A). Shallow comparisons are used to perform the diff.
|
||||
* @param a The first array. Must be defined.
|
||||
* @param b The second array. Must be defined.
|
||||
* @returns The diff between the arrays.
|
||||
*/
|
||||
export function arrayDiff<T>(a: T[], b: T[]): { added: T[], removed: T[] } {
|
||||
return {
|
||||
added: b.filter(i => !a.includes(i)),
|
||||
removed: a.filter(i => !b.includes(i)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the union of two arrays.
|
||||
* @param a The first array. Must be defined.
|
||||
* @param b The second array. Must be defined.
|
||||
* @returns The union of the arrays.
|
||||
*/
|
||||
export function arrayUnion<T>(a: T[], b: T[]): T[] {
|
||||
return a.filter(i => b.includes(i));
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges arrays, deduping contents using a Set.
|
||||
* @param a The arrays to merge.
|
||||
* @returns The merged array.
|
||||
*/
|
||||
export function arrayMerge<T>(...a: T[][]): T[] {
|
||||
return Array.from(a.reduce((c, v) => {
|
||||
v.forEach(i => c.add(i));
|
||||
return c;
|
||||
}, new Set<T>()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a single element from fromIndex to toIndex.
|
||||
* @param {array} list the list from which to construct the new list.
|
||||
* @param {number} fromIndex the index of the element to move.
|
||||
* @param {number} toIndex the index of where to put the element.
|
||||
* @returns {array} A new array with the requested value moved.
|
||||
*/
|
||||
export function moveElement<T>(list: T[], fromIndex: number, toIndex: number): T[] {
|
||||
const result = Array.from(list);
|
||||
const [removed] = result.splice(fromIndex, 1);
|
||||
result.splice(toIndex, 0, removed);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper functions to perform LINQ-like queries on arrays.
|
||||
*/
|
||||
export class ArrayUtil<T> {
|
||||
/**
|
||||
* Create a new array helper.
|
||||
* @param a The array to help. Can be modified in-place.
|
||||
*/
|
||||
constructor(private a: T[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The value of this array, after all appropriate alterations.
|
||||
*/
|
||||
public get value(): T[] {
|
||||
return this.a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups an array by keys.
|
||||
* @param fn The key-finding function.
|
||||
* @returns This.
|
||||
*/
|
||||
public groupBy<K>(fn: (a: T) => K): GroupedArray<K, T> {
|
||||
const obj = this.a.reduce((rv: Map<K, T[]>, val: T) => {
|
||||
const k = fn(val);
|
||||
if (!rv.has(k)) rv.set(k, []);
|
||||
rv.get(k).push(val);
|
||||
return rv;
|
||||
}, new Map<K, T[]>());
|
||||
return new GroupedArray(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper functions to perform LINQ-like queries on groups (maps).
|
||||
*/
|
||||
export class GroupedArray<K, T> {
|
||||
/**
|
||||
* Creates a new group helper.
|
||||
* @param val The group to help. Can be modified in-place.
|
||||
*/
|
||||
constructor(private val: Map<K, T[]>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The value of this group, after all applicable alterations.
|
||||
*/
|
||||
public get value(): Map<K, T[]> {
|
||||
return this.val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Orders the grouping into an array using the provided key order.
|
||||
* @param keyOrder The key order.
|
||||
* @returns An array helper of the result.
|
||||
*/
|
||||
public orderBy(keyOrder: K[]): ArrayUtil<T> {
|
||||
const a: T[] = [];
|
||||
for (const k of keyOrder) {
|
||||
if (!this.val.has(k)) continue;
|
||||
a.push(...this.val.get(k));
|
||||
}
|
||||
return new ArrayUtil(a);
|
||||
}
|
||||
}
|
78
src/utils/blobs.ts
Normal file
78
src/utils/blobs.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// WARNING: We have to be very careful about what mime-types we allow into blobs,
|
||||
// as for performance reasons these are now rendered via URL.createObjectURL()
|
||||
// rather than by converting into data: URIs.
|
||||
//
|
||||
// This means that the content is rendered using the origin of the script which
|
||||
// called createObjectURL(), and so if the content contains any scripting then it
|
||||
// will pose a XSS vulnerability when the browser renders it. This is particularly
|
||||
// bad if the user right-clicks the URI and pastes it into a new window or tab,
|
||||
// as the blob will then execute with access to Element's full JS environment(!)
|
||||
//
|
||||
// See https://github.com/matrix-org/matrix-react-sdk/pull/1820#issuecomment-385210647
|
||||
// for details.
|
||||
//
|
||||
// We mitigate this by only allowing mime-types into blobs which we know don't
|
||||
// contain any scripting, and instantiate all others as application/octet-stream
|
||||
// regardless of what mime-type the event claimed. Even if the payload itself
|
||||
// is some malicious HTML, the fact we instantiate it with a media mimetype or
|
||||
// application/octet-stream means the browser doesn't try to render it as such.
|
||||
//
|
||||
// One interesting edge case is image/svg+xml, which empirically *is* rendered
|
||||
// correctly if the blob is set to the src attribute of an img tag (for thumbnails)
|
||||
// *even if the mimetype is application/octet-stream*. However, empirically JS
|
||||
// in the SVG isn't executed in this scenario, so we seem to be okay.
|
||||
//
|
||||
// Tested on Chrome 65 and Firefox 60
|
||||
//
|
||||
// The list below is taken mainly from
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
|
||||
// N.B. Matrix doesn't currently specify which mimetypes are valid in given
|
||||
// events, so we pick the ones which HTML5 browsers should be able to display
|
||||
//
|
||||
// For the record, mime-types which must NEVER enter this list below include:
|
||||
// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar.
|
||||
|
||||
const ALLOWED_BLOB_MIMETYPES = [
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/png',
|
||||
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
|
||||
'audio/mp4',
|
||||
'audio/webm',
|
||||
'audio/aac',
|
||||
'audio/mpeg',
|
||||
'audio/ogg',
|
||||
'audio/wave',
|
||||
'audio/wav',
|
||||
'audio/x-wav',
|
||||
'audio/x-pn-wav',
|
||||
'audio/flac',
|
||||
'audio/x-flac',
|
||||
];
|
||||
|
||||
export function getBlobSafeMimeType(mimetype: string): string {
|
||||
if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) {
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
return mimetype;
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export function hueToRGB(h, s, l) {
|
||||
const c = s * (1 - Math.abs(2 * l - 1));
|
||||
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
|
||||
const m = l - c / 2;
|
||||
|
||||
let r = 0;
|
||||
let g = 0;
|
||||
let b = 0;
|
||||
|
||||
if (0 <= h && h < 60) {
|
||||
r = c;
|
||||
g = x;
|
||||
b = 0;
|
||||
} else if (60 <= h && h < 120) {
|
||||
r = x;
|
||||
g = c;
|
||||
b = 0;
|
||||
} else if (120 <= h && h < 180) {
|
||||
r = 0;
|
||||
g = c;
|
||||
b = x;
|
||||
} else if (180 <= h && h < 240) {
|
||||
r = 0;
|
||||
g = x;
|
||||
b = c;
|
||||
} else if (240 <= h && h < 300) {
|
||||
r = x;
|
||||
g = 0;
|
||||
b = c;
|
||||
} else if (300 <= h && h < 360) {
|
||||
r = c;
|
||||
g = 0;
|
||||
b = x;
|
||||
}
|
||||
|
||||
return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
|
||||
}
|
||||
|
||||
|
||||
export function textToHtmlRainbow(str) {
|
||||
const frequency = 360 / str.length;
|
||||
|
||||
return Array.from(str).map((c, i) => {
|
||||
const [r, g, b] = hueToRGB(i * frequency, 1.0, 0.5);
|
||||
return '<font color="#' +
|
||||
r.toString(16).padStart(2, "0") +
|
||||
g.toString(16).padStart(2, "0") +
|
||||
b.toString(16).padStart(2, "0") +
|
||||
'">' + c + '</font>';
|
||||
}).join("");
|
||||
}
|
88
src/utils/colour.ts
Normal file
88
src/utils/colour.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export function textToHtmlRainbow(str: string): string {
|
||||
const frequency = (2 * Math.PI) / str.length;
|
||||
|
||||
return Array.from(str)
|
||||
.map((c, i) => {
|
||||
if (c === " ") {
|
||||
return c;
|
||||
}
|
||||
const [a, b] = generateAB(i * frequency, 1);
|
||||
const [red, green, blue] = labToRGB(75, a, b);
|
||||
return (
|
||||
'<font color="#' +
|
||||
red.toString(16).padStart(2, "0") +
|
||||
green.toString(16).padStart(2, "0") +
|
||||
blue.toString(16).padStart(2, "0") +
|
||||
'">' +
|
||||
c +
|
||||
"</font>"
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function generateAB(hue: number, chroma: number): [number, number] {
|
||||
const a = chroma * 127 * Math.cos(hue);
|
||||
const b = chroma * 127 * Math.sin(hue);
|
||||
|
||||
return [a, b];
|
||||
}
|
||||
|
||||
function labToRGB(l: number, a: number, b: number): [number, number, number] {
|
||||
// https://en.wikipedia.org/wiki/CIELAB_color_space#Reverse_transformation
|
||||
// https://en.wikipedia.org/wiki/SRGB#The_forward_transformation_(CIE_XYZ_to_sRGB)
|
||||
|
||||
// Convert CIELAB to CIEXYZ (D65)
|
||||
let y = (l + 16) / 116;
|
||||
const x = adjustXYZ(y + a / 500) * 0.9505;
|
||||
const z = adjustXYZ(y - b / 200) * 1.089;
|
||||
|
||||
y = adjustXYZ(y);
|
||||
|
||||
// Linear transformation from CIEXYZ to RGB
|
||||
const red = 3.24096994 * x - 1.53738318 * y - 0.49861076 * z;
|
||||
const green = -0.96924364 * x + 1.8759675 * y + 0.04155506 * z;
|
||||
const blue = 0.05563008 * x - 0.20397696 * y + 1.05697151 * z;
|
||||
|
||||
return [adjustRGB(red), adjustRGB(green), adjustRGB(blue)];
|
||||
}
|
||||
|
||||
function adjustXYZ(v: number): number {
|
||||
if (v > 0.2069) {
|
||||
return Math.pow(v, 3);
|
||||
}
|
||||
return 0.1284 * v - 0.01771;
|
||||
}
|
||||
|
||||
function gammaCorrection(v: number): number {
|
||||
// Non-linear transformation to sRGB
|
||||
if (v <= 0.0031308) {
|
||||
return 12.92 * v;
|
||||
}
|
||||
return 1.055 * Math.pow(v, 1 / 2.4) - 0.055;
|
||||
}
|
||||
|
||||
function adjustRGB(v: number): number {
|
||||
const corrected = gammaCorrection(v);
|
||||
|
||||
// Limits number between 0 and 1
|
||||
const limited = Math.min(Math.max(corrected, 0), 1);
|
||||
|
||||
return Math.round(limited * 255);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,7 +14,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
// @ts-ignore - `.ts` is needed here to make TS happy
|
||||
import IndexedDBWorker from "../workers/indexeddb.worker.ts";
|
||||
import { createClient, ICreateClientOpts } from "matrix-js-sdk/src/matrix";
|
||||
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
|
||||
import { WebStorageSessionStore } from "matrix-js-sdk/src/store/session/webstorage";
|
||||
import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb";
|
||||
|
||||
const localStorage = window.localStorage;
|
||||
|
||||
|
@ -32,39 +37,34 @@ try {
|
|||
* @param {Object} opts options to pass to Matrix.createClient. This will be
|
||||
* extended with `sessionStore` and `store` members.
|
||||
*
|
||||
* @property {string} indexedDbWorkerScript Optional URL for a web worker script
|
||||
* for IndexedDB store operations. By default, indexeddb ops are done on
|
||||
* the main thread.
|
||||
*
|
||||
* @returns {MatrixClient} the newly-created MatrixClient
|
||||
*/
|
||||
export default function createMatrixClient(opts) {
|
||||
const storeOpts = {
|
||||
export default function createMatrixClient(opts: ICreateClientOpts) {
|
||||
const storeOpts: Partial<ICreateClientOpts> = {
|
||||
useAuthorizationHeader: true,
|
||||
};
|
||||
|
||||
if (indexedDB && localStorage) {
|
||||
storeOpts.store = new Matrix.IndexedDBStore({
|
||||
storeOpts.store = new IndexedDBStore({
|
||||
indexedDB: indexedDB,
|
||||
dbName: "riot-web-sync",
|
||||
localStorage: localStorage,
|
||||
workerScript: createMatrixClient.indexedDbWorkerScript,
|
||||
workerFactory: () => new IndexedDBWorker(),
|
||||
});
|
||||
}
|
||||
|
||||
if (localStorage) {
|
||||
storeOpts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
|
||||
storeOpts.sessionStore = new WebStorageSessionStore(localStorage);
|
||||
}
|
||||
|
||||
if (indexedDB) {
|
||||
storeOpts.cryptoStore = new Matrix.IndexedDBCryptoStore(
|
||||
storeOpts.cryptoStore = new IndexedDBCryptoStore(
|
||||
indexedDB, "matrix-js-sdk:crypto",
|
||||
);
|
||||
}
|
||||
|
||||
opts = Object.assign(storeOpts, opts);
|
||||
|
||||
return Matrix.createClient(opts);
|
||||
return createClient({
|
||||
...storeOpts,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
|
||||
createMatrixClient.indexedDbWorkerScript = null;
|
49
src/utils/enums.ts
Normal file
49
src/utils/enums.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the values for an enum.
|
||||
* @param e The enum.
|
||||
* @returns The enum values.
|
||||
*/
|
||||
export function getEnumValues(e: any): (string | number)[] {
|
||||
// String-based enums will simply be objects ({Key: "value"}), but number-based
|
||||
// enums will instead map themselves twice: in one direction for {Key: 12} and
|
||||
// the reverse for easy lookup, presumably ({12: Key}). In the reverse mapping,
|
||||
// the key is a string, not a number.
|
||||
//
|
||||
// For this reason, we try to determine what kind of enum we're dealing with.
|
||||
|
||||
const keys = Object.keys(e);
|
||||
const values: (string | number)[] = [];
|
||||
for (const key of keys) {
|
||||
const value = e[key];
|
||||
if (Number.isFinite(value) || e[value.toString()] !== Number(key)) {
|
||||
values.push(value);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given value is a valid value for the provided enum.
|
||||
* @param e The enum to check against.
|
||||
* @param val The value to search for.
|
||||
* @returns True if the enum contains the value.
|
||||
*/
|
||||
export function isEnumValue<T>(e: T, val: string | number): boolean {
|
||||
return getEnumValues(e).includes(val);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {_t} from "../languageHandler";
|
||||
import { _t } from "../languageHandler";
|
||||
|
||||
// These are the constants we use for when to break the text
|
||||
const MILLISECONDS_RECENT = 15000;
|
||||
const MILLISECONDS_1_MIN = 75000;
|
||||
const MINUTES_UNDER_1_HOUR = 45;
|
||||
const MINUTES_1_HOUR = 75;
|
||||
const HOURS_UNDER_1_DAY = 23;
|
||||
const HOURS_1_DAY = 26;
|
||||
|
||||
/**
|
||||
* Converts a timestamp into human-readable, translated, text.
|
||||
* @param {number} timeMillis The time in millis to compare against.
|
||||
* @returns {string} The humanized time.
|
||||
*/
|
||||
export function humanizeTime(timeMillis) {
|
||||
// These are the constants we use for when to break the text
|
||||
const MILLISECONDS_RECENT = 15000;
|
||||
const MILLISECONDS_1_MIN = 75000;
|
||||
const MINUTES_UNDER_1_HOUR = 45;
|
||||
const MINUTES_1_HOUR = 75;
|
||||
const HOURS_UNDER_1_DAY = 23;
|
||||
const HOURS_1_DAY = 26;
|
||||
|
||||
export function humanizeTime(timeMillis: number): string {
|
||||
const now = (new Date()).getTime();
|
||||
let msAgo = now - timeMillis;
|
||||
const minutes = Math.abs(Math.ceil(msAgo / 60000));
|
||||
|
@ -39,19 +39,19 @@ export function humanizeTime(timeMillis) {
|
|||
if (msAgo >= 0) { // Past
|
||||
if (msAgo <= MILLISECONDS_RECENT) return _t("a few seconds ago");
|
||||
if (msAgo <= MILLISECONDS_1_MIN) return _t("about a minute ago");
|
||||
if (minutes <= MINUTES_UNDER_1_HOUR) return _t("%(num)s minutes ago", {num: minutes});
|
||||
if (minutes <= MINUTES_UNDER_1_HOUR) return _t("%(num)s minutes ago", { num: minutes });
|
||||
if (minutes <= MINUTES_1_HOUR) return _t("about an hour ago");
|
||||
if (hours <= HOURS_UNDER_1_DAY) return _t("%(num)s hours ago", {num: hours});
|
||||
if (hours <= HOURS_UNDER_1_DAY) return _t("%(num)s hours ago", { num: hours });
|
||||
if (hours <= HOURS_1_DAY) return _t("about a day ago");
|
||||
return _t("%(num)s days ago", {num: days});
|
||||
return _t("%(num)s days ago", { num: days });
|
||||
} else { // Future
|
||||
msAgo = Math.abs(msAgo);
|
||||
if (msAgo <= MILLISECONDS_RECENT) return _t("a few seconds from now");
|
||||
if (msAgo <= MILLISECONDS_1_MIN) return _t("about a minute from now");
|
||||
if (minutes <= MINUTES_UNDER_1_HOUR) return _t("%(num)s minutes from now", {num: minutes});
|
||||
if (minutes <= MINUTES_UNDER_1_HOUR) return _t("%(num)s minutes from now", { num: minutes });
|
||||
if (minutes <= MINUTES_1_HOUR) return _t("about an hour from now");
|
||||
if (hours <= HOURS_UNDER_1_DAY) return _t("%(num)s hours from now", {num: hours});
|
||||
if (hours <= HOURS_UNDER_1_DAY) return _t("%(num)s hours from now", { num: hours });
|
||||
if (hours <= HOURS_1_DAY) return _t("about a day from now");
|
||||
return _t("%(num)s days from now", {num: days});
|
||||
return _t("%(num)s days from now", { num: days });
|
||||
}
|
||||
}
|
25
src/utils/iterables.ts
Normal file
25
src/utils/iterables.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { arrayDiff, arrayUnion } from "./arrays";
|
||||
|
||||
export function iterableUnion<T>(a: Iterable<T>, b: Iterable<T>): Iterable<T> {
|
||||
return arrayUnion(Array.from(a), Array.from(b));
|
||||
}
|
||||
|
||||
export function iterableDiff<T>(a: Iterable<T>, b: Iterable<T>): { added: Iterable<T>, removed: Iterable<T> } {
|
||||
return arrayDiff(Array.from(a), Array.from(b));
|
||||
}
|
69
src/utils/maps.ts
Normal file
69
src/utils/maps.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { arrayDiff, arrayMerge, arrayUnion } from "./arrays";
|
||||
|
||||
/**
|
||||
* Determines the keys added, changed, and removed between two Maps.
|
||||
* For changes, simple triple equal comparisons are done, not in-depth tree checking.
|
||||
* @param a The first Map. Must be defined.
|
||||
* @param b The second Map. Must be defined.
|
||||
* @returns The difference between the keys of each Map.
|
||||
*/
|
||||
export function mapDiff<K, V>(a: Map<K, V>, b: Map<K, V>): { changed: K[], added: K[], removed: K[] } {
|
||||
const aKeys = [...a.keys()];
|
||||
const bKeys = [...b.keys()];
|
||||
const keyDiff = arrayDiff(aKeys, bKeys);
|
||||
const possibleChanges = arrayUnion(aKeys, bKeys);
|
||||
const changes = possibleChanges.filter(k => a.get(k) !== b.get(k));
|
||||
|
||||
return { changed: changes, added: keyDiff.added, removed: keyDiff.removed };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the key changes (added, removed, or value difference) between two Maps.
|
||||
* Triple equals is used to compare values, not in-depth tree checking.
|
||||
* @param a The first Map. Must be defined.
|
||||
* @param b The second Map. Must be defined.
|
||||
* @returns The keys which have been added, removed, or changed between the two Maps.
|
||||
*/
|
||||
export function mapKeyChanges<K, V>(a: Map<K, V>, b: Map<K, V>): K[] {
|
||||
const diff = mapDiff(a, b);
|
||||
return arrayMerge(diff.removed, diff.added, diff.changed);
|
||||
}
|
||||
|
||||
/**
|
||||
* A Map<K, V> with added utility.
|
||||
*/
|
||||
export class EnhancedMap<K, V> extends Map<K, V> {
|
||||
public constructor(entries?: Iterable<[K, V]>) {
|
||||
super(entries);
|
||||
}
|
||||
|
||||
public getOrCreate(key: K, def: V): V {
|
||||
if (this.has(key)) {
|
||||
return this.get(key);
|
||||
}
|
||||
this.set(key, def);
|
||||
return def;
|
||||
}
|
||||
|
||||
public remove(key: K): V {
|
||||
const v = this.get(key);
|
||||
this.delete(key);
|
||||
return v;
|
||||
}
|
||||
}
|
145
src/utils/membership.ts
Normal file
145
src/utils/membership.ts
Normal file
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { _t } from "../languageHandler";
|
||||
import Modal from "../Modal";
|
||||
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
|
||||
import React from "react";
|
||||
import dis from "../dispatcher/dispatcher";
|
||||
import RoomViewStore from "../stores/RoomViewStore";
|
||||
|
||||
/**
|
||||
* Approximation of a membership status for a given room.
|
||||
*/
|
||||
export enum EffectiveMembership {
|
||||
/**
|
||||
* The user is effectively joined to the room. For example, actually joined
|
||||
* or knocking on the room (when that becomes possible).
|
||||
*/
|
||||
Join = "JOIN",
|
||||
|
||||
/**
|
||||
* The user is effectively invited to the room. Currently this is a direct map
|
||||
* to the invite membership as no other membership states are effectively
|
||||
* invites.
|
||||
*/
|
||||
Invite = "INVITE",
|
||||
|
||||
/**
|
||||
* The user is effectively no longer in the room. For example, kicked,
|
||||
* banned, or voluntarily left.
|
||||
*/
|
||||
Leave = "LEAVE",
|
||||
}
|
||||
|
||||
export interface MembershipSplit {
|
||||
// @ts-ignore - TS wants this to be a string key, but we know better.
|
||||
[state: EffectiveMembership]: Room[];
|
||||
}
|
||||
|
||||
export function splitRoomsByMembership(rooms: Room[]): MembershipSplit {
|
||||
const split: MembershipSplit = {
|
||||
[EffectiveMembership.Invite]: [],
|
||||
[EffectiveMembership.Join]: [],
|
||||
[EffectiveMembership.Leave]: [],
|
||||
};
|
||||
|
||||
for (const room of rooms) {
|
||||
split[getEffectiveMembership(room.getMyMembership())].push(room);
|
||||
}
|
||||
|
||||
return split;
|
||||
}
|
||||
|
||||
export function getEffectiveMembership(membership: string): EffectiveMembership {
|
||||
if (membership === 'invite') {
|
||||
return EffectiveMembership.Invite;
|
||||
} else if (membership === 'join') {
|
||||
// TODO: Include knocks? Update docs as needed in the enum. https://github.com/vector-im/element-web/issues/14237
|
||||
return EffectiveMembership.Join;
|
||||
} else {
|
||||
// Probably a leave, kick, or ban
|
||||
return EffectiveMembership.Leave;
|
||||
}
|
||||
}
|
||||
|
||||
export function isJoinedOrNearlyJoined(membership: string): boolean {
|
||||
const effective = getEffectiveMembership(membership);
|
||||
return effective === EffectiveMembership.Join || effective === EffectiveMembership.Invite;
|
||||
}
|
||||
|
||||
export async function leaveRoomBehaviour(roomId: string) {
|
||||
let leavingAllVersions = true;
|
||||
const history = await MatrixClientPeg.get().getRoomUpgradeHistory(roomId);
|
||||
if (history && history.length > 0) {
|
||||
const currentRoom = history[history.length - 1];
|
||||
if (currentRoom.roomId !== roomId) {
|
||||
// The user is trying to leave an older version of the room. Let them through
|
||||
// without making them leave the current version of the room.
|
||||
leavingAllVersions = false;
|
||||
}
|
||||
}
|
||||
|
||||
let results: { [roomId: string]: Error & { errcode: string, message: string } } = {};
|
||||
if (!leavingAllVersions) {
|
||||
try {
|
||||
await MatrixClientPeg.get().leave(roomId);
|
||||
} catch (e) {
|
||||
if (e && e.data && e.data.errcode) {
|
||||
const message = e.data.error || _t("Unexpected server error trying to leave the room");
|
||||
results[roomId] = Object.assign(new Error(message), { errcode: e.data.errcode });
|
||||
} else {
|
||||
results[roomId] = e || new Error("Failed to leave room for unknown causes");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
results = await MatrixClientPeg.get().leaveRoomChain(roomId);
|
||||
}
|
||||
|
||||
const errors = Object.entries(results).filter(r => !!r[1]);
|
||||
if (errors.length > 0) {
|
||||
const messages = [];
|
||||
for (const roomErr of errors) {
|
||||
const err = roomErr[1]; // [0] is the roomId
|
||||
let message = _t("Unexpected server error trying to leave the room");
|
||||
if (err.errcode && err.message) {
|
||||
if (err.errcode === 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') {
|
||||
Modal.createTrackedDialog('Error Leaving Room', '', ErrorDialog, {
|
||||
title: _t("Can't leave Server Notices room"),
|
||||
description: _t(
|
||||
"This room is used for important messages from the Homeserver, " +
|
||||
"so you cannot leave it.",
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
message = results[roomId].message;
|
||||
}
|
||||
messages.push(message, React.createElement('BR')); // createElement to avoid using a tsx file in utils
|
||||
}
|
||||
Modal.createTrackedDialog('Error Leaving Room', '', ErrorDialog, {
|
||||
title: _t("Error leaving room"),
|
||||
description: messages,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (RoomViewStore.getRoomId() === roomId) {
|
||||
dis.dispatch({ action: 'view_home_page' });
|
||||
}
|
||||
}
|
42
src/utils/numbers.ts
Normal file
42
src/utils/numbers.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the default number if the given value, i, is not a number. Otherwise
|
||||
* returns the given value.
|
||||
* @param {*} i The value to check.
|
||||
* @param {number} def The default value.
|
||||
* @returns {number} Either the value or the default value, whichever is a number.
|
||||
*/
|
||||
export function defaultNumber(i: unknown, def: number): number {
|
||||
return Number.isFinite(i) ? Number(i) : def;
|
||||
}
|
||||
|
||||
export function clamp(i: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(i, min), max);
|
||||
}
|
||||
|
||||
export function sum(...i: number[]): number {
|
||||
return [...i].reduce((p, c) => c + p, 0);
|
||||
}
|
||||
|
||||
export function percentageWithin(pct: number, min: number, max: number): number {
|
||||
return (pct * (max - min)) + min;
|
||||
}
|
||||
|
||||
export function percentageOf(val: number, min: number, max: number): number {
|
||||
return (val - min) / (max - min);
|
||||
}
|
161
src/utils/objects.ts
Normal file
161
src/utils/objects.ts
Normal file
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { arrayDiff, arrayMerge, arrayUnion } from "./arrays";
|
||||
|
||||
type ObjectExcluding<O extends {}, P extends (keyof O)[]> = {[k in Exclude<keyof O, P[number]>]: O[k]};
|
||||
|
||||
/**
|
||||
* Gets a new object which represents the provided object, excluding some properties.
|
||||
* @param a The object to strip properties of. Must be defined.
|
||||
* @param props The property names to remove.
|
||||
* @returns The new object without the provided properties.
|
||||
*/
|
||||
export function objectExcluding<O extends {}, P extends Array<keyof O>>(a: O, props: P): ObjectExcluding<O, P> {
|
||||
// We use a Map to avoid hammering the `delete` keyword, which is slow and painful.
|
||||
const tempMap = new Map<keyof O, any>(Object.entries(a) as [keyof O, any][]);
|
||||
for (const prop of props) {
|
||||
tempMap.delete(prop);
|
||||
}
|
||||
|
||||
// Convert the map to an object again
|
||||
return Array.from(tempMap.entries()).reduce((c, [k, v]) => {
|
||||
c[k] = v;
|
||||
return c;
|
||||
}, {} as O);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a new object which represents the provided object, with only some properties
|
||||
* included.
|
||||
* @param a The object to clone properties of. Must be defined.
|
||||
* @param props The property names to keep.
|
||||
* @returns The new object with only the provided properties.
|
||||
*/
|
||||
export function objectWithOnly<O extends {}, P extends Array<keyof O>>(a: O, props: P): {[k in P[number]]: O[k]} {
|
||||
const existingProps = Object.keys(a) as (keyof O)[];
|
||||
const diff = arrayDiff(existingProps, props);
|
||||
if (diff.removed.length === 0) {
|
||||
return objectShallowClone(a);
|
||||
} else {
|
||||
return objectExcluding(a, diff.removed) as {[k in P[number]]: O[k]};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones an object to a caller-controlled depth. When a propertyCloner is supplied, the
|
||||
* object's properties will be passed through it with the return value used as the new
|
||||
* object's type. This is intended to be used to deep clone a reference, but without
|
||||
* having to deep clone the entire object. This function is safe to call recursively within
|
||||
* the propertyCloner.
|
||||
* @param a The object to clone. Must be defined.
|
||||
* @param propertyCloner The function to clone the properties of the object with, optionally.
|
||||
* First argument is the property key with the second being the current value.
|
||||
* @returns A cloned object.
|
||||
*/
|
||||
export function objectShallowClone<O extends {}>(a: O, propertyCloner?: (k: keyof O, v: O[keyof O]) => any): O {
|
||||
const newObj = {} as O;
|
||||
for (const [k, v] of Object.entries(a) as [keyof O, O[keyof O]][]) {
|
||||
newObj[k] = v;
|
||||
if (propertyCloner) {
|
||||
newObj[k] = propertyCloner(k, v);
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if any keys were added, removed, or changed between two objects.
|
||||
* For changes, simple triple equal comparisons are done, not in-depth
|
||||
* tree checking.
|
||||
* @param a The first object. Must be defined.
|
||||
* @param b The second object. Must be defined.
|
||||
* @returns True if there's a difference between the objects, false otherwise
|
||||
*/
|
||||
export function objectHasDiff<O extends {}>(a: O, b: O): boolean {
|
||||
if (a === b) return false;
|
||||
const aKeys = Object.keys(a);
|
||||
const bKeys = Object.keys(b);
|
||||
if (aKeys.length !== bKeys.length) return true;
|
||||
const possibleChanges = arrayUnion(aKeys, bKeys);
|
||||
// if the amalgamation of both sets of keys has the a different length to the inputs then there must be a change
|
||||
if (possibleChanges.length !== aKeys.length) return true;
|
||||
|
||||
return possibleChanges.some(k => a[k] !== b[k]);
|
||||
}
|
||||
|
||||
type Diff<K> = { changed: K[], added: K[], removed: K[] };
|
||||
|
||||
/**
|
||||
* Determines the keys added, changed, and removed between two objects.
|
||||
* For changes, simple triple equal comparisons are done, not in-depth
|
||||
* tree checking.
|
||||
* @param a The first object. Must be defined.
|
||||
* @param b The second object. Must be defined.
|
||||
* @returns The difference between the keys of each object.
|
||||
*/
|
||||
export function objectDiff<O extends {}>(a: O, b: O): Diff<keyof O> {
|
||||
const aKeys = Object.keys(a) as (keyof O)[];
|
||||
const bKeys = Object.keys(b) as (keyof O)[];
|
||||
const keyDiff = arrayDiff(aKeys, bKeys);
|
||||
const possibleChanges = arrayUnion(aKeys, bKeys);
|
||||
const changes = possibleChanges.filter(k => a[k] !== b[k]);
|
||||
|
||||
return { changed: changes, added: keyDiff.added, removed: keyDiff.removed };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the key changes (added, removed, or value difference) between
|
||||
* two objects. Triple equals is used to compare values, not in-depth tree
|
||||
* checking.
|
||||
* @param a The first object. Must be defined.
|
||||
* @param b The second object. Must be defined.
|
||||
* @returns The keys which have been added, removed, or changed between the
|
||||
* two objects.
|
||||
*/
|
||||
export function objectKeyChanges<O extends {}>(a: O, b: O): (keyof O)[] {
|
||||
const diff = objectDiff(a, b);
|
||||
return arrayMerge(diff.removed, diff.added, diff.changed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones an object by running it through JSON parsing. Note that this
|
||||
* will destroy any complicated object types which do not translate to
|
||||
* JSON.
|
||||
* @param obj The object to clone.
|
||||
* @returns The cloned object
|
||||
*/
|
||||
export function objectClone<O extends {}>(obj: O): O {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a series of entries to an object.
|
||||
* @param entries The entries to convert.
|
||||
* @returns The converted object.
|
||||
*/
|
||||
// NOTE: Deprecated once we have Object.fromEntries() support.
|
||||
// @ts-ignore - return type is complaining about non-string keys, but we know better
|
||||
export function objectFromEntries<K, V>(entries: Iterable<[K, V]>): {[k: K]: V} {
|
||||
const obj: {
|
||||
// @ts-ignore - same as return type
|
||||
[k: K]: V;} = {};
|
||||
for (const e of entries) {
|
||||
// @ts-ignore - same as return type
|
||||
obj[e[0]] = e[1];
|
||||
}
|
||||
return obj;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
export function getHomePageUrl(appConfig) {
|
||||
import { ConfigOptions } from "../SdkConfig";
|
||||
|
||||
export function getHomePageUrl(appConfig: ConfigOptions): string | null {
|
||||
const pagesConfig = appConfig.embeddedPages;
|
||||
let pageUrl = null;
|
||||
if (pagesConfig) {
|
||||
pageUrl = pagesConfig.homeUrl;
|
||||
}
|
||||
let pageUrl = pagesConfig?.homeUrl;
|
||||
|
||||
if (!pageUrl) {
|
||||
// This is a deprecated config option for the home page
|
||||
// (despite the name, given we also now have a welcome
|
||||
|
@ -29,3 +29,8 @@ export function getHomePageUrl(appConfig) {
|
|||
|
||||
return pageUrl;
|
||||
}
|
||||
|
||||
export function shouldUseLoginForWelcome(appConfig: ConfigOptions): boolean {
|
||||
const pagesConfig = appConfig.embeddedPages;
|
||||
return pagesConfig?.loginForWelcome === true;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,37 +14,37 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import PermalinkConstructor, {PermalinkParts} from "./PermalinkConstructor";
|
||||
import PermalinkConstructor, { PermalinkParts } from "./PermalinkConstructor";
|
||||
|
||||
/**
|
||||
* Generates permalinks that self-reference the running webapp
|
||||
*/
|
||||
export default class RiotPermalinkConstructor extends PermalinkConstructor {
|
||||
_riotUrl: string;
|
||||
export default class ElementPermalinkConstructor extends PermalinkConstructor {
|
||||
private elementUrl: string;
|
||||
|
||||
constructor(riotUrl: string) {
|
||||
constructor(elementUrl: string) {
|
||||
super();
|
||||
this._riotUrl = riotUrl;
|
||||
this.elementUrl = elementUrl;
|
||||
|
||||
if (!this._riotUrl.startsWith("http:") && !this._riotUrl.startsWith("https:")) {
|
||||
throw new Error("Riot prefix URL does not appear to be an HTTP(S) URL");
|
||||
if (!this.elementUrl.startsWith("http:") && !this.elementUrl.startsWith("https:")) {
|
||||
throw new Error("Element prefix URL does not appear to be an HTTP(S) URL");
|
||||
}
|
||||
}
|
||||
|
||||
forEvent(roomId: string, eventId: string, serverCandidates: string[]): string {
|
||||
return `${this._riotUrl}/#/room/${roomId}/${eventId}${this.encodeServerCandidates(serverCandidates)}`;
|
||||
return `${this.elementUrl}/#/room/${roomId}/${eventId}${this.encodeServerCandidates(serverCandidates)}`;
|
||||
}
|
||||
|
||||
forRoom(roomIdOrAlias: string, serverCandidates: string[]): string {
|
||||
return `${this._riotUrl}/#/room/${roomIdOrAlias}${this.encodeServerCandidates(serverCandidates)}`;
|
||||
forRoom(roomIdOrAlias: string, serverCandidates?: string[]): string {
|
||||
return `${this.elementUrl}/#/room/${roomIdOrAlias}${this.encodeServerCandidates(serverCandidates)}`;
|
||||
}
|
||||
|
||||
forUser(userId: string): string {
|
||||
return `${this._riotUrl}/#/user/${userId}`;
|
||||
return `${this.elementUrl}/#/user/${userId}`;
|
||||
}
|
||||
|
||||
forGroup(groupId: string): string {
|
||||
return `${this._riotUrl}/#/group/${groupId}`;
|
||||
return `${this.elementUrl}/#/group/${groupId}`;
|
||||
}
|
||||
|
||||
forEntity(entityId: string): string {
|
||||
|
@ -58,28 +58,45 @@ export default class RiotPermalinkConstructor extends PermalinkConstructor {
|
|||
}
|
||||
|
||||
isPermalinkHost(testHost: string): boolean {
|
||||
const parsedUrl = new URL(this._riotUrl);
|
||||
const parsedUrl = new URL(this.elementUrl);
|
||||
return testHost === (parsedUrl.host || parsedUrl.hostname); // one of the hosts should match
|
||||
}
|
||||
|
||||
encodeServerCandidates(candidates: string[]) {
|
||||
encodeServerCandidates(candidates?: string[]) {
|
||||
if (!candidates || candidates.length === 0) return '';
|
||||
return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
|
||||
}
|
||||
|
||||
// Heavily inspired by/borrowed from the matrix-bot-sdk (with permission):
|
||||
// https://github.com/turt2live/matrix-js-bot-sdk/blob/7c4665c9a25c2c8e0fe4e509f2616505b5b66a1c/src/Permalinks.ts#L33-L61
|
||||
// Adapted for Riot's URL format
|
||||
// Adapted for Element's URL format
|
||||
parsePermalink(fullUrl: string): PermalinkParts {
|
||||
if (!fullUrl || !fullUrl.startsWith(this._riotUrl)) {
|
||||
if (!fullUrl || !fullUrl.startsWith(this.elementUrl)) {
|
||||
throw new Error("Does not appear to be a permalink");
|
||||
}
|
||||
|
||||
const parts = fullUrl.substring(`${this._riotUrl}/#/`.length).split("/");
|
||||
const parts = fullUrl.substring(`${this.elementUrl}/#/`.length);
|
||||
return ElementPermalinkConstructor.parseAppRoute(parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an app route (`(user|room|group)/identifer`) to a Matrix entity
|
||||
* (room, user, group).
|
||||
* @param {string} route The app route
|
||||
* @returns {PermalinkParts}
|
||||
*/
|
||||
static parseAppRoute(route: string): PermalinkParts {
|
||||
const parts = route.split("/");
|
||||
|
||||
if (parts.length < 2) { // we're expecting an entity and an ID of some kind at least
|
||||
throw new Error("URL is missing parts");
|
||||
}
|
||||
|
||||
// Split optional query out of last part
|
||||
const [lastPartMaybeWithQuery] = parts.splice(-1, 1);
|
||||
const [lastPart, query = ""] = lastPartMaybeWithQuery.split("?");
|
||||
parts.push(lastPart);
|
||||
|
||||
const entityType = parts[0];
|
||||
const entity = parts[1];
|
||||
if (entityType === 'user') {
|
||||
|
@ -89,20 +106,9 @@ export default class RiotPermalinkConstructor extends PermalinkConstructor {
|
|||
// Probably a group, no further parsing needed.
|
||||
return PermalinkParts.forGroup(entity);
|
||||
} else if (entityType === 'room') {
|
||||
if (parts.length === 2) {
|
||||
return PermalinkParts.forRoom(entity, []);
|
||||
}
|
||||
|
||||
// rejoin the rest because v3 events can have slashes (annoyingly)
|
||||
const eventIdAndQuery = parts.length > 2 ? parts.slice(2).join('/') : "";
|
||||
const secondaryParts = eventIdAndQuery.split("?");
|
||||
|
||||
const eventId = secondaryParts[0];
|
||||
const query = secondaryParts.length > 1 ? secondaryParts[1] : "";
|
||||
|
||||
// TODO: Verify Riot works with via args
|
||||
const via = query.split("via=").filter(p => !!p);
|
||||
|
||||
// Rejoin the rest because v3 events can have slashes (annoyingly)
|
||||
const eventId = parts.length > 2 ? parts.slice(2).join('/') : "";
|
||||
const via = query.split(/&?via=/).filter(p => !!p);
|
||||
return PermalinkParts.forEvent(entity, eventId, via);
|
||||
} else {
|
||||
throw new Error("Unknown entity type in permalink");
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -19,11 +19,11 @@ limitations under the License.
|
|||
* TODO: Convert this to a real TypeScript interface
|
||||
*/
|
||||
export default class PermalinkConstructor {
|
||||
forEvent(roomId: string, eventId: string, serverCandidates: string[]): string {
|
||||
forEvent(roomId: string, eventId: string, serverCandidates: string[] = []): string {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
forRoom(roomIdOrAlias: string, serverCandidates: string[]): string {
|
||||
forRoom(roomIdOrAlias: string, serverCandidates: string[] = []): string {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
|
@ -73,11 +73,19 @@ export class PermalinkParts {
|
|||
return new PermalinkParts(null, null, null, groupId, null);
|
||||
}
|
||||
|
||||
static forRoom(roomIdOrAlias: string, viaServers: string[]): PermalinkParts {
|
||||
return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers || []);
|
||||
static forRoom(roomIdOrAlias: string, viaServers: string[] = []): PermalinkParts {
|
||||
return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers);
|
||||
}
|
||||
|
||||
static forEvent(roomId: string, eventId: string, viaServers: string[]): PermalinkParts {
|
||||
return new PermalinkParts(roomId, eventId, null, null, viaServers || []);
|
||||
static forEvent(roomId: string, eventId: string, viaServers: string[] = []): PermalinkParts {
|
||||
return new PermalinkParts(roomId, eventId, null, null, viaServers);
|
||||
}
|
||||
|
||||
get primaryEntityId(): string {
|
||||
return this.roomIdOrAlias || this.userId || this.groupId;
|
||||
}
|
||||
|
||||
get sigil(): string {
|
||||
return this.primaryEntityId[0];
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,12 +14,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import isIp from "is-ip";
|
||||
import * as utils from 'matrix-js-sdk/src/utils';
|
||||
import SpecPermalinkConstructor, {baseUrl as matrixtoBaseUrl} from "./SpecPermalinkConstructor";
|
||||
import PermalinkConstructor, {PermalinkParts} from "./PermalinkConstructor";
|
||||
import RiotPermalinkConstructor from "./RiotPermalinkConstructor";
|
||||
import * as utils from "matrix-js-sdk/src/utils";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import SpecPermalinkConstructor, { baseUrl as matrixtoBaseUrl } from "./SpecPermalinkConstructor";
|
||||
import PermalinkConstructor, { PermalinkParts } from "./PermalinkConstructor";
|
||||
import ElementPermalinkConstructor from "./ElementPermalinkConstructor";
|
||||
import matrixLinkify from "../../linkify-matrix";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
|
||||
|
@ -27,7 +32,6 @@ import SdkConfig from "../../SdkConfig";
|
|||
// to add to permalinks. The servers are appended as ?via=example.org
|
||||
const MAX_SERVER_CANDIDATES = 3;
|
||||
|
||||
|
||||
// Permalinks can have servers appended to them so that the user
|
||||
// receiving them can have a fighting chance at joining the room.
|
||||
// These servers are called "candidates" at this point because
|
||||
|
@ -72,29 +76,35 @@ const MAX_SERVER_CANDIDATES = 3;
|
|||
// the list and magically have the link work.
|
||||
|
||||
export class RoomPermalinkCreator {
|
||||
private room: Room;
|
||||
private roomId: string;
|
||||
private highestPlUserId: string;
|
||||
private populationMap: { [serverName: string]: number };
|
||||
private bannedHostsRegexps: RegExp[];
|
||||
private allowedHostsRegexps: RegExp[];
|
||||
private _serverCandidates: string[];
|
||||
private started: boolean;
|
||||
|
||||
// We support being given a roomId as a fallback in the event the `room` object
|
||||
// doesn't exist or is not healthy for us to rely on. For example, loading a
|
||||
// permalink to a room which the MatrixClient doesn't know about.
|
||||
constructor(room, roomId = null) {
|
||||
this._room = room;
|
||||
this._roomId = room ? room.roomId : roomId;
|
||||
this._highestPlUserId = null;
|
||||
this._populationMap = null;
|
||||
this._bannedHostsRegexps = null;
|
||||
this._allowedHostsRegexps = null;
|
||||
constructor(room: Room, roomId: string = null) {
|
||||
this.room = room;
|
||||
this.roomId = room ? room.roomId : roomId;
|
||||
this.highestPlUserId = null;
|
||||
this.populationMap = null;
|
||||
this.bannedHostsRegexps = null;
|
||||
this.allowedHostsRegexps = null;
|
||||
this._serverCandidates = null;
|
||||
this._started = false;
|
||||
this.started = false;
|
||||
|
||||
if (!this._roomId) {
|
||||
if (!this.roomId) {
|
||||
throw new Error("Failed to resolve a roomId for the permalink creator to use");
|
||||
}
|
||||
|
||||
this.onMembership = this.onMembership.bind(this);
|
||||
this.onRoomState = this.onRoomState.bind(this);
|
||||
}
|
||||
|
||||
load() {
|
||||
if (!this._room || !this._room.currentState) {
|
||||
if (!this.room || !this.room.currentState) {
|
||||
// Under rare and unknown circumstances it is possible to have a room with no
|
||||
// currentState, at least potentially at the early stages of joining a room.
|
||||
// To avoid breaking everything, we'll just warn rather than throw as well as
|
||||
|
@ -102,53 +112,68 @@ export class RoomPermalinkCreator {
|
|||
console.warn("Tried to load a permalink creator with no room state");
|
||||
return;
|
||||
}
|
||||
this._updateAllowedServers();
|
||||
this._updateHighestPlUser();
|
||||
this._updatePopulationMap();
|
||||
this._updateServerCandidates();
|
||||
this.updateAllowedServers();
|
||||
this.updateHighestPlUser();
|
||||
this.updatePopulationMap();
|
||||
this.updateServerCandidates();
|
||||
}
|
||||
|
||||
start() {
|
||||
this.load();
|
||||
this._room.on("RoomMember.membership", this.onMembership);
|
||||
this._room.on("RoomState.events", this.onRoomState);
|
||||
this._started = true;
|
||||
this.room.on("RoomMember.membership", this.onMembership);
|
||||
this.room.on("RoomState.events", this.onRoomState);
|
||||
this.started = true;
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._room.removeListener("RoomMember.membership", this.onMembership);
|
||||
this._room.removeListener("RoomState.events", this.onRoomState);
|
||||
this._started = false;
|
||||
this.room.removeListener("RoomMember.membership", this.onMembership);
|
||||
this.room.removeListener("RoomState.events", this.onRoomState);
|
||||
this.started = false;
|
||||
}
|
||||
|
||||
get serverCandidates() {
|
||||
return this._serverCandidates;
|
||||
}
|
||||
|
||||
isStarted() {
|
||||
return this._started;
|
||||
return this.started;
|
||||
}
|
||||
|
||||
forEvent(eventId) {
|
||||
return getPermalinkConstructor().forEvent(this._roomId, eventId, this._serverCandidates);
|
||||
forEvent(eventId: string): string {
|
||||
return getPermalinkConstructor().forEvent(this.roomId, eventId, this._serverCandidates);
|
||||
}
|
||||
|
||||
forRoom() {
|
||||
return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates);
|
||||
forShareableRoom(): string {
|
||||
if (this.room) {
|
||||
// Prefer to use canonical alias for permalink if possible
|
||||
const alias = this.room.getCanonicalAlias();
|
||||
if (alias) {
|
||||
return getPermalinkConstructor().forRoom(alias);
|
||||
}
|
||||
}
|
||||
return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);
|
||||
}
|
||||
|
||||
onRoomState(event) {
|
||||
forRoom(): string {
|
||||
return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);
|
||||
}
|
||||
|
||||
private onRoomState = (event: MatrixEvent) => {
|
||||
switch (event.getType()) {
|
||||
case "m.room.server_acl":
|
||||
this._updateAllowedServers();
|
||||
this._updateHighestPlUser();
|
||||
this._updatePopulationMap();
|
||||
this._updateServerCandidates();
|
||||
case EventType.RoomServerAcl:
|
||||
this.updateAllowedServers();
|
||||
this.updateHighestPlUser();
|
||||
this.updatePopulationMap();
|
||||
this.updateServerCandidates();
|
||||
return;
|
||||
case "m.room.power_levels":
|
||||
this._updateHighestPlUser();
|
||||
this._updateServerCandidates();
|
||||
case EventType.RoomPowerLevels:
|
||||
this.updateHighestPlUser();
|
||||
this.updateServerCandidates();
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMembership(evt, member, oldMembership) {
|
||||
private onMembership = (evt: MatrixEvent, member: RoomMember, oldMembership: string) => {
|
||||
const userId = member.userId;
|
||||
const membership = member.membership;
|
||||
const serverName = getServerName(userId);
|
||||
|
@ -156,17 +181,17 @@ export class RoomPermalinkCreator {
|
|||
const hasLeft = oldMembership === "join" && membership !== "join";
|
||||
|
||||
if (hasLeft) {
|
||||
this._populationMap[serverName]--;
|
||||
this.populationMap[serverName]--;
|
||||
} else if (hasJoined) {
|
||||
this._populationMap[serverName]++;
|
||||
this.populationMap[serverName]++;
|
||||
}
|
||||
|
||||
this._updateHighestPlUser();
|
||||
this._updateServerCandidates();
|
||||
}
|
||||
this.updateHighestPlUser();
|
||||
this.updateServerCandidates();
|
||||
};
|
||||
|
||||
_updateHighestPlUser() {
|
||||
const plEvent = this._room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
private updateHighestPlUser() {
|
||||
const plEvent = this.room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (plEvent) {
|
||||
const content = plEvent.getContent();
|
||||
if (content) {
|
||||
|
@ -174,14 +199,14 @@ export class RoomPermalinkCreator {
|
|||
if (users) {
|
||||
const entries = Object.entries(users);
|
||||
const allowedEntries = entries.filter(([userId]) => {
|
||||
const member = this._room.getMember(userId);
|
||||
const member = this.room.getMember(userId);
|
||||
if (!member || member.membership !== "join") {
|
||||
return false;
|
||||
}
|
||||
const serverName = getServerName(userId);
|
||||
return !isHostnameIpAddress(serverName) &&
|
||||
!isHostInRegex(serverName, this._bannedHostsRegexps) &&
|
||||
isHostInRegex(serverName, this._allowedHostsRegexps);
|
||||
!isHostInRegex(serverName, this.bannedHostsRegexps) &&
|
||||
isHostInRegex(serverName, this.allowedHostsRegexps);
|
||||
});
|
||||
const maxEntry = allowedEntries.reduce((max, entry) => {
|
||||
return (entry[1] > max[1]) ? entry : max;
|
||||
|
@ -189,20 +214,20 @@ export class RoomPermalinkCreator {
|
|||
const [userId, powerLevel] = maxEntry;
|
||||
// object wasn't empty, and max entry wasn't a demotion from the default
|
||||
if (userId !== null && powerLevel >= 50) {
|
||||
this._highestPlUserId = userId;
|
||||
this.highestPlUserId = userId;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this._highestPlUserId = null;
|
||||
this.highestPlUserId = null;
|
||||
}
|
||||
|
||||
_updateAllowedServers() {
|
||||
private updateAllowedServers() {
|
||||
const bannedHostsRegexps = [];
|
||||
let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone
|
||||
if (this._room.currentState) {
|
||||
const aclEvent = this._room.currentState.getStateEvents("m.room.server_acl", "");
|
||||
if (this.room.currentState) {
|
||||
const aclEvent = this.room.currentState.getStateEvents("m.room.server_acl", "");
|
||||
if (aclEvent && aclEvent.getContent()) {
|
||||
const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$");
|
||||
|
||||
|
@ -214,35 +239,35 @@ export class RoomPermalinkCreator {
|
|||
allowed.forEach(h => allowedHostsRegexps.push(getRegex(h)));
|
||||
}
|
||||
}
|
||||
this._bannedHostsRegexps = bannedHostsRegexps;
|
||||
this._allowedHostsRegexps = allowedHostsRegexps;
|
||||
this.bannedHostsRegexps = bannedHostsRegexps;
|
||||
this.allowedHostsRegexps = allowedHostsRegexps;
|
||||
}
|
||||
|
||||
_updatePopulationMap() {
|
||||
private updatePopulationMap() {
|
||||
const populationMap: { [server: string]: number } = {};
|
||||
for (const member of this._room.getJoinedMembers()) {
|
||||
for (const member of this.room.getJoinedMembers()) {
|
||||
const serverName = getServerName(member.userId);
|
||||
if (!populationMap[serverName]) {
|
||||
populationMap[serverName] = 0;
|
||||
}
|
||||
populationMap[serverName]++;
|
||||
}
|
||||
this._populationMap = populationMap;
|
||||
this.populationMap = populationMap;
|
||||
}
|
||||
|
||||
_updateServerCandidates() {
|
||||
private updateServerCandidates() {
|
||||
let candidates = [];
|
||||
if (this._highestPlUserId) {
|
||||
candidates.push(getServerName(this._highestPlUserId));
|
||||
if (this.highestPlUserId) {
|
||||
candidates.push(getServerName(this.highestPlUserId));
|
||||
}
|
||||
|
||||
const serversByPopulation = Object.keys(this._populationMap)
|
||||
.sort((a, b) => this._populationMap[b] - this._populationMap[a])
|
||||
const serversByPopulation = Object.keys(this.populationMap)
|
||||
.sort((a, b) => this.populationMap[b] - this.populationMap[a])
|
||||
.filter(a => {
|
||||
return !candidates.includes(a) &&
|
||||
!isHostnameIpAddress(a) &&
|
||||
!isHostInRegex(a, this._bannedHostsRegexps) &&
|
||||
isHostInRegex(a, this._allowedHostsRegexps);
|
||||
!isHostInRegex(a, this.bannedHostsRegexps) &&
|
||||
isHostInRegex(a, this.allowedHostsRegexps);
|
||||
});
|
||||
|
||||
const remainingServers = serversByPopulation.slice(0, MAX_SERVER_CANDIDATES - candidates.length);
|
||||
|
@ -256,11 +281,11 @@ export function makeGenericPermalink(entityId: string): string {
|
|||
return getPermalinkConstructor().forEntity(entityId);
|
||||
}
|
||||
|
||||
export function makeUserPermalink(userId) {
|
||||
export function makeUserPermalink(userId: string): string {
|
||||
return getPermalinkConstructor().forUser(userId);
|
||||
}
|
||||
|
||||
export function makeRoomPermalink(roomId) {
|
||||
export function makeRoomPermalink(roomId: string): string {
|
||||
if (!roomId) {
|
||||
throw new Error("can't permalink a falsey roomId");
|
||||
}
|
||||
|
@ -276,10 +301,10 @@ export function makeRoomPermalink(roomId) {
|
|||
}
|
||||
const permalinkCreator = new RoomPermalinkCreator(room);
|
||||
permalinkCreator.load();
|
||||
return permalinkCreator.forRoom();
|
||||
return permalinkCreator.forShareableRoom();
|
||||
}
|
||||
|
||||
export function makeGroupPermalink(groupId) {
|
||||
export function makeGroupPermalink(groupId: string): string {
|
||||
return getPermalinkConstructor().forGroup(groupId);
|
||||
}
|
||||
|
||||
|
@ -320,18 +345,26 @@ export function tryTransformPermalinkToLocalHref(permalink: string): string {
|
|||
return permalink;
|
||||
}
|
||||
|
||||
const m = permalink.match(matrixLinkify.VECTOR_URL_PATTERN);
|
||||
if (m) {
|
||||
return m[1];
|
||||
try {
|
||||
const m = decodeURIComponent(permalink).match(matrixLinkify.ELEMENT_URL_PATTERN);
|
||||
if (m) {
|
||||
return m[1];
|
||||
}
|
||||
} catch (e) {
|
||||
// Not a valid URI
|
||||
return permalink;
|
||||
}
|
||||
|
||||
// A bit of a hack to convert permalinks of unknown origin to Riot links
|
||||
// A bit of a hack to convert permalinks of unknown origin to Element links
|
||||
try {
|
||||
const permalinkParts = parsePermalink(permalink);
|
||||
if (permalinkParts) {
|
||||
if (permalinkParts.roomIdOrAlias) {
|
||||
const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : '';
|
||||
permalink = `#/room/${permalinkParts.roomIdOrAlias}${eventIdPart}`;
|
||||
if (permalinkParts.viaServers.length > 0) {
|
||||
permalink += new SpecPermalinkConstructor().encodeServerCandidates(permalinkParts.viaServers);
|
||||
}
|
||||
} else if (permalinkParts.groupId) {
|
||||
permalink = `#/group/${permalinkParts.groupId}`;
|
||||
} else if (permalinkParts.userId) {
|
||||
|
@ -351,10 +384,10 @@ export function getPrimaryPermalinkEntity(permalink: string): string {
|
|||
|
||||
// If not a permalink, try the vector patterns.
|
||||
if (!permalinkParts) {
|
||||
const m = permalink.match(matrixLinkify.VECTOR_URL_PATTERN);
|
||||
const m = permalink.match(matrixLinkify.ELEMENT_URL_PATTERN);
|
||||
if (m) {
|
||||
// A bit of a hack, but it gets the job done
|
||||
const handler = new RiotPermalinkConstructor("http://localhost");
|
||||
const handler = new ElementPermalinkConstructor("http://localhost");
|
||||
const entityInfo = m[1].split('#').slice(1).join('#');
|
||||
permalinkParts = handler.parsePermalink(`http://localhost/#${entityInfo}`);
|
||||
}
|
||||
|
@ -372,43 +405,60 @@ export function getPrimaryPermalinkEntity(permalink: string): string {
|
|||
}
|
||||
|
||||
function getPermalinkConstructor(): PermalinkConstructor {
|
||||
const riotPrefix = SdkConfig.get()['permalinkPrefix'];
|
||||
if (riotPrefix && riotPrefix !== matrixtoBaseUrl) {
|
||||
return new RiotPermalinkConstructor(riotPrefix);
|
||||
const elementPrefix = SdkConfig.get()['permalinkPrefix'];
|
||||
if (elementPrefix && elementPrefix !== matrixtoBaseUrl) {
|
||||
return new ElementPermalinkConstructor(elementPrefix);
|
||||
}
|
||||
|
||||
return new SpecPermalinkConstructor();
|
||||
}
|
||||
|
||||
export function parsePermalink(fullUrl: string): PermalinkParts {
|
||||
const riotPrefix = SdkConfig.get()['permalinkPrefix'];
|
||||
if (fullUrl.startsWith(matrixtoBaseUrl)) {
|
||||
return new SpecPermalinkConstructor().parsePermalink(fullUrl);
|
||||
} else if (riotPrefix && fullUrl.startsWith(riotPrefix)) {
|
||||
return new RiotPermalinkConstructor(riotPrefix).parsePermalink(fullUrl);
|
||||
const elementPrefix = SdkConfig.get()['permalinkPrefix'];
|
||||
if (decodeURIComponent(fullUrl).startsWith(matrixtoBaseUrl)) {
|
||||
return new SpecPermalinkConstructor().parsePermalink(decodeURIComponent(fullUrl));
|
||||
} else if (elementPrefix && fullUrl.startsWith(elementPrefix)) {
|
||||
return new ElementPermalinkConstructor(elementPrefix).parsePermalink(fullUrl);
|
||||
}
|
||||
|
||||
return null; // not a permalink we can handle
|
||||
}
|
||||
|
||||
function getServerName(userId) {
|
||||
/**
|
||||
* Parses an app local link (`#/(user|room|group)/identifer`) to a Matrix entity
|
||||
* (room, user, group). Such links are produced by `HtmlUtils` when encountering
|
||||
* links, which calls `tryTransformPermalinkToLocalHref` in this module.
|
||||
* @param {string} localLink The app local link
|
||||
* @returns {PermalinkParts}
|
||||
*/
|
||||
export function parseAppLocalLink(localLink: string): PermalinkParts {
|
||||
try {
|
||||
const segments = localLink.replace("#/", "");
|
||||
return ElementPermalinkConstructor.parseAppRoute(segments);
|
||||
} catch (e) {
|
||||
// Ignore failures
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getServerName(userId: string): string {
|
||||
return userId.split(":").splice(1).join(":");
|
||||
}
|
||||
|
||||
function getHostnameFromMatrixDomain(domain) {
|
||||
function getHostnameFromMatrixDomain(domain: string): string {
|
||||
if (!domain) return null;
|
||||
return new URL(`https://${domain}`).hostname;
|
||||
}
|
||||
|
||||
function isHostInRegex(hostname, regexps) {
|
||||
function isHostInRegex(hostname: string, regexps: RegExp[]) {
|
||||
hostname = getHostnameFromMatrixDomain(hostname);
|
||||
if (!hostname) return true; // assumed
|
||||
if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0]);
|
||||
if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0].toString());
|
||||
|
||||
return regexps.filter(h => h.test(hostname)).length > 0;
|
||||
}
|
||||
|
||||
function isHostnameIpAddress(hostname) {
|
||||
function isHostnameIpAddress(hostname: string): boolean {
|
||||
hostname = getHostnameFromMatrixDomain(hostname);
|
||||
if (!hostname) return false;
|
||||
|
||||
|
@ -420,3 +470,9 @@ function isHostnameIpAddress(hostname) {
|
|||
|
||||
return isIp(hostname);
|
||||
}
|
||||
|
||||
export const calculateRoomVia = (room: Room) => {
|
||||
const permalinkCreator = new RoomPermalinkCreator(room);
|
||||
permalinkCreator.load();
|
||||
return permalinkCreator.serverCandidates;
|
||||
};
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import PermalinkConstructor, {PermalinkParts} from "./PermalinkConstructor";
|
||||
import PermalinkConstructor, { PermalinkParts } from "./PermalinkConstructor";
|
||||
|
||||
export const host = "matrix.to";
|
||||
export const baseUrl = `https://${host}`;
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,25 +16,28 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
import ReactDOM from 'react-dom';
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import {PushProcessor} from 'matrix-js-sdk/src/pushprocessor';
|
||||
import * as sdk from '../index';
|
||||
import Pill from "../components/views/elements/Pill";
|
||||
import { parseAppLocalLink } from "./permalinks/Permalinks";
|
||||
|
||||
/**
|
||||
* Recurses depth-first through a DOM tree, converting matrix.to links
|
||||
* into pills based on the context of a given room. Returns a list of
|
||||
* the resulting React nodes so they can be unmounted rather than leaking.
|
||||
*
|
||||
* @param {Node[]} nodes - a list of sibling DOM nodes to traverse to try
|
||||
* @param {Element[]} nodes - a list of sibling DOM nodes to traverse to try
|
||||
* to turn into pills.
|
||||
* @param {MatrixEvent} mxEvent - the matrix event which the DOM nodes are
|
||||
* part of representing.
|
||||
* @param {Node[]} pills: an accumulator of the DOM nodes which contain
|
||||
* @param {Element[]} pills: an accumulator of the DOM nodes which contain
|
||||
* React components which have been mounted as part of this.
|
||||
* The initial caller should pass in an empty array to seed the accumulator.
|
||||
*/
|
||||
export function pillifyLinks(nodes, mxEvent, pills) {
|
||||
export function pillifyLinks(nodes: ArrayLike<Element>, mxEvent: MatrixEvent, pills: Element[]) {
|
||||
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId());
|
||||
const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar");
|
||||
let node = nodes[0];
|
||||
|
@ -43,10 +46,10 @@ export function pillifyLinks(nodes, mxEvent, pills) {
|
|||
|
||||
if (node.tagName === "A" && node.getAttribute("href")) {
|
||||
const href = node.getAttribute("href");
|
||||
|
||||
const parts = parseAppLocalLink(href);
|
||||
// If the link is a (localised) matrix.to link, replace it with a pill
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
if (Pill.isMessagePillUrl(href)) {
|
||||
// We don't want to pill event permalinks, so those are ignored.
|
||||
if (parts && !parts.eventId) {
|
||||
const pillContainer = document.createElement('span');
|
||||
|
||||
const pill = <Pill
|
||||
|
@ -72,9 +75,7 @@ export function pillifyLinks(nodes, mxEvent, pills) {
|
|||
// to clear the pills from the last run of pillifyLinks
|
||||
!node.parentElement.classList.contains("mx_AtRoomPill")
|
||||
) {
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
|
||||
let currentTextNode = node;
|
||||
let currentTextNode = node as Node as Text;
|
||||
const roomNotifTextNodes = [];
|
||||
|
||||
// Take a textNode and break it up to make all the instances of @room their
|
||||
|
@ -111,7 +112,7 @@ export function pillifyLinks(nodes, mxEvent, pills) {
|
|||
type={Pill.TYPE_AT_ROOM_MENTION}
|
||||
inMessage={true}
|
||||
room={room}
|
||||
shouldShowPillAvatar={true}
|
||||
shouldShowPillAvatar={shouldShowPillAvatar}
|
||||
/>;
|
||||
|
||||
ReactDOM.render(pill, pillContainer);
|
||||
|
@ -126,10 +127,10 @@ export function pillifyLinks(nodes, mxEvent, pills) {
|
|||
}
|
||||
|
||||
if (node.childNodes && node.childNodes.length && !pillified) {
|
||||
pillifyLinks(node.childNodes, mxEvent, pills);
|
||||
pillifyLinks(node.childNodes as NodeListOf<Element>, mxEvent, pills);
|
||||
}
|
||||
|
||||
node = node.nextSibling;
|
||||
node = node.nextSibling as Element;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,12 +140,12 @@ export function pillifyLinks(nodes, mxEvent, pills) {
|
|||
* It's critical to call this after pillifyLinks, otherwise
|
||||
* Pills will leak, leaking entire DOM trees via the event
|
||||
* emitter on BaseAvatar as per
|
||||
* https://github.com/vector-im/riot-web/issues/12417
|
||||
* https://github.com/vector-im/element-web/issues/12417
|
||||
*
|
||||
* @param {Node[]} pills - array of pill containers whose React
|
||||
* @param {Element[]} pills - array of pill containers whose React
|
||||
* components should be unmounted.
|
||||
*/
|
||||
export function unmountPills(pills) {
|
||||
export function unmountPills(pills: Element[]) {
|
||||
for (const pillContainer of pills) {
|
||||
ReactDOM.unmountComponentAtNode(pillContainer);
|
||||
}
|
26
src/utils/presence.ts
Normal file
26
src/utils/presence.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
|
||||
export function isPresenceEnabled() {
|
||||
const hsUrl = MatrixClientPeg.get().baseUrl;
|
||||
const urls = SdkConfig.get()['enable_presence_by_hs_url'];
|
||||
if (!urls) return true;
|
||||
if (urls[hsUrl] || urls[hsUrl] === undefined) return true;
|
||||
return false;
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// @flow
|
||||
|
||||
// Returns a promise which resolves with a given value after the given number of ms
|
||||
export const sleep = (ms: number, value: any): Promise => new Promise((resolve => { setTimeout(resolve, ms, value); }));
|
||||
|
||||
// Returns a promise which resolves when the input promise resolves with its value
|
||||
// or when the timeout of ms is reached with the value of given timeoutValue
|
||||
export async function timeout(promise: Promise, timeoutValue: any, ms: number): Promise {
|
||||
const timeoutPromise = new Promise((resolve) => {
|
||||
const timeoutId = setTimeout(resolve, ms, timeoutValue);
|
||||
promise.then(() => {
|
||||
clearTimeout(timeoutId);
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.race([promise, timeoutPromise]);
|
||||
}
|
||||
|
||||
// Returns a Deferred
|
||||
export function defer(): {resolve: () => {}, reject: () => {}, promise: Promise} {
|
||||
let resolve;
|
||||
let reject;
|
||||
|
||||
const promise = new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve;
|
||||
reject = _reject;
|
||||
});
|
||||
|
||||
return {resolve, reject, promise};
|
||||
}
|
||||
|
||||
// Promise.allSettled polyfill until browser support is stable in Firefox
|
||||
export function allSettled(promises: Promise[]): {status: string, value?: any, reason?: any}[] {
|
||||
if (Promise.allSettled) {
|
||||
return Promise.allSettled(promises);
|
||||
}
|
||||
|
||||
return Promise.all(promises.map((promise) => {
|
||||
return promise.then(value => ({
|
||||
status: "fulfilled",
|
||||
value,
|
||||
})).catch(reason => ({
|
||||
status: "rejected",
|
||||
reason,
|
||||
}));
|
||||
}));
|
||||
}
|
46
src/utils/promise.ts
Normal file
46
src/utils/promise.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Returns a promise which resolves when the input promise resolves with its value
|
||||
// or when the timeout of ms is reached with the value of given timeoutValue
|
||||
export async function timeout<T>(promise: Promise<T>, timeoutValue: T, ms: number): Promise<T> {
|
||||
const timeoutPromise = new Promise<T>((resolve) => {
|
||||
const timeoutId = setTimeout(resolve, ms, timeoutValue);
|
||||
promise.then(() => {
|
||||
clearTimeout(timeoutId);
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.race([promise, timeoutPromise]);
|
||||
}
|
||||
|
||||
// Helper method to retry a Promise a given number of times or until a predicate fails
|
||||
export async function retry<T, E extends Error>(fn: () => Promise<T>, num: number, predicate?: (e: E) => boolean) {
|
||||
let lastErr: E;
|
||||
for (let i = 0; i < num; i++) {
|
||||
try {
|
||||
const v = await fn();
|
||||
// If `await fn()` throws then we won't reach here
|
||||
return v;
|
||||
} catch (err) {
|
||||
if (predicate && !predicate(err)) {
|
||||
throw err;
|
||||
}
|
||||
lastErr = err;
|
||||
}
|
||||
}
|
||||
throw lastErr;
|
||||
}
|
34
src/utils/read-receipts.ts
Normal file
34
src/utils/read-receipts.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
/**
|
||||
* Determines if a read receipt update event includes the client's own user.
|
||||
* @param event The event to check.
|
||||
* @param client The client to check against.
|
||||
* @returns True if the read receipt update includes the client, false otherwise.
|
||||
*/
|
||||
export function readReceiptChangeIsFor(event: MatrixEvent, client: MatrixClient): boolean {
|
||||
const myUserId = client.getUserId();
|
||||
for (const eventId of Object.keys(event.getContent())) {
|
||||
const receiptUsers = Object.keys(event.getContent()[eventId]['m.read'] || {});
|
||||
if (receiptUsers.includes(myUserId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,9 +30,11 @@ import * as sdk from '../index';
|
|||
* @param {string} name The dot-path name of the component being replaced.
|
||||
* @param {React.Component} origComponent The component that can be replaced
|
||||
* with a skinned version. If no skinned version is available, this component
|
||||
* will be used.
|
||||
* will be used. Note that this is automatically provided to the function and
|
||||
* thus is optional for purposes of types.
|
||||
* @returns {ClassDecorator} The decorator.
|
||||
*/
|
||||
export function replaceableComponent(name: string, origComponent: React.Component) {
|
||||
export function replaceableComponent(name: string, origComponent?: React.Component): ClassDecorator {
|
||||
// Decorators return a function to override the class (origComponent). This
|
||||
// ultimately assumes that `getComponent()` won't throw an error and instead
|
||||
// return a falsey value like `null` when the skin doesn't have a component.
|
||||
|
|
34
src/utils/sets.ts
Normal file
34
src/utils/sets.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determines if two sets are different through a shallow comparison.
|
||||
* @param a The first set. Must be defined.
|
||||
* @param b The second set. Must be defined.
|
||||
* @returns True if they are different, false otherwise.
|
||||
*/
|
||||
export function setHasDiff<T>(a: Set<T>, b: Set<T>): boolean {
|
||||
if (a.size === b.size) {
|
||||
// When the lengths are equal, check to see if either set is missing an element from the other.
|
||||
if (Array.from(b).some(i => !a.has(i))) return true;
|
||||
if (Array.from(a).some(i => !b.has(i))) return true;
|
||||
|
||||
// if all the keys are common, say so
|
||||
return false;
|
||||
} else {
|
||||
return true; // different lengths means they are naturally diverged
|
||||
}
|
||||
}
|
105
src/utils/space.tsx
Normal file
105
src/utils/space.tsx
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import { calculateRoomVia } from "../utils/permalinks/Permalinks";
|
||||
import Modal from "../Modal";
|
||||
import SpaceSettingsDialog from "../components/views/dialogs/SpaceSettingsDialog";
|
||||
import AddExistingToSpaceDialog from "../components/views/dialogs/AddExistingToSpaceDialog";
|
||||
import CreateRoomDialog from "../components/views/dialogs/CreateRoomDialog";
|
||||
import createRoom, { IOpts } from "../createRoom";
|
||||
import { _t } from "../languageHandler";
|
||||
import SpacePublicShare from "../components/views/spaces/SpacePublicShare";
|
||||
import InfoDialog from "../components/views/dialogs/InfoDialog";
|
||||
import { showRoomInviteDialog } from "../RoomInvite";
|
||||
|
||||
export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => {
|
||||
const userId = cli.getUserId();
|
||||
return space.getMyMembership() === "join"
|
||||
&& (space.currentState.maySendStateEvent(EventType.RoomAvatar, userId)
|
||||
|| space.currentState.maySendStateEvent(EventType.RoomName, userId)
|
||||
|| space.currentState.maySendStateEvent(EventType.RoomTopic, userId)
|
||||
|| space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId));
|
||||
};
|
||||
|
||||
export const makeSpaceParentEvent = (room: Room, canonical = false) => ({
|
||||
type: EventType.SpaceParent,
|
||||
content: {
|
||||
"via": calculateRoomVia(room),
|
||||
"canonical": canonical,
|
||||
},
|
||||
state_key: room.roomId,
|
||||
});
|
||||
|
||||
export const showSpaceSettings = (cli: MatrixClient, space: Room) => {
|
||||
Modal.createTrackedDialog("Space Settings", "", SpaceSettingsDialog, {
|
||||
matrixClient: cli,
|
||||
space,
|
||||
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||
};
|
||||
|
||||
export const showAddExistingRooms = async (cli: MatrixClient, space: Room) => {
|
||||
return Modal.createTrackedDialog(
|
||||
"Space Landing",
|
||||
"Add Existing",
|
||||
AddExistingToSpaceDialog,
|
||||
{
|
||||
matrixClient: cli,
|
||||
onCreateRoomClick: showCreateNewRoom,
|
||||
space,
|
||||
},
|
||||
"mx_AddExistingToSpaceDialog_wrapper",
|
||||
).finished;
|
||||
};
|
||||
|
||||
export const showCreateNewRoom = async (cli: MatrixClient, space: Room) => {
|
||||
const modal = Modal.createTrackedDialog<[boolean, IOpts]>(
|
||||
"Space Landing",
|
||||
"Create Room",
|
||||
CreateRoomDialog,
|
||||
{
|
||||
defaultPublic: space.getJoinRule() === "public",
|
||||
parentSpace: space,
|
||||
},
|
||||
);
|
||||
const [shouldCreate, opts] = await modal.finished;
|
||||
if (shouldCreate) {
|
||||
await createRoom(opts);
|
||||
}
|
||||
return shouldCreate;
|
||||
};
|
||||
|
||||
export const showSpaceInvite = (space: Room, initialText = "") => {
|
||||
if (space.getJoinRule() === "public") {
|
||||
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
|
||||
title: _t("Invite to %(spaceName)s", { spaceName: space.name }),
|
||||
description: <React.Fragment>
|
||||
<span>{ _t("Share your public space") }</span>
|
||||
<SpacePublicShare space={space} onFinished={() => modal.close()} />
|
||||
</React.Fragment>,
|
||||
fixedWidth: false,
|
||||
button: false,
|
||||
className: "mx_SpacePanel_sharePublicSpace",
|
||||
hasCloseButton: true,
|
||||
});
|
||||
} else {
|
||||
showRoomInviteDialog(space.roomId, initialText);
|
||||
}
|
||||
};
|
148
src/utils/stringOrderField.ts
Normal file
148
src/utils/stringOrderField.ts
Normal file
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { alphabetPad, baseToString, stringToBase, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { moveElement } from "./arrays";
|
||||
|
||||
export function midPointsBetweenStrings(
|
||||
a: string,
|
||||
b: string,
|
||||
count: number,
|
||||
maxLen: number,
|
||||
alphabet = DEFAULT_ALPHABET,
|
||||
): string[] {
|
||||
const padN = Math.min(Math.max(a.length, b.length), maxLen);
|
||||
const padA = alphabetPad(a, padN, alphabet);
|
||||
const padB = alphabetPad(b, padN, alphabet);
|
||||
const baseA = stringToBase(padA, alphabet);
|
||||
const baseB = stringToBase(padB, alphabet);
|
||||
|
||||
if (baseB - baseA - BigInt(1) < count) {
|
||||
if (padN < maxLen) {
|
||||
// this recurses once at most due to the new limit of n+1
|
||||
return midPointsBetweenStrings(
|
||||
alphabetPad(padA, padN + 1, alphabet),
|
||||
alphabetPad(padB, padN + 1, alphabet),
|
||||
count,
|
||||
padN + 1,
|
||||
alphabet,
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
const step = (baseB - baseA) / BigInt(count + 1);
|
||||
const start = BigInt(baseA + step);
|
||||
return Array(count).fill(undefined).map((_, i) => baseToString(start + (BigInt(i) * step), alphabet));
|
||||
}
|
||||
|
||||
interface IEntry {
|
||||
index: number;
|
||||
order: string;
|
||||
}
|
||||
|
||||
export const reorderLexicographically = (
|
||||
orders: Array<string | undefined>,
|
||||
fromIndex: number,
|
||||
toIndex: number,
|
||||
maxLen = 50,
|
||||
): IEntry[] => {
|
||||
// sanity check inputs
|
||||
if (
|
||||
fromIndex < 0 || toIndex < 0 ||
|
||||
fromIndex > orders.length || toIndex > orders.length ||
|
||||
fromIndex === toIndex
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// zip orders with their indices to simplify later index wrangling
|
||||
const ordersWithIndices: IEntry[] = orders.map((order, index) => ({ index, order }));
|
||||
// apply the fundamental order update to the zipped array
|
||||
const newOrder = moveElement(ordersWithIndices, fromIndex, toIndex);
|
||||
|
||||
// check if we have to fill undefined orders to complete placement
|
||||
const orderToLeftUndefined = newOrder[toIndex - 1]?.order === undefined;
|
||||
|
||||
let leftBoundIdx = toIndex;
|
||||
let rightBoundIdx = toIndex;
|
||||
|
||||
let canMoveLeft = true;
|
||||
const nextBase = newOrder[toIndex + 1]?.order !== undefined
|
||||
? stringToBase(newOrder[toIndex + 1].order)
|
||||
: BigInt(Number.MAX_VALUE);
|
||||
|
||||
// check how far left we would have to mutate to fit in that direction
|
||||
for (let i = toIndex - 1, j = 1; i >= 0; i--, j++) {
|
||||
if (newOrder[i]?.order !== undefined && nextBase - stringToBase(newOrder[i].order) > j) break;
|
||||
leftBoundIdx = i;
|
||||
}
|
||||
|
||||
// verify the left move would be sufficient
|
||||
const firstOrderBase = newOrder[0].order === undefined ? undefined : stringToBase(newOrder[0].order);
|
||||
const bigToIndex = BigInt(toIndex);
|
||||
if (leftBoundIdx === 0 &&
|
||||
firstOrderBase !== undefined &&
|
||||
nextBase - firstOrderBase <= bigToIndex &&
|
||||
firstOrderBase <= bigToIndex
|
||||
) {
|
||||
canMoveLeft = false;
|
||||
}
|
||||
|
||||
const canDisplaceRight = !orderToLeftUndefined;
|
||||
let canMoveRight = canDisplaceRight;
|
||||
if (canDisplaceRight) {
|
||||
const prevBase = newOrder[toIndex - 1]?.order !== undefined
|
||||
? stringToBase(newOrder[toIndex - 1]?.order)
|
||||
: BigInt(Number.MIN_VALUE);
|
||||
|
||||
// check how far right we would have to mutate to fit in that direction
|
||||
for (let i = toIndex + 1, j = 1; i < newOrder.length; i++, j++) {
|
||||
if (newOrder[i]?.order === undefined || stringToBase(newOrder[i].order) - prevBase > j) break;
|
||||
rightBoundIdx = i;
|
||||
}
|
||||
|
||||
// verify the right move would be sufficient
|
||||
if (rightBoundIdx === newOrder.length - 1 &&
|
||||
(newOrder[rightBoundIdx]
|
||||
? stringToBase(newOrder[rightBoundIdx].order)
|
||||
: BigInt(Number.MAX_VALUE)) - prevBase <= (rightBoundIdx - toIndex)
|
||||
) {
|
||||
canMoveRight = false;
|
||||
}
|
||||
}
|
||||
|
||||
// pick the cheaper direction
|
||||
const leftDiff = canMoveLeft ? toIndex - leftBoundIdx : Number.MAX_SAFE_INTEGER;
|
||||
const rightDiff = canMoveRight ? rightBoundIdx - toIndex : Number.MAX_SAFE_INTEGER;
|
||||
if (orderToLeftUndefined || leftDiff < rightDiff) {
|
||||
rightBoundIdx = toIndex;
|
||||
} else {
|
||||
leftBoundIdx = toIndex;
|
||||
}
|
||||
|
||||
const prevOrder = newOrder[leftBoundIdx - 1]?.order ?? "";
|
||||
const nextOrder = newOrder[rightBoundIdx + 1]?.order
|
||||
?? DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1).repeat(prevOrder.length || 1);
|
||||
|
||||
const changes = midPointsBetweenStrings(prevOrder, nextOrder, 1 + rightBoundIdx - leftBoundIdx, maxLen);
|
||||
|
||||
return changes.map((order, i) => ({
|
||||
index: newOrder[leftBoundIdx + i].index,
|
||||
order,
|
||||
}));
|
||||
};
|
85
src/utils/strings.ts
Normal file
85
src/utils/strings.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copy plaintext to user's clipboard
|
||||
* It will overwrite user's selection range
|
||||
* In certain browsers it may only work if triggered by a user action or may ask user for permissions
|
||||
* Tries to use new async clipboard API if available
|
||||
* @param text the plaintext to put in the user's clipboard
|
||||
*/
|
||||
export async function copyPlaintext(text: string): Promise<boolean> {
|
||||
try {
|
||||
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} else {
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.top = "0";
|
||||
textArea.style.left = "0";
|
||||
textArea.style.position = "fixed";
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
const selection = document.getSelection();
|
||||
const range = document.createRange();
|
||||
// range.selectNodeContents(textArea);
|
||||
range.selectNode(textArea);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
const successful = document.execCommand("copy");
|
||||
selection.removeAllRanges();
|
||||
document.body.removeChild(textArea);
|
||||
return successful;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("copyPlaintext failed", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function selectText(target: Element) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(target);
|
||||
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy rich text to user's clipboard
|
||||
* It will overwrite user's selection range
|
||||
* In certain browsers it may only work if triggered by a user action or may ask user for permissions
|
||||
* @param ref pointer to the node to copy
|
||||
*/
|
||||
export function copyNode(ref: Element): boolean {
|
||||
selectText(ref);
|
||||
return document.execCommand('copy');
|
||||
}
|
||||
|
||||
const collator = new Intl.Collator();
|
||||
/**
|
||||
* Performant language-sensitive string comparison
|
||||
* @param a the first string to compare
|
||||
* @param b the second string to compare
|
||||
*/
|
||||
export function compare(a: string, b: string): number {
|
||||
return collator.compare(a, b);
|
||||
}
|
|
@ -14,16 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as projectNameGenerator from "project-name-generator";
|
||||
|
||||
/**
|
||||
* Generates a human readable identifier. This should not be used for anything
|
||||
* which needs secure/cryptographic random: just a level uniquness that is offered
|
||||
* by something like Date.now().
|
||||
* @returns {string} The randomly generated ID
|
||||
/* Simple utils for formatting style values
|
||||
*/
|
||||
export function generateHumanReadableId(): string {
|
||||
return projectNameGenerator({words: 3}).raw.map(w => {
|
||||
return w[0].toUpperCase() + w.substring(1).toLowerCase();
|
||||
}).join('');
|
||||
|
||||
// converts a pixel value to rem.
|
||||
export function toRem(pixelValue: number): string {
|
||||
return pixelValue / 10 + "rem";
|
||||
}
|
||||
|
||||
export function toPx(pixelValue: number): string {
|
||||
return pixelValue + "px";
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue