Apply prettier formatting
This commit is contained in:
parent
1cac306093
commit
526645c791
1576 changed files with 65385 additions and 62478 deletions
|
@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { ReactNode } from "react";
|
||||
import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t, _td, newTranslatableError } from "../languageHandler";
|
||||
import { makeType } from "./TypeUtils";
|
||||
import SdkConfig from '../SdkConfig';
|
||||
import { ValidatedServerConfig } from './ValidatedServerConfig';
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import { ValidatedServerConfig } from "./ValidatedServerConfig";
|
||||
|
||||
const LIVELINESS_DISCOVERY_ERRORS: string[] = [
|
||||
AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
|
@ -44,7 +44,9 @@ export default class AutoDiscoveryUtils {
|
|||
*/
|
||||
static isLivelinessError(error: string | Error): boolean {
|
||||
if (!error) return false;
|
||||
return !!LIVELINESS_DISCOVERY_ERRORS.find(e => typeof error === "string" ? e === error : e === error.message);
|
||||
return !!LIVELINESS_DISCOVERY_ERRORS.find((e) =>
|
||||
typeof error === "string" ? e === error : e === error.message,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -75,11 +77,15 @@ export default class AutoDiscoveryUtils {
|
|||
},
|
||||
{
|
||||
a: (sub) => {
|
||||
return <a
|
||||
href="https://github.com/vector-im/element-web/blob/master/docs/config.md"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>{ sub }</a>;
|
||||
return (
|
||||
<a
|
||||
href="https://github.com/vector-im/element-web/blob/master/docs/config.md"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{sub}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -96,20 +102,20 @@ export default class AutoDiscoveryUtils {
|
|||
if (pageName === "register") {
|
||||
body = _t(
|
||||
"You can register, but some features will be unavailable until the identity server is " +
|
||||
"back online. If you keep seeing this warning, check your configuration or contact a server " +
|
||||
"admin.",
|
||||
"back online. If you keep seeing this warning, check your configuration or contact a server " +
|
||||
"admin.",
|
||||
);
|
||||
} else if (pageName === "reset_password") {
|
||||
body = _t(
|
||||
"You can reset your password, but some features will be unavailable until the identity " +
|
||||
"server is back online. If you keep seeing this warning, check your configuration or contact " +
|
||||
"a server admin.",
|
||||
"server is back online. If you keep seeing this warning, check your configuration or contact " +
|
||||
"a server admin.",
|
||||
);
|
||||
} else {
|
||||
body = _t(
|
||||
"You can log in, but some features will be unavailable until the identity server is " +
|
||||
"back online. If you keep seeing this warning, check your configuration or contact a server " +
|
||||
"admin.",
|
||||
"back online. If you keep seeing this warning, check your configuration or contact a server " +
|
||||
"admin.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -119,8 +125,8 @@ export default class AutoDiscoveryUtils {
|
|||
serverErrorIsFatal: isFatalError,
|
||||
serverDeadError: (
|
||||
<div>
|
||||
<strong>{ title }</strong>
|
||||
<div>{ body }</div>
|
||||
<strong>{title}</strong>
|
||||
<div>{body}</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
@ -150,7 +156,7 @@ export default class AutoDiscoveryUtils {
|
|||
};
|
||||
|
||||
if (identityUrl) {
|
||||
wellknownConfig['m.identity_server'] = {
|
||||
wellknownConfig["m.identity_server"] = {
|
||||
base_url: identityUrl,
|
||||
};
|
||||
}
|
||||
|
@ -183,7 +189,11 @@ export default class AutoDiscoveryUtils {
|
|||
* @returns {Promise<ValidatedServerConfig>} Resolves to the validated configuration.
|
||||
*/
|
||||
static buildValidatedConfigFromDiscovery(
|
||||
serverName: string, discoveryResult, syntaxOnly=false, isSynthetic=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".
|
||||
|
@ -191,8 +201,8 @@ export default class AutoDiscoveryUtils {
|
|||
throw newTranslatableError(_td("Unexpected error resolving homeserver configuration"));
|
||||
}
|
||||
|
||||
const hsResult = discoveryResult['m.homeserver'];
|
||||
const isResult = discoveryResult['m.identity_server'];
|
||||
const hsResult = discoveryResult["m.homeserver"];
|
||||
const isResult = discoveryResult["m.identity_server"];
|
||||
|
||||
const defaultConfig = SdkConfig.get("validated_server_config");
|
||||
|
||||
|
@ -203,7 +213,7 @@ export default class AutoDiscoveryUtils {
|
|||
// 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 Element.
|
||||
let preferredIdentityUrl = defaultConfig && defaultConfig['isUrl'];
|
||||
let preferredIdentityUrl = defaultConfig && defaultConfig["isUrl"];
|
||||
if (isResult && isResult.state === AutoDiscovery.SUCCESS) {
|
||||
preferredIdentityUrl = isResult["base_url"];
|
||||
} else if (isResult && isResult.state !== AutoDiscovery.PROMPT) {
|
||||
|
|
|
@ -20,5 +20,5 @@ export function chromeFileInputFix(event: MouseEvent<HTMLInputElement>): void {
|
|||
// Workaround for Chromium Bug
|
||||
// Chrome does not fire onChange events if the same file is selected twice
|
||||
// Only required on Chromium-based browsers (Electron, Chrome, Edge, Opera, Vivaldi, etc)
|
||||
event.currentTarget.value = '';
|
||||
event.currentTarget.value = "";
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
|||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
|
||||
/**
|
||||
* Class that takes a Matrix Client and flips the m.direct map
|
||||
|
@ -35,10 +35,10 @@ export default class DMRoomMap {
|
|||
private static sharedInstance: DMRoomMap;
|
||||
|
||||
// TODO: convert these to maps
|
||||
private roomToUser: {[key: string]: string} = null;
|
||||
private userToRooms: {[key: string]: string[]} = null;
|
||||
private roomToUser: { [key: string]: string } = null;
|
||||
private userToRooms: { [key: string]: string[] } = null;
|
||||
private hasSentOutPatchDirectAccountDataPatch: boolean;
|
||||
private mDirectEvent: {[key: string]: string[]};
|
||||
private mDirectEvent: { [key: string]: string[] };
|
||||
|
||||
constructor(private readonly matrixClient: MatrixClient) {
|
||||
// see onAccountData
|
||||
|
@ -102,23 +102,24 @@ export default class DMRoomMap {
|
|||
const selfRoomIds = userToRooms[myUserId];
|
||||
if (selfRoomIds) {
|
||||
// any self-chats that should not be self-chats?
|
||||
const guessedUserIdsThatChanged = selfRoomIds.map((roomId) => {
|
||||
const room = this.matrixClient.getRoom(roomId);
|
||||
if (room) {
|
||||
const userId = room.guessDMUserId();
|
||||
if (userId && userId !== myUserId) {
|
||||
return { userId, roomId };
|
||||
const guessedUserIdsThatChanged = selfRoomIds
|
||||
.map((roomId) => {
|
||||
const room = this.matrixClient.getRoom(roomId);
|
||||
if (room) {
|
||||
const userId = room.guessDMUserId();
|
||||
if (userId && userId !== myUserId) {
|
||||
return { userId, roomId };
|
||||
}
|
||||
}
|
||||
}
|
||||
}).filter((ids) => !!ids); //filter out
|
||||
})
|
||||
.filter((ids) => !!ids); //filter out
|
||||
// these are actually all legit self-chats
|
||||
// bail out
|
||||
if (!guessedUserIdsThatChanged.length) {
|
||||
return false;
|
||||
}
|
||||
userToRooms[myUserId] = selfRoomIds.filter((roomId) => {
|
||||
return !guessedUserIdsThatChanged
|
||||
.some((ids) => ids.roomId === roomId);
|
||||
return !guessedUserIdsThatChanged.some((ids) => ids.roomId === roomId);
|
||||
});
|
||||
guessedUserIdsThatChanged.forEach(({ userId, roomId }) => {
|
||||
const roomIds = userToRooms[userId];
|
||||
|
@ -151,11 +152,12 @@ export default class DMRoomMap {
|
|||
let commonRooms = this.getDMRoomsForUserId(ids[0]);
|
||||
for (let i = 1; i < ids.length; i++) {
|
||||
const userRooms = this.getDMRoomsForUserId(ids[i]);
|
||||
commonRooms = commonRooms.filter(r => userRooms.includes(r));
|
||||
commonRooms = commonRooms.filter((r) => userRooms.includes(r));
|
||||
}
|
||||
|
||||
const joinedRooms = commonRooms.map(r => MatrixClientPeg.get().getRoom(r))
|
||||
.filter(r => r && r.getMyMembership() === 'join');
|
||||
const joinedRooms = commonRooms
|
||||
.map((r) => MatrixClientPeg.get().getRoom(r))
|
||||
.filter((r) => r && r.getMyMembership() === "join");
|
||||
|
||||
return joinedRooms[0];
|
||||
}
|
||||
|
@ -182,15 +184,15 @@ export default class DMRoomMap {
|
|||
return this.roomToUser[roomId];
|
||||
}
|
||||
|
||||
public 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) }))
|
||||
.filter(r => r.userId && r.room && r.room.getInvitedAndJoinedMemberCount() === 2)
|
||||
.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, {});
|
||||
}
|
||||
|
||||
private getUserToRooms(): {[key: string]: string[]} {
|
||||
private getUserToRooms(): { [key: string]: string[] } {
|
||||
if (!this.userToRooms) {
|
||||
const userToRooms = this.mDirectEvent;
|
||||
const myUserId = this.matrixClient.getUserId();
|
||||
|
@ -200,8 +202,9 @@ export default class DMRoomMap {
|
|||
// to avoid multiple devices fighting to correct
|
||||
// the account data, only try to send the corrected
|
||||
// version once.
|
||||
logger.warn(`Invalid m.direct account data detected ` +
|
||||
`(self-chats that shouldn't be), patching it up.`);
|
||||
logger.warn(
|
||||
`Invalid m.direct account data detected ` + `(self-chats that shouldn't be), patching it up.`,
|
||||
);
|
||||
if (neededPatching && !this.hasSentOutPatchDirectAccountDataPatch) {
|
||||
this.hasSentOutPatchDirectAccountDataPatch = true;
|
||||
this.matrixClient.setAccountData(EventType.Direct, userToRooms);
|
||||
|
|
|
@ -15,8 +15,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
// Pull in the encryption lib so that we can decrypt attachments.
|
||||
import encrypt from 'matrix-encrypt-attachment';
|
||||
import { parseErrorResponse } from 'matrix-js-sdk/src/http-api';
|
||||
import encrypt from "matrix-encrypt-attachment";
|
||||
import { parseErrorResponse } from "matrix-js-sdk/src/http-api";
|
||||
|
||||
import { mediaFromContent } from "../customisations/Media";
|
||||
import { IEncryptedFile, IMediaEventInfo } from "../customisations/models/IMediaEventContent";
|
||||
|
@ -47,10 +47,7 @@ export class DecryptError extends Error {
|
|||
* @param {IMediaEventInfo} info The info parameter taken from the matrix event.
|
||||
* @returns {Promise<Blob>} Resolves to a Blob of the file.
|
||||
*/
|
||||
export async function decryptFile(
|
||||
file: IEncryptedFile,
|
||||
info?: IMediaEventInfo,
|
||||
): Promise<Blob> {
|
||||
export async function decryptFile(file: IEncryptedFile, info?: IMediaEventInfo): Promise<Blob> {
|
||||
const media = mediaFromContent({ file });
|
||||
|
||||
let responseData: ArrayBuffer;
|
||||
|
@ -74,7 +71,7 @@ export async function decryptFile(
|
|||
// 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 = info?.mimetype ? info.mimetype.split(";")[0].trim() : '';
|
||||
let mimetype = info?.mimetype ? info.mimetype.split(";")[0].trim() : "";
|
||||
mimetype = getBlobSafeMimeType(mimetype);
|
||||
|
||||
return new Blob([dataArray], { type: mimetype });
|
||||
|
|
|
@ -43,8 +43,7 @@ export class DialogOpener {
|
|||
|
||||
private isRegistered = false;
|
||||
|
||||
private constructor() {
|
||||
}
|
||||
private constructor() {}
|
||||
|
||||
// We could do this in the constructor, but then we wouldn't have
|
||||
// a function to call from Lifecycle to capture the class.
|
||||
|
@ -56,11 +55,17 @@ export class DialogOpener {
|
|||
|
||||
private onDispatch = (payload: ActionPayload) => {
|
||||
switch (payload.action) {
|
||||
case 'open_room_settings':
|
||||
Modal.createDialog(RoomSettingsDialog, {
|
||||
roomId: payload.room_id || SdkContextClass.instance.roomViewStore.getRoomId(),
|
||||
initialTabId: payload.initial_tab_id,
|
||||
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||
case "open_room_settings":
|
||||
Modal.createDialog(
|
||||
RoomSettingsDialog,
|
||||
{
|
||||
roomId: payload.room_id || SdkContextClass.instance.roomViewStore.getRoomId(),
|
||||
initialTabId: payload.initial_tab_id,
|
||||
},
|
||||
/*className=*/ null,
|
||||
/*isPriority=*/ false,
|
||||
/*isStatic=*/ true,
|
||||
);
|
||||
break;
|
||||
case Action.OpenForwardDialog:
|
||||
Modal.createDialog(ForwardDialog, {
|
||||
|
@ -70,31 +75,52 @@ export class DialogOpener {
|
|||
});
|
||||
break;
|
||||
case Action.OpenReportEventDialog:
|
||||
Modal.createDialog(ReportEventDialog, {
|
||||
mxEvent: payload.event,
|
||||
}, 'mx_Dialog_reportEvent');
|
||||
Modal.createDialog(
|
||||
ReportEventDialog,
|
||||
{
|
||||
mxEvent: payload.event,
|
||||
},
|
||||
"mx_Dialog_reportEvent",
|
||||
);
|
||||
break;
|
||||
case Action.OpenSpacePreferences:
|
||||
Modal.createDialog(SpacePreferencesDialog, {
|
||||
initialTabId: payload.initalTabId,
|
||||
space: payload.space,
|
||||
}, null, false, true);
|
||||
Modal.createDialog(
|
||||
SpacePreferencesDialog,
|
||||
{
|
||||
initialTabId: payload.initalTabId,
|
||||
space: payload.space,
|
||||
},
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
break;
|
||||
case Action.OpenSpaceSettings:
|
||||
Modal.createDialog(SpaceSettingsDialog, {
|
||||
matrixClient: payload.space.client,
|
||||
space: payload.space,
|
||||
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||
Modal.createDialog(
|
||||
SpaceSettingsDialog,
|
||||
{
|
||||
matrixClient: payload.space.client,
|
||||
space: payload.space,
|
||||
},
|
||||
/*className=*/ null,
|
||||
/*isPriority=*/ false,
|
||||
/*isStatic=*/ true,
|
||||
);
|
||||
break;
|
||||
case Action.OpenInviteDialog:
|
||||
Modal.createDialog(InviteDialog, {
|
||||
kind: payload.kind,
|
||||
call: payload.call,
|
||||
roomId: payload.roomId,
|
||||
}, classnames("mx_InviteDialog_flexWrapper", payload.className), false, true).finished
|
||||
.then((results) => {
|
||||
payload.onFinishedCallback?.(results);
|
||||
});
|
||||
Modal.createDialog(
|
||||
InviteDialog,
|
||||
{
|
||||
kind: payload.kind,
|
||||
call: payload.call,
|
||||
roomId: payload.roomId,
|
||||
},
|
||||
classnames("mx_InviteDialog_flexWrapper", payload.className),
|
||||
false,
|
||||
true,
|
||||
).finished.then((results) => {
|
||||
payload.onFinishedCallback?.(results);
|
||||
});
|
||||
break;
|
||||
case Action.OpenAddToExistingSpaceDialog: {
|
||||
const space = payload.space;
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React, { ReactNode } from "react";
|
||||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
|
||||
import { _t, _td, Tags, TranslatedString } from '../languageHandler';
|
||||
import { _t, _td, Tags, TranslatedString } from "../languageHandler";
|
||||
|
||||
/**
|
||||
* Produce a translated error message for a
|
||||
|
@ -40,48 +40,44 @@ export function messageForResourceLimitError(
|
|||
extraTranslations?: Tags,
|
||||
): TranslatedString {
|
||||
let errString = strings[limitType];
|
||||
if (errString === undefined) errString = strings[''];
|
||||
if (errString === undefined) errString = strings[""];
|
||||
|
||||
const linkSub = sub => {
|
||||
const linkSub = (sub) => {
|
||||
if (adminContact) {
|
||||
return <a href={adminContact} target="_blank" rel="noreferrer noopener">{ sub }</a>;
|
||||
return (
|
||||
<a href={adminContact} target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return sub;
|
||||
}
|
||||
};
|
||||
|
||||
if (errString.includes('<a>')) {
|
||||
return _t(errString, {}, Object.assign({ 'a': linkSub }, extraTranslations));
|
||||
if (errString.includes("<a>")) {
|
||||
return _t(errString, {}, Object.assign({ a: linkSub }, extraTranslations));
|
||||
} else {
|
||||
return _t(errString, {}, extraTranslations);
|
||||
}
|
||||
}
|
||||
|
||||
export function messageForSyncError(err: Error): ReactNode {
|
||||
if (err instanceof MatrixError && 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."),
|
||||
},
|
||||
if (err instanceof MatrixError && 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."),
|
||||
});
|
||||
const adminContact = messageForResourceLimitError(err.data.limit_type, err.data.admin_contact, {
|
||||
"": _td("Please <a>contact your service administrator</a> to continue using the service."),
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div>{limitError}</div>
|
||||
<div>{adminContact}</div>
|
||||
</div>
|
||||
);
|
||||
const adminContact = messageForResourceLimitError(
|
||||
err.data.limit_type,
|
||||
err.data.admin_contact,
|
||||
{
|
||||
'': _td("Please <a>contact your service administrator</a> to continue using the service."),
|
||||
},
|
||||
);
|
||||
return <div>
|
||||
<div>{ limitError }</div>
|
||||
<div>{ adminContact }</div>
|
||||
</div>;
|
||||
} else {
|
||||
return <div>
|
||||
{ _t("Unable to connect to Homeserver. Retrying...") }
|
||||
</div>;
|
||||
return <div>{_t("Unable to connect to Homeserver. Retrying...")}</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,11 @@ import { MatrixClientPeg } from "../MatrixClientPeg";
|
|||
import { getMessageModerationState, isLocationEvent, MessageModerationState } from "./EventUtils";
|
||||
import { ElementCall } from "../models/Call";
|
||||
|
||||
export function getEventDisplayInfo(mxEvent: MatrixEvent, showHiddenEvents: boolean, hideEvent?: boolean): {
|
||||
export function getEventDisplayInfo(
|
||||
mxEvent: MatrixEvent,
|
||||
showHiddenEvents: boolean,
|
||||
hideEvent?: boolean,
|
||||
): {
|
||||
isInfoMessage: boolean;
|
||||
hasRenderer: boolean;
|
||||
isBubbleMessage: boolean;
|
||||
|
@ -55,17 +59,15 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent, showHiddenEvents: bool
|
|||
let factory = pickFactory(mxEvent, MatrixClientPeg.get(), showHiddenEvents);
|
||||
|
||||
// Info messages are basically information about commands processed on a room
|
||||
let isBubbleMessage = (
|
||||
let isBubbleMessage =
|
||||
eventType.startsWith("m.key.verification") ||
|
||||
(eventType === EventType.RoomMessage && msgtype?.startsWith("m.key.verification")) ||
|
||||
(eventType === EventType.RoomCreate) ||
|
||||
(eventType === EventType.RoomEncryption) ||
|
||||
(factory === JitsiEventFactory)
|
||||
);
|
||||
const isLeftAlignedBubbleMessage = !isBubbleMessage && (
|
||||
eventType === EventType.CallInvite || ElementCall.CALL_EVENT_TYPE.matches(eventType)
|
||||
);
|
||||
let isInfoMessage = (
|
||||
eventType === EventType.RoomCreate ||
|
||||
eventType === EventType.RoomEncryption ||
|
||||
factory === JitsiEventFactory;
|
||||
const isLeftAlignedBubbleMessage =
|
||||
!isBubbleMessage && (eventType === EventType.CallInvite || ElementCall.CALL_EVENT_TYPE.matches(eventType));
|
||||
let isInfoMessage =
|
||||
!isBubbleMessage &&
|
||||
!isLeftAlignedBubbleMessage &&
|
||||
eventType !== EventType.RoomMessage &&
|
||||
|
@ -73,15 +75,13 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent, showHiddenEvents: bool
|
|||
eventType !== EventType.Sticker &&
|
||||
eventType !== EventType.RoomCreate &&
|
||||
!M_POLL_START.matches(eventType) &&
|
||||
!M_BEACON_INFO.matches(eventType)
|
||||
);
|
||||
!M_BEACON_INFO.matches(eventType);
|
||||
// Some non-info messages want to be rendered in the appropriate bubble column but without the bubble background
|
||||
const noBubbleEvent = (
|
||||
const noBubbleEvent =
|
||||
(eventType === EventType.RoomMessage && msgtype === MsgType.Emote) ||
|
||||
M_POLL_START.matches(eventType) ||
|
||||
M_BEACON_INFO.matches(eventType) ||
|
||||
isLocationEvent(mxEvent)
|
||||
);
|
||||
isLocationEvent(mxEvent);
|
||||
|
||||
// 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
|
||||
|
|
|
@ -14,16 +14,16 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType, EVENT_VISIBILITY_CHANGE_TYPE, MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { M_POLL_START } from "matrix-events-sdk";
|
||||
import { M_LOCATION } from "matrix-js-sdk/src/@types/location";
|
||||
import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon';
|
||||
import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
|
||||
import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
|
||||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import shouldHideEvent from "../shouldHideEvent";
|
||||
import { GetRelationsForEvent } from "../components/views/rooms/EventTile";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
|
@ -47,13 +47,13 @@ export function isContentActionable(mxEvent: MatrixEvent): boolean {
|
|||
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
||||
|
||||
if (isSent && !mxEvent.isRedacted()) {
|
||||
if (mxEvent.getType() === 'm.room.message') {
|
||||
if (mxEvent.getType() === "m.room.message") {
|
||||
const content = mxEvent.getContent();
|
||||
if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) {
|
||||
if (content.msgtype && content.msgtype !== "m.bad.encrypted" && content.hasOwnProperty("body")) {
|
||||
return true;
|
||||
}
|
||||
} else if (
|
||||
mxEvent.getType() === 'm.sticker' ||
|
||||
mxEvent.getType() === "m.sticker" ||
|
||||
M_POLL_START.matches(mxEvent.getType()) ||
|
||||
M_BEACON_INFO.matches(mxEvent.getType())
|
||||
) {
|
||||
|
@ -65,10 +65,7 @@ export function isContentActionable(mxEvent: MatrixEvent): boolean {
|
|||
}
|
||||
|
||||
export function canEditContent(mxEvent: MatrixEvent): boolean {
|
||||
const isCancellable = (
|
||||
mxEvent.getType() === EventType.RoomMessage ||
|
||||
M_POLL_START.matches(mxEvent.getType())
|
||||
);
|
||||
const isCancellable = mxEvent.getType() === EventType.RoomMessage || M_POLL_START.matches(mxEvent.getType());
|
||||
|
||||
if (
|
||||
!isCancellable ||
|
||||
|
@ -83,11 +80,7 @@ export function canEditContent(mxEvent: MatrixEvent): boolean {
|
|||
const { msgtype, body } = mxEvent.getOriginalContent();
|
||||
return (
|
||||
M_POLL_START.matches(mxEvent.getType()) ||
|
||||
(
|
||||
(msgtype === MsgType.Text || msgtype === MsgType.Emote) &&
|
||||
!!body &&
|
||||
typeof body === 'string'
|
||||
)
|
||||
((msgtype === MsgType.Text || msgtype === MsgType.Emote) && !!body && typeof body === "string")
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -117,17 +110,17 @@ export function findEditableEvent({
|
|||
const beginIdx = isForward ? 0 : maxIdx;
|
||||
let endIdx = isForward ? maxIdx : 0;
|
||||
if (!fromEventId) {
|
||||
endIdx = Math.min(Math.max(0, beginIdx + (inc * MAX_JUMP_DISTANCE)), maxIdx);
|
||||
endIdx = Math.min(Math.max(0, beginIdx + inc * MAX_JUMP_DISTANCE), maxIdx);
|
||||
}
|
||||
let foundFromEventId = !fromEventId;
|
||||
for (let i = beginIdx; i !== (endIdx + inc); i += inc) {
|
||||
for (let i = beginIdx; i !== endIdx + inc; i += inc) {
|
||||
const e = events[i];
|
||||
// find start event first
|
||||
if (!foundFromEventId && e.getId() === fromEventId) {
|
||||
foundFromEventId = true;
|
||||
// don't look further than MAX_JUMP_DISTANCE events from `fromEventId`
|
||||
// to not iterate potentially 1000nds of events on key up/down
|
||||
endIdx = Math.min(Math.max(0, i + (inc * MAX_JUMP_DISTANCE)), maxIdx);
|
||||
endIdx = Math.min(Math.max(0, i + inc * MAX_JUMP_DISTANCE), maxIdx);
|
||||
} else if (foundFromEventId && !shouldHideEvent(e) && canEditOwnEvent(e)) {
|
||||
// otherwise look for editable event
|
||||
return e;
|
||||
|
@ -193,14 +186,16 @@ export function getMessageModerationState(mxEvent: MatrixEvent, client?: MatrixC
|
|||
}
|
||||
|
||||
const room = client.getRoom(mxEvent.getRoomId());
|
||||
if (EVENT_VISIBILITY_CHANGE_TYPE.name
|
||||
&& room.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.name, client.getUserId())
|
||||
if (
|
||||
EVENT_VISIBILITY_CHANGE_TYPE.name &&
|
||||
room.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.name, client.getUserId())
|
||||
) {
|
||||
// We're a moderator (as indicated by prefixed event name), show the message.
|
||||
return MessageModerationState.SEE_THROUGH_FOR_CURRENT_USER;
|
||||
}
|
||||
if (EVENT_VISIBILITY_CHANGE_TYPE.altName
|
||||
&& room.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.altName, client.getUserId())
|
||||
if (
|
||||
EVENT_VISIBILITY_CHANGE_TYPE.altName &&
|
||||
room.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.altName, client.getUserId())
|
||||
) {
|
||||
// We're a moderator (as indicated by unprefixed event name), show the message.
|
||||
return MessageModerationState.SEE_THROUGH_FOR_CURRENT_USER;
|
||||
|
@ -212,10 +207,7 @@ export function getMessageModerationState(mxEvent: MatrixEvent, client?: MatrixC
|
|||
export function isVoiceMessage(mxEvent: MatrixEvent): boolean {
|
||||
const content = mxEvent.getContent();
|
||||
// MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245
|
||||
return (
|
||||
!!content['org.matrix.msc2516.voice'] ||
|
||||
!!content['org.matrix.msc3245.voice']
|
||||
);
|
||||
return !!content["org.matrix.msc2516.voice"] || !!content["org.matrix.msc3245.voice"];
|
||||
}
|
||||
|
||||
export async function fetchInitialEvent(
|
||||
|
@ -233,15 +225,15 @@ export async function fetchInitialEvent(
|
|||
initialEvent = null;
|
||||
}
|
||||
|
||||
if (client.supportsExperimentalThreads() &&
|
||||
if (
|
||||
client.supportsExperimentalThreads() &&
|
||||
initialEvent?.isRelation(THREAD_RELATION_TYPE.name) &&
|
||||
!initialEvent.getThread()
|
||||
) {
|
||||
const threadId = initialEvent.threadRootId;
|
||||
const room = client.getRoom(roomId);
|
||||
const mapper = client.getEventMapper();
|
||||
const rootEvent = room.findEventById(threadId)
|
||||
?? mapper(await client.fetchRoomEvent(roomId, threadId));
|
||||
const rootEvent = room.findEventById(threadId) ?? mapper(await client.fetchRoomEvent(roomId, threadId));
|
||||
try {
|
||||
room.createThread(threadId, rootEvent, [initialEvent], true);
|
||||
} catch (e) {
|
||||
|
@ -278,10 +270,7 @@ export const isLocationEvent = (event: MatrixEvent): boolean => {
|
|||
const eventType = event.getType();
|
||||
return (
|
||||
M_LOCATION.matches(eventType) ||
|
||||
(
|
||||
eventType === EventType.RoomMessage &&
|
||||
M_LOCATION.matches(event.getContent().msgtype)
|
||||
)
|
||||
(eventType === EventType.RoomMessage && M_LOCATION.matches(event.getContent().msgtype))
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ type DownloadOptions = {
|
|||
// set up the iframe as a singleton so we don't have to figure out destruction of it down the line.
|
||||
let managedIframe: HTMLIFrameElement;
|
||||
let onLoadPromise: Promise<void>;
|
||||
function getManagedIframe(): { iframe: HTMLIFrameElement, onLoadPromise: Promise<void> } {
|
||||
function getManagedIframe(): { iframe: HTMLIFrameElement; onLoadPromise: Promise<void> } {
|
||||
if (managedIframe) return { iframe: managedIframe, onLoadPromise };
|
||||
|
||||
managedIframe = document.createElement("iframe");
|
||||
|
@ -49,7 +49,7 @@ function getManagedIframe(): { iframe: HTMLIFrameElement, onLoadPromise: Promise
|
|||
// noinspection JSConstantReassignment
|
||||
managedIframe.sandbox = "allow-scripts allow-downloads allow-downloads-without-user-activation";
|
||||
|
||||
onLoadPromise = new Promise(resolve => {
|
||||
onLoadPromise = new Promise((resolve) => {
|
||||
managedIframe.onload = () => {
|
||||
resolve();
|
||||
};
|
||||
|
@ -75,8 +75,7 @@ export class FileDownloader {
|
|||
* @param iframeFn Function to get a pre-configured iframe. Set to null to have the downloader
|
||||
* use a generic, hidden, iframe.
|
||||
*/
|
||||
constructor(private iframeFn: getIframeFn = null) {
|
||||
}
|
||||
constructor(private iframeFn: getIframeFn = null) {}
|
||||
|
||||
private get iframe(): HTMLIFrameElement {
|
||||
const iframe = this.iframeFn?.();
|
||||
|
@ -92,11 +91,14 @@ export class FileDownloader {
|
|||
public async download({ blob, name, autoDownload = true, opts = DEFAULT_STYLES }: DownloadOptions) {
|
||||
const iframe = this.iframe; // get the iframe first just in case we need to await onload
|
||||
if (this.onLoadPromise) await this.onLoadPromise;
|
||||
iframe.contentWindow.postMessage({
|
||||
...opts,
|
||||
blob: blob,
|
||||
download: name,
|
||||
auto: autoDownload,
|
||||
}, '*');
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
...opts,
|
||||
blob: blob,
|
||||
download: name,
|
||||
auto: autoDownload,
|
||||
},
|
||||
"*",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,10 +15,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { filesize } from 'filesize';
|
||||
import { filesize } from "filesize";
|
||||
|
||||
import { IMediaEventContent } from '../customisations/models/IMediaEventContent';
|
||||
import { _t } from '../languageHandler';
|
||||
import { IMediaEventContent } from "../customisations/models/IMediaEventContent";
|
||||
import { _t } from "../languageHandler";
|
||||
|
||||
/**
|
||||
* Extracts a human readable label for the file attachment to use as
|
||||
|
@ -47,13 +47,16 @@ export function presentableTextForFile(
|
|||
// will have a 3 character (plus full stop) extension. The goal is to knock
|
||||
// the label down to 15-25 characters, not perfect accuracy.
|
||||
if (shortened && text.length > 19) {
|
||||
const parts = text.split('.');
|
||||
let fileName = parts.slice(0, parts.length - 1).join('.').substring(0, 15);
|
||||
const parts = text.split(".");
|
||||
let fileName = parts
|
||||
.slice(0, parts.length - 1)
|
||||
.join(".")
|
||||
.substring(0, 15);
|
||||
const extension = parts[parts.length - 1];
|
||||
|
||||
// Trim off any full stops from the file name to avoid a case where we
|
||||
// add an ellipsis that looks really funky.
|
||||
fileName = fileName.replace(/\.*$/g, '');
|
||||
fileName = fileName.replace(/\.*$/g, "");
|
||||
|
||||
text = `${fileName}...${extension}`;
|
||||
}
|
||||
|
@ -66,7 +69,7 @@ export function presentableTextForFile(
|
|||
// it since it is "ugly", users generally aren't aware what it
|
||||
// means and the type of the attachment can usually be inferred
|
||||
// from the file extension.
|
||||
text += ' (' + filesize(content.info.size) + ')';
|
||||
text += " (" + filesize(content.info.size) + ")";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
|
|
@ -29,13 +29,15 @@ function safariVersionCheck(ua: string): boolean {
|
|||
if (safariVersionMatch) {
|
||||
const macOSVersionStr = safariVersionMatch[1];
|
||||
const safariVersionStr = safariVersionMatch[2];
|
||||
const macOSVersion = macOSVersionStr.split("_").map(n => parseInt(n, 10));
|
||||
const safariVersion = safariVersionStr.split(".").map(n => parseInt(n, 10));
|
||||
const macOSVersion = macOSVersionStr.split("_").map((n) => parseInt(n, 10));
|
||||
const safariVersion = safariVersionStr.split(".").map((n) => parseInt(n, 10));
|
||||
const colrFontSupported = macOSVersion[0] >= 10 && macOSVersion[1] >= 14 && safariVersion[0] >= 12;
|
||||
// https://www.colorfonts.wtf/ states safari supports COLR fonts from this version on
|
||||
logger.log(`COLR support on Safari requires macOS 10.14 and Safari 12, ` +
|
||||
`detected Safari ${safariVersionStr} on macOS ${macOSVersionStr}, ` +
|
||||
`COLR supported: ${colrFontSupported}`);
|
||||
logger.log(
|
||||
`COLR support on Safari requires macOS 10.14 and Safari 12, ` +
|
||||
`detected Safari ${safariVersionStr} on macOS ${macOSVersionStr}, ` +
|
||||
`COLR supported: ${colrFontSupported}`,
|
||||
);
|
||||
return colrFontSupported;
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -66,11 +68,12 @@ async function isColrFontSupported(): Promise<boolean> {
|
|||
}
|
||||
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
const img = new Image();
|
||||
// eslint-disable-next-line
|
||||
const fontCOLR = 'd09GRgABAAAAAAKAAAwAAAAAAowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDT0xSAAACVAAAABYAAAAYAAIAJUNQQUwAAAJsAAAAEgAAABLJAAAQT1MvMgAAAYAAAAA6AAAAYBfxJ0pjbWFwAAABxAAAACcAAAAsAAzpM2dseWYAAAH0AAAAGgAAABoNIh0kaGVhZAAAARwAAAAvAAAANgxLumdoaGVhAAABTAAAABUAAAAkCAEEAmhtdHgAAAG8AAAABgAAAAYEAAAAbG9jYQAAAewAAAAGAAAABgANAABtYXhwAAABZAAAABsAAAAgAg4AHW5hbWUAAAIQAAAAOAAAAD4C5wsecG9zdAAAAkgAAAAMAAAAIAADAAB4AWNgZGAAYQ5+qdB4fpuvDNIsDCBwaQGTAIi+VlscBaJZGMDiHAxMIAoAtjIF/QB4AWNgZGBgYQACOAkUQQWMAAGRABAAAAB4AWNgZGBgYGJgAdMMUJILJMQgAWICAAH3AC4AeAFjYGFhYJzAwMrAwDST6QwDA0M/hGZ8zWDMyMmAChgFkDgKQMBw4CXDSwYWEBdIYgAFBgYA/8sIdAAABAAAAAAAAAB4AWNgYGBkYAZiBgYeBhYGBSDNAoRA/kuG//8hpDgjWJ4BAFVMBiYAAAAAAAANAAAAAQAAAAAEAAQAAAMAABEhESEEAPwABAD8AAAAeAEtxgUNgAAAAMHHIQTShTlOAty9/4bf7AARCwlBNhBw4L/43qXjYGUmf19TMuLcj/BJL3XfBg54AWNgZsALAAB9AAR4AWNgYGAEYj4gFgGygGwICQACOwAoAAAAAAABAAEAAQAAAA4AAAAAyP8AAA==';
|
||||
const fontCOLR =
|
||||
"d09GRgABAAAAAAKAAAwAAAAAAowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDT0xSAAACVAAAABYAAAAYAAIAJUNQQUwAAAJsAAAAEgAAABLJAAAQT1MvMgAAAYAAAAA6AAAAYBfxJ0pjbWFwAAABxAAAACcAAAAsAAzpM2dseWYAAAH0AAAAGgAAABoNIh0kaGVhZAAAARwAAAAvAAAANgxLumdoaGVhAAABTAAAABUAAAAkCAEEAmhtdHgAAAG8AAAABgAAAAYEAAAAbG9jYQAAAewAAAAGAAAABgANAABtYXhwAAABZAAAABsAAAAgAg4AHW5hbWUAAAIQAAAAOAAAAD4C5wsecG9zdAAAAkgAAAAMAAAAIAADAAB4AWNgZGAAYQ5+qdB4fpuvDNIsDCBwaQGTAIi+VlscBaJZGMDiHAxMIAoAtjIF/QB4AWNgZGBgYQACOAkUQQWMAAGRABAAAAB4AWNgZGBgYGJgAdMMUJILJMQgAWICAAH3AC4AeAFjYGFhYJzAwMrAwDST6QwDA0M/hGZ8zWDMyMmAChgFkDgKQMBw4CXDSwYWEBdIYgAFBgYA/8sIdAAABAAAAAAAAAB4AWNgYGBkYAZiBgYeBhYGBSDNAoRA/kuG//8hpDgjWJ4BAFVMBiYAAAAAAAANAAAAAQAAAAAEAAQAAAMAABEhESEEAPwABAD8AAAAeAEtxgUNgAAAAMHHIQTShTlOAty9/4bf7AARCwlBNhBw4L/43qXjYGUmf19TMuLcj/BJL3XfBg54AWNgZsALAAB9AAR4AWNgYGAEYj4gFgGygGwICQACOwAoAAAAAAABAAEAAQAAAA4AAAAAyP8AAA==";
|
||||
const svg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="100" style="background:#fff;fill:#000;">
|
||||
<style type="text/css">
|
||||
|
@ -86,13 +89,13 @@ async function isColrFontSupported(): Promise<boolean> {
|
|||
canvas.width = 20;
|
||||
canvas.height = 100;
|
||||
|
||||
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
|
||||
img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
|
||||
|
||||
logger.log("Waiting for COLR SVG to load");
|
||||
await new Promise(resolve => img.onload = resolve);
|
||||
await new Promise((resolve) => (img.onload = resolve));
|
||||
logger.log("Drawing canvas to detect COLR support");
|
||||
context.drawImage(img, 0, 0);
|
||||
const colrFontSupported = (context.getImageData(10, 10, 1, 1).data[0] === 200);
|
||||
const colrFontSupported = context.getImageData(10, 10, 1, 1).data[0] === 200;
|
||||
logger.log("Canvas check revealed COLR is supported? " + colrFontSupported);
|
||||
return colrFontSupported;
|
||||
} catch (e) {
|
||||
|
@ -124,4 +127,3 @@ export async function fixupColorFonts(): Promise<void> {
|
|||
}
|
||||
// ...and if SBIX is not supported, the browser will fall back to one of the native fonts specified.
|
||||
}
|
||||
|
||||
|
|
|
@ -15,8 +15,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { _t } from '../languageHandler';
|
||||
import { jsxJoin } from './ReactUtils';
|
||||
import { _t } from "../languageHandler";
|
||||
import { jsxJoin } from "./ReactUtils";
|
||||
|
||||
/**
|
||||
* formats numbers to fit into ~3 characters, suitable for badge counts
|
||||
|
@ -45,15 +45,15 @@ export function formatCountLong(count: number): string {
|
|||
* e.g: 1024 -> 1.00 KB
|
||||
*/
|
||||
export function formatBytes(bytes: number, decimals = 2): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -82,7 +82,7 @@ export function hashCode(str: string): number {
|
|||
}
|
||||
for (i = 0; i < str.length; i++) {
|
||||
chr = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + chr;
|
||||
hash = (hash << 5) - hash + chr;
|
||||
hash |= 0;
|
||||
}
|
||||
return Math.abs(hash);
|
||||
|
@ -108,9 +108,7 @@ export function formatCommaSeparatedList(items: string[], itemLimit?: number): s
|
|||
export function formatCommaSeparatedList(items: JSX.Element[], itemLimit?: number): JSX.Element;
|
||||
export function formatCommaSeparatedList(items: Array<JSX.Element | string>, itemLimit?: number): JSX.Element | string;
|
||||
export function formatCommaSeparatedList(items: Array<JSX.Element | string>, itemLimit?: number): JSX.Element | string {
|
||||
const remaining = itemLimit === undefined ? 0 : Math.max(
|
||||
items.length - itemLimit, 0,
|
||||
);
|
||||
const remaining = itemLimit === undefined ? 0 : Math.max(items.length - itemLimit, 0);
|
||||
if (items.length === 0) {
|
||||
return "";
|
||||
} else if (items.length === 1) {
|
||||
|
@ -124,7 +122,7 @@ export function formatCommaSeparatedList(items: Array<JSX.Element | string>, ite
|
|||
}
|
||||
|
||||
let joinedItems;
|
||||
if (items.every(e => typeof e === "string")) {
|
||||
if (items.every((e) => typeof e === "string")) {
|
||||
joinedItems = items.join(", ");
|
||||
} else {
|
||||
joinedItems = jsxJoin(items, ", ");
|
||||
|
|
|
@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import SdkConfig from '../SdkConfig';
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
|
||||
export function getHostingLink(campaign: string): string {
|
||||
const hostingLink = SdkConfig.get().hosting_signup_link;
|
||||
if (!hostingLink) return null;
|
||||
if (!campaign) return hostingLink;
|
||||
|
||||
if (MatrixClientPeg.get().getDomain() !== 'matrix.org') return null;
|
||||
if (MatrixClientPeg.get().getDomain() !== "matrix.org") return null;
|
||||
|
||||
try {
|
||||
const hostingUrl = new URL(hostingLink);
|
||||
|
|
|
@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
|
||||
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import SdkConfig from '../SdkConfig';
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
|
||||
export function getDefaultIdentityServerUrl(): string {
|
||||
return SdkConfig.get("validated_server_config").isUrl;
|
||||
|
@ -45,10 +45,10 @@ export async function doesIdentityServerHaveTerms(fullUrl: string): Promise<bool
|
|||
}
|
||||
}
|
||||
|
||||
return terms && terms["policies"] && (Object.keys(terms["policies"]).length > 0);
|
||||
return terms && terms["policies"] && Object.keys(terms["policies"]).length > 0;
|
||||
}
|
||||
|
||||
export function doesAccountDataHaveIdentityServer(): boolean {
|
||||
const event = MatrixClientPeg.get().getAccountData("m.identity_server");
|
||||
return event && event.getContent() && event.getContent()['base_url'];
|
||||
return event && event.getContent() && event.getContent()["base_url"];
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ export async function blobIsAnimated(mimeType: string | undefined, blob: Blob):
|
|||
|
||||
// Graphics Control Extension section is where GIF animation data is stored
|
||||
// First 2 bytes must be 0x21 and 0xF9
|
||||
if ((extensionIntroducer & 0x21) && (graphicsControlLabel & 0xF9)) {
|
||||
if (extensionIntroducer & 0x21 && graphicsControlLabel & 0xf9) {
|
||||
// skip to the 2 bytes with the delay time
|
||||
delayTime = dv.getUint16(offset + 4);
|
||||
}
|
||||
|
@ -88,17 +88,13 @@ export async function blobIsAnimated(mimeType: string | undefined, blob: Blob):
|
|||
case "image/apng": {
|
||||
// Based on https://stackoverflow.com/a/68618296
|
||||
const arr = await blob.arrayBuffer();
|
||||
if (arrayHasDiff([
|
||||
0x89,
|
||||
0x50, 0x4E, 0x47,
|
||||
0x0D, 0x0A,
|
||||
0x1A,
|
||||
0x0A,
|
||||
], Array.from(arrayBufferRead(arr, 0, 8)))) {
|
||||
if (
|
||||
arrayHasDiff([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], Array.from(arrayBufferRead(arr, 0, 8)))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 8; i < blob.size;) {
|
||||
for (let i = 8; i < blob.size; ) {
|
||||
const length = arrayBufferReadInt(arr, i);
|
||||
i += 4;
|
||||
const type = arrayBufferReadStr(arr, i, 4);
|
||||
|
|
|
@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import { _t } from '../languageHandler';
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { _t } from "../languageHandler";
|
||||
|
||||
export function getNameForEventRoom(userId: string, roomId: string): string {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
|
|
@ -22,8 +22,7 @@ export class LazyValue<T> {
|
|||
private prom: Promise<T>;
|
||||
private done = false;
|
||||
|
||||
public constructor(private getFn: () => Promise<T>) {
|
||||
}
|
||||
public constructor(private getFn: () => Promise<T>) {}
|
||||
|
||||
/**
|
||||
* Whether or not a cached value is present.
|
||||
|
@ -48,7 +47,7 @@ export class LazyValue<T> {
|
|||
if (this.prom) return this.prom;
|
||||
this.prom = this.getFn();
|
||||
|
||||
return this.prom.then(v => {
|
||||
return this.prom.then((v) => {
|
||||
this.val = v;
|
||||
this.done = true;
|
||||
return v;
|
||||
|
|
|
@ -30,8 +30,7 @@ export class MarkedExecution {
|
|||
* @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) {
|
||||
}
|
||||
constructor(private fn: () => void, private onMarkCallback?: () => void) {}
|
||||
|
||||
/**
|
||||
* Resets the mark without calling the function.
|
||||
|
|
|
@ -47,9 +47,11 @@ export class MediaEventHelper implements IDestroyable {
|
|||
}
|
||||
|
||||
public get fileName(): string {
|
||||
return this.event.getContent<IMediaEventContent>().filename
|
||||
|| this.event.getContent<IMediaEventContent>().body
|
||||
|| "download";
|
||||
return (
|
||||
this.event.getContent<IMediaEventContent>().filename ||
|
||||
this.event.getContent<IMediaEventContent>().body ||
|
||||
"download"
|
||||
);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
|
@ -83,7 +85,7 @@ export class MediaEventHelper implements IDestroyable {
|
|||
const content = this.event.getContent<IMediaEventContent>();
|
||||
return decryptFile(content.file, content.info);
|
||||
}
|
||||
return this.media.downloadSource().then(r => r.blob());
|
||||
return this.media.downloadSource().then((r) => r.blob());
|
||||
};
|
||||
|
||||
private fetchThumbnail = () => {
|
||||
|
@ -100,7 +102,7 @@ export class MediaEventHelper implements IDestroyable {
|
|||
}
|
||||
}
|
||||
|
||||
return fetch(this.media.thumbnailHttp).then(r => r.blob());
|
||||
return fetch(this.media.thumbnailHttp).then((r) => r.blob());
|
||||
};
|
||||
|
||||
public static isEligible(event: MatrixEvent): boolean {
|
||||
|
@ -110,14 +112,9 @@ export class MediaEventHelper implements IDestroyable {
|
|||
if (event.getType() !== EventType.RoomMessage) return false;
|
||||
|
||||
const content = event.getContent();
|
||||
const mediaMsgTypes: string[] = [
|
||||
MsgType.Video,
|
||||
MsgType.Audio,
|
||||
MsgType.Image,
|
||||
MsgType.File,
|
||||
];
|
||||
const mediaMsgTypes: string[] = [MsgType.Video, MsgType.Audio, MsgType.Image, MsgType.File];
|
||||
if (mediaMsgTypes.includes(content.msgtype)) return true;
|
||||
if (typeof(content.url) === 'string') return true;
|
||||
if (typeof content.url === "string") return true;
|
||||
|
||||
// Finally, it's probably not media
|
||||
return false;
|
||||
|
|
|
@ -17,8 +17,8 @@ limitations under the License.
|
|||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t } from '../languageHandler';
|
||||
import SdkConfig from '../SdkConfig';
|
||||
import { _t } from "../languageHandler";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
|
||||
const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle;
|
||||
|
||||
|
@ -30,14 +30,12 @@ const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle;
|
|||
* @param {string} friendlyText
|
||||
* @returns {{message: string, friendlyText: string}}
|
||||
*/
|
||||
function friendlyError(
|
||||
message: string, friendlyText: string,
|
||||
): { message: string, friendlyText: string } {
|
||||
function friendlyError(message: string, friendlyText: string): { message: string; friendlyText: string } {
|
||||
return { message, friendlyText };
|
||||
}
|
||||
|
||||
function cryptoFailMsg(): string {
|
||||
return _t('Your browser does not support the required cryptography extensions');
|
||||
return _t("Your browser does not support the required cryptography extensions");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,26 +53,23 @@ export async function decryptMegolmKeyFile(data: ArrayBuffer, password: string):
|
|||
|
||||
// check we have a version byte
|
||||
if (body.length < 1) {
|
||||
throw friendlyError('Invalid file: too short',
|
||||
_t('Not a valid %(brand)s keyfile', { brand }));
|
||||
throw friendlyError("Invalid file: too short", _t("Not a valid %(brand)s keyfile", { brand }));
|
||||
}
|
||||
|
||||
const version = body[0];
|
||||
if (version !== 1) {
|
||||
throw friendlyError('Unsupported version',
|
||||
_t('Not a valid %(brand)s keyfile', { brand }));
|
||||
throw friendlyError("Unsupported version", _t("Not a valid %(brand)s keyfile", { brand }));
|
||||
}
|
||||
|
||||
const ciphertextLength = body.length-(1+16+16+4+32);
|
||||
const ciphertextLength = body.length - (1 + 16 + 16 + 4 + 32);
|
||||
if (ciphertextLength < 0) {
|
||||
throw friendlyError('Invalid file: too short',
|
||||
_t('Not a valid %(brand)s keyfile', { brand }));
|
||||
throw friendlyError("Invalid file: too short", _t("Not a valid %(brand)s keyfile", { brand }));
|
||||
}
|
||||
|
||||
const salt = body.subarray(1, 1+16);
|
||||
const iv = body.subarray(17, 17+16);
|
||||
const iterations = body[33] << 24 | body[34] << 16 | body[35] << 8 | body[36];
|
||||
const ciphertext = body.subarray(37, 37+ciphertextLength);
|
||||
const salt = body.subarray(1, 1 + 16);
|
||||
const iv = body.subarray(17, 17 + 16);
|
||||
const iterations = (body[33] << 24) | (body[34] << 16) | (body[35] << 8) | body[36];
|
||||
const ciphertext = body.subarray(37, 37 + ciphertextLength);
|
||||
const hmac = body.subarray(-32);
|
||||
|
||||
const [aesKey, hmacKey] = await deriveKeys(salt, iterations, password);
|
||||
|
@ -82,18 +77,12 @@ export async function decryptMegolmKeyFile(data: ArrayBuffer, password: string):
|
|||
|
||||
let isValid;
|
||||
try {
|
||||
isValid = await subtleCrypto.verify(
|
||||
{ name: 'HMAC' },
|
||||
hmacKey,
|
||||
hmac,
|
||||
toVerify,
|
||||
);
|
||||
isValid = await subtleCrypto.verify({ name: "HMAC" }, hmacKey, hmac, toVerify);
|
||||
} catch (e) {
|
||||
throw friendlyError('subtleCrypto.verify failed: ' + e, cryptoFailMsg());
|
||||
throw friendlyError("subtleCrypto.verify failed: " + e, cryptoFailMsg());
|
||||
}
|
||||
if (!isValid) {
|
||||
throw friendlyError('hmac mismatch',
|
||||
_t('Authentication check failed: incorrect password?'));
|
||||
throw friendlyError("hmac mismatch", _t("Authentication check failed: incorrect password?"));
|
||||
}
|
||||
|
||||
let plaintext;
|
||||
|
@ -108,7 +97,7 @@ export async function decryptMegolmKeyFile(data: ArrayBuffer, password: string):
|
|||
ciphertext,
|
||||
);
|
||||
} catch (e) {
|
||||
throw friendlyError('subtleCrypto.decrypt failed: ' + e, cryptoFailMsg());
|
||||
throw friendlyError("subtleCrypto.decrypt failed: " + e, cryptoFailMsg());
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(new Uint8Array(plaintext));
|
||||
|
@ -158,33 +147,32 @@ export async function encryptMegolmKeyFile(
|
|||
encodedData,
|
||||
);
|
||||
} catch (e) {
|
||||
throw friendlyError('subtleCrypto.encrypt failed: ' + e, cryptoFailMsg());
|
||||
throw friendlyError("subtleCrypto.encrypt failed: " + e, cryptoFailMsg());
|
||||
}
|
||||
|
||||
const cipherArray = new Uint8Array(ciphertext);
|
||||
const bodyLength = (1+salt.length+iv.length+4+cipherArray.length+32);
|
||||
const bodyLength = 1 + salt.length + iv.length + 4 + cipherArray.length + 32;
|
||||
const resultBuffer = new Uint8Array(bodyLength);
|
||||
let idx = 0;
|
||||
resultBuffer[idx++] = 1; // version
|
||||
resultBuffer.set(salt, idx); idx += salt.length;
|
||||
resultBuffer.set(iv, idx); idx += iv.length;
|
||||
resultBuffer.set(salt, idx);
|
||||
idx += salt.length;
|
||||
resultBuffer.set(iv, idx);
|
||||
idx += iv.length;
|
||||
resultBuffer[idx++] = kdfRounds >> 24;
|
||||
resultBuffer[idx++] = (kdfRounds >> 16) & 0xff;
|
||||
resultBuffer[idx++] = (kdfRounds >> 8) & 0xff;
|
||||
resultBuffer[idx++] = kdfRounds & 0xff;
|
||||
resultBuffer.set(cipherArray, idx); idx += cipherArray.length;
|
||||
resultBuffer.set(cipherArray, idx);
|
||||
idx += cipherArray.length;
|
||||
|
||||
const toSign = resultBuffer.subarray(0, idx);
|
||||
|
||||
let hmac;
|
||||
try {
|
||||
hmac = await subtleCrypto.sign(
|
||||
{ name: 'HMAC' },
|
||||
hmacKey,
|
||||
toSign,
|
||||
);
|
||||
hmac = await subtleCrypto.sign({ name: "HMAC" }, hmacKey, toSign);
|
||||
} catch (e) {
|
||||
throw friendlyError('subtleCrypto.sign failed: ' + e, cryptoFailMsg());
|
||||
throw friendlyError("subtleCrypto.sign failed: " + e, cryptoFailMsg());
|
||||
}
|
||||
|
||||
const hmacArray = new Uint8Array(hmac);
|
||||
|
@ -205,31 +193,27 @@ async function deriveKeys(salt: Uint8Array, iterations: number, password: string
|
|||
|
||||
let key;
|
||||
try {
|
||||
key = await subtleCrypto.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(password),
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveBits'],
|
||||
);
|
||||
key = await subtleCrypto.importKey("raw", new TextEncoder().encode(password), { name: "PBKDF2" }, false, [
|
||||
"deriveBits",
|
||||
]);
|
||||
} catch (e) {
|
||||
throw friendlyError('subtleCrypto.importKey failed: ' + e, cryptoFailMsg());
|
||||
throw friendlyError("subtleCrypto.importKey failed: " + e, cryptoFailMsg());
|
||||
}
|
||||
|
||||
let keybits;
|
||||
try {
|
||||
keybits = await subtleCrypto.deriveBits(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
name: "PBKDF2",
|
||||
salt: salt,
|
||||
iterations: iterations,
|
||||
hash: 'SHA-512',
|
||||
hash: "SHA-512",
|
||||
},
|
||||
key,
|
||||
512,
|
||||
);
|
||||
} catch (e) {
|
||||
throw friendlyError('subtleCrypto.deriveBits failed: ' + e, cryptoFailMsg());
|
||||
throw friendlyError("subtleCrypto.deriveBits failed: " + e, cryptoFailMsg());
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
@ -238,34 +222,32 @@ async function deriveKeys(salt: Uint8Array, iterations: number, password: string
|
|||
const aesKey = keybits.slice(0, 32);
|
||||
const hmacKey = keybits.slice(32);
|
||||
|
||||
const aesProm = subtleCrypto.importKey(
|
||||
'raw',
|
||||
aesKey,
|
||||
{ name: 'AES-CTR' },
|
||||
false,
|
||||
['encrypt', 'decrypt'],
|
||||
).catch((e) => {
|
||||
throw friendlyError('subtleCrypto.importKey failed for AES key: ' + e, cryptoFailMsg());
|
||||
});
|
||||
const aesProm = subtleCrypto
|
||||
.importKey("raw", aesKey, { name: "AES-CTR" }, false, ["encrypt", "decrypt"])
|
||||
.catch((e) => {
|
||||
throw friendlyError("subtleCrypto.importKey failed for AES key: " + e, cryptoFailMsg());
|
||||
});
|
||||
|
||||
const hmacProm = subtleCrypto.importKey(
|
||||
'raw',
|
||||
hmacKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: { name: 'SHA-256' },
|
||||
},
|
||||
false,
|
||||
['sign', 'verify'],
|
||||
).catch((e) => {
|
||||
throw friendlyError('subtleCrypto.importKey failed for HMAC key: ' + e, cryptoFailMsg());
|
||||
});
|
||||
const hmacProm = subtleCrypto
|
||||
.importKey(
|
||||
"raw",
|
||||
hmacKey,
|
||||
{
|
||||
name: "HMAC",
|
||||
hash: { name: "SHA-256" },
|
||||
},
|
||||
false,
|
||||
["sign", "verify"],
|
||||
)
|
||||
.catch((e) => {
|
||||
throw friendlyError("subtleCrypto.importKey failed for HMAC key: " + e, cryptoFailMsg());
|
||||
});
|
||||
|
||||
return Promise.all([aesProm, hmacProm]);
|
||||
}
|
||||
|
||||
const HEADER_LINE = '-----BEGIN MEGOLM SESSION DATA-----';
|
||||
const TRAILER_LINE = '-----END MEGOLM SESSION DATA-----';
|
||||
const HEADER_LINE = "-----BEGIN MEGOLM SESSION DATA-----";
|
||||
const TRAILER_LINE = "-----END MEGOLM SESSION DATA-----";
|
||||
|
||||
/**
|
||||
* Unbase64 an ascii-armoured megolm key file
|
||||
|
@ -285,14 +267,14 @@ function unpackMegolmKeyFile(data: ArrayBuffer): Uint8Array {
|
|||
let lineStart = 0;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (1) {
|
||||
const lineEnd = fileStr.indexOf('\n', lineStart);
|
||||
const lineEnd = fileStr.indexOf("\n", lineStart);
|
||||
if (lineEnd < 0) {
|
||||
throw new Error('Header line not found');
|
||||
throw new Error("Header line not found");
|
||||
}
|
||||
const line = fileStr.slice(lineStart, lineEnd).trim();
|
||||
|
||||
// start the next line after the newline
|
||||
lineStart = lineEnd+1;
|
||||
lineStart = lineEnd + 1;
|
||||
|
||||
if (line === HEADER_LINE) {
|
||||
break;
|
||||
|
@ -304,18 +286,18 @@ function unpackMegolmKeyFile(data: ArrayBuffer): Uint8Array {
|
|||
// look for the end line
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (1) {
|
||||
const lineEnd = fileStr.indexOf('\n', lineStart);
|
||||
const lineEnd = fileStr.indexOf("\n", lineStart);
|
||||
const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd).trim();
|
||||
if (line === TRAILER_LINE) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (lineEnd < 0) {
|
||||
throw new Error('Trailer line not found');
|
||||
throw new Error("Trailer line not found");
|
||||
}
|
||||
|
||||
// start the next line after the newline
|
||||
lineStart = lineEnd+1;
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
|
||||
const dataEnd = lineStart;
|
||||
|
@ -333,19 +315,19 @@ function unpackMegolmKeyFile(data: ArrayBuffer): Uint8Array {
|
|||
function packMegolmKeyFile(data: Uint8Array): ArrayBuffer {
|
||||
// we split into lines before base64ing, because encodeBase64 doesn't deal
|
||||
// terribly well with large arrays.
|
||||
const LINE_LENGTH = (72 * 4 / 3);
|
||||
const LINE_LENGTH = (72 * 4) / 3;
|
||||
const nLines = Math.ceil(data.length / LINE_LENGTH);
|
||||
const lines = new Array(nLines + 3);
|
||||
lines[0] = HEADER_LINE;
|
||||
let o = 0;
|
||||
let i;
|
||||
for (i = 1; i <= nLines; i++) {
|
||||
lines[i] = encodeBase64(data.subarray(o, o+LINE_LENGTH));
|
||||
lines[i] = encodeBase64(data.subarray(o, o + LINE_LENGTH));
|
||||
o += LINE_LENGTH;
|
||||
}
|
||||
lines[i++] = TRAILER_LINE;
|
||||
lines[i] = '';
|
||||
return (new TextEncoder().encode(lines.join('\n'))).buffer;
|
||||
lines[i] = "";
|
||||
return new TextEncoder().encode(lines.join("\n")).buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch';
|
||||
import React, { ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
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 { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { bodyToHtml, checkBlockNode, IOptsReturnString } from "../HtmlUtils";
|
||||
|
||||
const decodeEntities = (function() {
|
||||
const decodeEntities = (function () {
|
||||
let textarea = null;
|
||||
return function(str: string): string {
|
||||
return function (str: string): string {
|
||||
if (!textarea) {
|
||||
textarea = document.createElement("textarea");
|
||||
}
|
||||
|
@ -235,7 +235,8 @@ function filterCancelingOutDiffs(originalDiffActions: IDiff[]): IDiff[] {
|
|||
const diff = diffActions[i];
|
||||
if (diff.action === "removeTextElement") {
|
||||
const nextDiff = diffActions[i + 1];
|
||||
const cancelsOut = nextDiff &&
|
||||
const cancelsOut =
|
||||
nextDiff &&
|
||||
nextDiff.action === "addTextElement" &&
|
||||
nextDiff.text === diff.text &&
|
||||
routeIsEqual(nextDiff.route, diff.route);
|
||||
|
@ -283,8 +284,8 @@ export function editBodyDiffToHtml(originalContent: IContent, editContent: ICont
|
|||
// take the html out of the modified DOM tree again
|
||||
const safeBody = originalRootNode.innerHTML;
|
||||
const className = classNames({
|
||||
'mx_EventTile_body': true,
|
||||
'markdown-body': true,
|
||||
"mx_EventTile_body": true,
|
||||
"markdown-body": true,
|
||||
});
|
||||
return <span key="body" className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" />;
|
||||
}
|
||||
|
|
|
@ -27,24 +27,22 @@ export function normalizeWheelEvent(event: WheelEvent): WheelEvent {
|
|||
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);
|
||||
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,
|
||||
},
|
||||
);
|
||||
return new WheelEvent("syntheticWheel", {
|
||||
deltaMode: 0,
|
||||
deltaY: deltaY,
|
||||
deltaX: deltaX,
|
||||
deltaZ: deltaZ,
|
||||
...event,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -21,8 +21,8 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
|
|||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { HistoryVisibility } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import { AddressType, getAddressType } from '../UserAddress';
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { AddressType, getAddressType } from "../UserAddress";
|
||||
import { _t } from "../languageHandler";
|
||||
import Modal from "../Modal";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
|
@ -38,7 +38,7 @@ interface IError {
|
|||
errcode: string;
|
||||
}
|
||||
|
||||
const UNKNOWN_PROFILE_ERRORS = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND'];
|
||||
const UNKNOWN_PROFILE_ERRORS = ["M_NOT_FOUND", "M_USER_NOT_FOUND", "M_PROFILE_UNDISCLOSED", "M_PROFILE_NOT_FOUND"];
|
||||
|
||||
export type CompletionStates = Record<string, InviteState>;
|
||||
|
||||
|
@ -92,8 +92,8 @@ export default class MultiInviter {
|
|||
if (getAddressType(addr) === null) {
|
||||
this.completionStates[addr] = InviteState.Error;
|
||||
this.errors[addr] = {
|
||||
errcode: 'M_INVALID',
|
||||
errorText: _t('Unrecognised address'),
|
||||
errcode: "M_INVALID",
|
||||
errorText: _t("Unrecognised address"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ export default class MultiInviter {
|
|||
return this.deferred.promise;
|
||||
}
|
||||
|
||||
return this.deferred.promise.then(async states => {
|
||||
return this.deferred.promise.then(async (states) => {
|
||||
const invitedUsers = [];
|
||||
for (const [addr, state] of Object.entries(states)) {
|
||||
if (state === InviteState.Invited && getAddressType(addr) === AddressType.MatrixUserId) {
|
||||
|
@ -134,7 +134,7 @@ export default class MultiInviter {
|
|||
if (!this.busy) return;
|
||||
|
||||
this.canceled = true;
|
||||
this.deferred.reject(new Error('canceled'));
|
||||
this.deferred.reject(new Error("canceled"));
|
||||
}
|
||||
|
||||
public getCompletionState(addr: string): InviteState {
|
||||
|
@ -174,10 +174,10 @@ export default class MultiInviter {
|
|||
// The error handling during the invitation process covers any API.
|
||||
// Some errors must to me mapped from profile API errors to more specific ones to avoid collisions.
|
||||
switch (err.errcode) {
|
||||
case 'M_FORBIDDEN':
|
||||
throw new MatrixError({ errcode: 'M_PROFILE_UNDISCLOSED' });
|
||||
case 'M_NOT_FOUND':
|
||||
throw new MatrixError({ errcode: 'M_USER_NOT_FOUND' });
|
||||
case "M_FORBIDDEN":
|
||||
throw new MatrixError({ errcode: "M_PROFILE_UNDISCLOSED" });
|
||||
case "M_NOT_FOUND":
|
||||
throw new MatrixError({ errcode: "M_USER_NOT_FOUND" });
|
||||
default:
|
||||
throw err;
|
||||
}
|
||||
|
@ -186,7 +186,7 @@ export default class MultiInviter {
|
|||
|
||||
return this.matrixClient.invite(roomId, addr, this.reason);
|
||||
} else {
|
||||
throw new Error('Unsupported address');
|
||||
throw new Error("Unsupported address");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -195,99 +195,101 @@ export default class MultiInviter {
|
|||
logger.log(`Inviting ${address}`);
|
||||
|
||||
const doInvite = this.inviteToRoom(this.roomId, address, ignoreProfile);
|
||||
doInvite.then(() => {
|
||||
if (this.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.completionStates[address] = InviteState.Invited;
|
||||
delete this.errors[address];
|
||||
|
||||
resolve();
|
||||
this.progressCallback?.();
|
||||
}).catch((err) => {
|
||||
if (this.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error(err);
|
||||
|
||||
const isSpace = this.roomId && this.matrixClient.getRoom(this.roomId)?.isSpaceRoom();
|
||||
|
||||
let errorText: string;
|
||||
let fatal = false;
|
||||
switch (err.errcode) {
|
||||
case "M_FORBIDDEN":
|
||||
if (isSpace) {
|
||||
errorText = _t('You do not have permission to invite people to this space.');
|
||||
} else {
|
||||
errorText = _t('You do not have permission to invite people to this room.');
|
||||
}
|
||||
fatal = true;
|
||||
break;
|
||||
case USER_ALREADY_INVITED:
|
||||
if (isSpace) {
|
||||
errorText = _t("User is already invited to the space");
|
||||
} else {
|
||||
errorText = _t("User is already invited to the room");
|
||||
}
|
||||
break;
|
||||
case USER_ALREADY_JOINED:
|
||||
if (isSpace) {
|
||||
errorText = _t("User is already in the space");
|
||||
} else {
|
||||
errorText = _t("User is already in the room");
|
||||
}
|
||||
break;
|
||||
case "M_LIMIT_EXCEEDED":
|
||||
// we're being throttled so wait a bit & try again
|
||||
window.setTimeout(() => {
|
||||
this.doInvite(address, ignoreProfile).then(resolve, reject);
|
||||
}, 5000);
|
||||
doInvite
|
||||
.then(() => {
|
||||
if (this.canceled) {
|
||||
return;
|
||||
case "M_NOT_FOUND":
|
||||
case "M_USER_NOT_FOUND":
|
||||
errorText = _t("User does not exist");
|
||||
break;
|
||||
case "M_PROFILE_UNDISCLOSED":
|
||||
errorText = _t("User may or may not exist");
|
||||
break;
|
||||
case "M_PROFILE_NOT_FOUND":
|
||||
if (!ignoreProfile) {
|
||||
// Invite without the profile check
|
||||
logger.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":
|
||||
if (isSpace) {
|
||||
errorText = _t("The user's homeserver does not support the version of the space.");
|
||||
} else {
|
||||
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.Invited;
|
||||
delete this.errors[address];
|
||||
|
||||
this.completionStates[address] = InviteState.Error;
|
||||
this.errors[address] = { errorText, errcode: err.errcode };
|
||||
|
||||
this.busy = !fatal;
|
||||
this._fatal = fatal;
|
||||
|
||||
if (fatal) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
this.progressCallback?.();
|
||||
})
|
||||
.catch((err) => {
|
||||
if (this.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error(err);
|
||||
|
||||
const isSpace = this.roomId && this.matrixClient.getRoom(this.roomId)?.isSpaceRoom();
|
||||
|
||||
let errorText: string;
|
||||
let fatal = false;
|
||||
switch (err.errcode) {
|
||||
case "M_FORBIDDEN":
|
||||
if (isSpace) {
|
||||
errorText = _t("You do not have permission to invite people to this space.");
|
||||
} else {
|
||||
errorText = _t("You do not have permission to invite people to this room.");
|
||||
}
|
||||
fatal = true;
|
||||
break;
|
||||
case USER_ALREADY_INVITED:
|
||||
if (isSpace) {
|
||||
errorText = _t("User is already invited to the space");
|
||||
} else {
|
||||
errorText = _t("User is already invited to the room");
|
||||
}
|
||||
break;
|
||||
case USER_ALREADY_JOINED:
|
||||
if (isSpace) {
|
||||
errorText = _t("User is already in the space");
|
||||
} else {
|
||||
errorText = _t("User is already in the room");
|
||||
}
|
||||
break;
|
||||
case "M_LIMIT_EXCEEDED":
|
||||
// we're being throttled so wait a bit & try again
|
||||
window.setTimeout(() => {
|
||||
this.doInvite(address, ignoreProfile).then(resolve, reject);
|
||||
}, 5000);
|
||||
return;
|
||||
case "M_NOT_FOUND":
|
||||
case "M_USER_NOT_FOUND":
|
||||
errorText = _t("User does not exist");
|
||||
break;
|
||||
case "M_PROFILE_UNDISCLOSED":
|
||||
errorText = _t("User may or may not exist");
|
||||
break;
|
||||
case "M_PROFILE_NOT_FOUND":
|
||||
if (!ignoreProfile) {
|
||||
// Invite without the profile check
|
||||
logger.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":
|
||||
if (isSpace) {
|
||||
errorText = _t("The user's homeserver does not support the version of the space.");
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -301,12 +303,13 @@ export default class MultiInviter {
|
|||
if (Object.keys(this.errors).length > 0) {
|
||||
// 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));
|
||||
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));
|
||||
const promises = unknownProfileUsers.map((u) => this.doInvite(u, true));
|
||||
Promise.all(promises).then(() => this.deferred.resolve(this.completionStates));
|
||||
};
|
||||
|
||||
|
@ -317,7 +320,7 @@ export default class MultiInviter {
|
|||
|
||||
logger.log("Showing failed to invite dialog...");
|
||||
Modal.createDialog(AskInviteAnywayDialog, {
|
||||
unknownProfileUsers: unknownProfileUsers.map(u => ({
|
||||
unknownProfileUsers: unknownProfileUsers.map((u) => ({
|
||||
userId: u,
|
||||
errorText: this.errors[u].errorText,
|
||||
})),
|
||||
|
@ -354,8 +357,10 @@ export default class MultiInviter {
|
|||
return;
|
||||
}
|
||||
|
||||
this.doInvite(addr, ignoreProfile).then(() => {
|
||||
this.inviteMore(nextIndex + 1, ignoreProfile);
|
||||
}).catch(() => this.deferred.resolve(this.completionStates));
|
||||
this.doInvite(addr, ignoreProfile)
|
||||
.then(() => {
|
||||
this.inviteMore(nextIndex + 1, ignoreProfile);
|
||||
})
|
||||
.catch(() => this.deferred.resolve(this.completionStates));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,8 @@ import React from "react";
|
|||
|
||||
// Wrap DOM event handlers with stopPropagation and preventDefault
|
||||
export const preventDefaultWrapper =
|
||||
<T extends React.BaseSyntheticEvent = React.BaseSyntheticEvent>(callback: () => void) => (e?: T) => {
|
||||
<T extends React.BaseSyntheticEvent = React.BaseSyntheticEvent>(callback: () => void) =>
|
||||
(e?: T) => {
|
||||
e?.stopPropagation();
|
||||
e?.preventDefault();
|
||||
callback();
|
||||
|
|
|
@ -14,15 +14,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import zxcvbn, { ZXCVBNFeedbackWarning } from 'zxcvbn';
|
||||
import zxcvbn, { ZXCVBNFeedbackWarning } from "zxcvbn";
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import { _t, _td } from '../languageHandler';
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { _t, _td } from "../languageHandler";
|
||||
|
||||
const ZXCVBN_USER_INPUTS = [
|
||||
'riot',
|
||||
'matrix',
|
||||
];
|
||||
const ZXCVBN_USER_INPUTS = ["riot", "matrix"];
|
||||
|
||||
// Translations for zxcvbn's suggestion strings
|
||||
_td("Use a few words, avoid common phrases");
|
||||
|
@ -40,8 +37,8 @@ _td("Predictable substitutions like '@' instead of 'a' don't help very much");
|
|||
_td("Add another word or two. Uncommon words are better.");
|
||||
|
||||
// and warnings
|
||||
_td("Repeats like \"aaa\" are easy to guess");
|
||||
_td("Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"");
|
||||
_td('Repeats like "aaa" are easy to guess');
|
||||
_td('Repeats like "abcabcabc" are only slightly harder to guess than "abc"');
|
||||
_td("Sequences like abc or 6543 are easy to guess");
|
||||
_td("Recent years are easy to guess");
|
||||
_td("Dates are often easy to guess");
|
||||
|
@ -73,8 +70,8 @@ export function scorePassword(password: string) {
|
|||
|
||||
let zxcvbnResult = zxcvbn(password, userInputs);
|
||||
// Work around https://github.com/dropbox/zxcvbn/issues/216
|
||||
if (password.includes(' ')) {
|
||||
const resultNoSpaces = zxcvbn(password.replace(/ /g, ''), userInputs);
|
||||
if (password.includes(" ")) {
|
||||
const resultNoSpaces = zxcvbn(password.replace(/ /g, ""), userInputs);
|
||||
if (resultNoSpaces.score < zxcvbnResult.score) zxcvbnResult = resultNoSpaces;
|
||||
}
|
||||
|
||||
|
|
|
@ -52,4 +52,3 @@ export function doesRoomVersionSupport(roomVer: string, featureVer: string): boo
|
|||
// from a mile away and can course-correct this function if needed.
|
||||
return Number(roomVer) >= Number(featureVer);
|
||||
}
|
||||
|
||||
|
|
|
@ -25,9 +25,7 @@ import React from "react";
|
|||
export function jsxJoin(array: Array<string | JSX.Element>, joiner?: string | JSX.Element): JSX.Element {
|
||||
const newArray = [];
|
||||
array.forEach((element, index) => {
|
||||
newArray.push(element, (index === array.length - 1) ? null : joiner);
|
||||
newArray.push(element, index === array.length - 1 ? null : joiner);
|
||||
});
|
||||
return (
|
||||
<span>{ newArray }</span>
|
||||
);
|
||||
return <span>{newArray}</span>;
|
||||
}
|
||||
|
|
|
@ -36,11 +36,11 @@ export function getParentEventId(ev?: MatrixEvent): string | undefined {
|
|||
// Part of Replies fallback support
|
||||
export function stripPlainReply(body: string): string {
|
||||
// Removes lines beginning with `> ` until you reach one that doesn't.
|
||||
const lines = body.split('\n');
|
||||
while (lines.length && lines[0].startsWith('> ')) lines.shift();
|
||||
const lines = body.split("\n");
|
||||
while (lines.length && lines[0].startsWith("> ")) lines.shift();
|
||||
// Reply fallback has a blank line after it, so remove it to prevent leading newline
|
||||
if (lines[0] === '') lines.shift();
|
||||
return lines.join('\n');
|
||||
if (lines[0] === "") lines.shift();
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// Part of Replies fallback support
|
||||
|
@ -52,24 +52,21 @@ export function stripHTMLReply(html: string): string {
|
|||
// anyways. However, we sanitize to 1) remove any mx-reply, so that we
|
||||
// don't generate a nested mx-reply, and 2) make sure that the HTML is
|
||||
// properly formatted (e.g. tags are closed where necessary)
|
||||
return sanitizeHtml(
|
||||
html,
|
||||
{
|
||||
allowedTags: false, // false means allow everything
|
||||
allowedAttributes: false,
|
||||
// we somehow can't allow all schemes, so we allow all that we
|
||||
// know of and mxc (for img tags)
|
||||
allowedSchemes: [...PERMITTED_URL_SCHEMES, 'mxc'],
|
||||
exclusiveFilter: (frame) => frame.tag === "mx-reply",
|
||||
},
|
||||
);
|
||||
return sanitizeHtml(html, {
|
||||
allowedTags: false, // false means allow everything
|
||||
allowedAttributes: false,
|
||||
// we somehow can't allow all schemes, so we allow all that we
|
||||
// know of and mxc (for img tags)
|
||||
allowedSchemes: [...PERMITTED_URL_SCHEMES, "mxc"],
|
||||
exclusiveFilter: (frame) => frame.tag === "mx-reply",
|
||||
});
|
||||
}
|
||||
|
||||
// Part of Replies fallback support
|
||||
export function getNestedReplyText(
|
||||
ev: MatrixEvent,
|
||||
permalinkCreator: RoomPermalinkCreator,
|
||||
): { body: string, html: string } | null {
|
||||
): { body: string; html: string } | null {
|
||||
if (!ev) return null;
|
||||
|
||||
let { body, formatted_body: html, msgtype } = ev.getContent();
|
||||
|
@ -86,7 +83,7 @@ export function getNestedReplyText(
|
|||
// Escape the body to use as HTML below.
|
||||
// We also run a nl2br over the result to fix the fallback representation. We do this
|
||||
// after converting the text to safe HTML to avoid user-provided BR's from being converted.
|
||||
html = escapeHtml(body).replace(/\n/g, '<br/>');
|
||||
html = escapeHtml(body).replace(/\n/g, "<br/>");
|
||||
}
|
||||
|
||||
// dev note: do not rely on `body` being safe for HTML usage below.
|
||||
|
@ -98,8 +95,9 @@ export function getNestedReplyText(
|
|||
if (M_BEACON_INFO.matches(ev.getType())) {
|
||||
const aTheir = isSelfLocation(ev.getContent()) ? "their" : "a";
|
||||
return {
|
||||
html: `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
|
||||
+ `<br>shared ${aTheir} live location.</blockquote></mx-reply>`,
|
||||
html:
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` +
|
||||
`<br>shared ${aTheir} live location.</blockquote></mx-reply>`,
|
||||
body: `> <${mxid}> shared ${aTheir} live location.\n\n`,
|
||||
};
|
||||
}
|
||||
|
@ -108,49 +106,56 @@ export function getNestedReplyText(
|
|||
switch (msgtype) {
|
||||
case MsgType.Text:
|
||||
case MsgType.Notice: {
|
||||
html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
|
||||
+ `<br>${html}</blockquote></mx-reply>`;
|
||||
const lines = body.trim().split('\n');
|
||||
html =
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` +
|
||||
`<br>${html}</blockquote></mx-reply>`;
|
||||
const lines = body.trim().split("\n");
|
||||
if (lines.length > 0) {
|
||||
lines[0] = `<${mxid}> ${lines[0]}`;
|
||||
body = lines.map((line) => `> ${line}`).join('\n') + '\n\n';
|
||||
body = lines.map((line) => `> ${line}`).join("\n") + "\n\n";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MsgType.Image:
|
||||
html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
|
||||
+ `<br>sent an image.</blockquote></mx-reply>`;
|
||||
html =
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` +
|
||||
`<br>sent an image.</blockquote></mx-reply>`;
|
||||
body = `> <${mxid}> sent an image.\n\n`;
|
||||
break;
|
||||
case MsgType.Video:
|
||||
html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
|
||||
+ `<br>sent a video.</blockquote></mx-reply>`;
|
||||
html =
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` +
|
||||
`<br>sent a video.</blockquote></mx-reply>`;
|
||||
body = `> <${mxid}> sent a video.\n\n`;
|
||||
break;
|
||||
case MsgType.Audio:
|
||||
html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
|
||||
+ `<br>sent an audio file.</blockquote></mx-reply>`;
|
||||
html =
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` +
|
||||
`<br>sent an audio file.</blockquote></mx-reply>`;
|
||||
body = `> <${mxid}> sent an audio file.\n\n`;
|
||||
break;
|
||||
case MsgType.File:
|
||||
html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
|
||||
+ `<br>sent a file.</blockquote></mx-reply>`;
|
||||
html =
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` +
|
||||
`<br>sent a file.</blockquote></mx-reply>`;
|
||||
body = `> <${mxid}> sent a file.\n\n`;
|
||||
break;
|
||||
case MsgType.Location: {
|
||||
const aTheir = isSelfLocation(ev.getContent()) ? "their" : "a";
|
||||
html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
|
||||
+ `<br>shared ${aTheir} location.</blockquote></mx-reply>`;
|
||||
html =
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` +
|
||||
`<br>shared ${aTheir} location.</blockquote></mx-reply>`;
|
||||
body = `> <${mxid}> shared ${aTheir} location.\n\n`;
|
||||
break;
|
||||
}
|
||||
case MsgType.Emote: {
|
||||
html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> * `
|
||||
+ `<a href="${userLink}">${mxid}</a><br>${html}</blockquote></mx-reply>`;
|
||||
const lines = body.trim().split('\n');
|
||||
html =
|
||||
`<mx-reply><blockquote><a href="${evLink}">In reply to</a> * ` +
|
||||
`<a href="${userLink}">${mxid}</a><br>${html}</blockquote></mx-reply>`;
|
||||
const lines = body.trim().split("\n");
|
||||
if (lines.length > 0) {
|
||||
lines[0] = `* <${mxid}> ${lines[0]}`;
|
||||
body = lines.map((line) => `> ${line}`).join('\n') + '\n\n';
|
||||
body = lines.map((line) => `> ${line}`).join("\n") + "\n\n";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -165,8 +170,8 @@ export function makeReplyMixIn(ev?: MatrixEvent): IEventRelation {
|
|||
if (!ev) return {};
|
||||
|
||||
const mixin: IEventRelation = {
|
||||
'm.in_reply_to': {
|
||||
'event_id': ev.getId(),
|
||||
"m.in_reply_to": {
|
||||
event_id: ev.getId(),
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -197,7 +202,8 @@ export function shouldDisplayReply(event: MatrixEvent): boolean {
|
|||
}
|
||||
|
||||
const relation = event.getRelation();
|
||||
if (SettingsStore.getValue("feature_thread") &&
|
||||
if (
|
||||
SettingsStore.getValue("feature_thread") &&
|
||||
relation?.rel_type === THREAD_RELATION_TYPE.name &&
|
||||
relation?.is_falling_back
|
||||
) {
|
||||
|
|
|
@ -76,4 +76,3 @@ export default class ResizeNotifier extends EventEmitter {
|
|||
this.updateMiddlePanel();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ export async function awaitRoomDownSync(cli: MatrixClient, roomId: string): Prom
|
|||
const room = cli.getRoom(roomId);
|
||||
if (room) return room; // already have the room
|
||||
|
||||
return new Promise<Room>(resolve => {
|
||||
return new Promise<Room>((resolve) => {
|
||||
// We have to wait for the js-sdk to give us the room back so
|
||||
// we can more effectively abuse the MultiInviter behaviour
|
||||
// which heavily relies on the Room object being available.
|
||||
|
@ -69,22 +69,21 @@ export async function upgradeRoom(
|
|||
|
||||
let toInvite: string[] = [];
|
||||
if (inviteUsers) {
|
||||
toInvite = [
|
||||
...room.getMembersWithMembership("join"),
|
||||
...room.getMembersWithMembership("invite"),
|
||||
].map(m => m.userId).filter(m => m !== cli.getUserId());
|
||||
toInvite = [...room.getMembersWithMembership("join"), ...room.getMembersWithMembership("invite")]
|
||||
.map((m) => m.userId)
|
||||
.filter((m) => m !== cli.getUserId());
|
||||
}
|
||||
|
||||
let parentsToRelink: Room[] = [];
|
||||
if (updateSpaces) {
|
||||
parentsToRelink = Array.from(SpaceStore.instance.getKnownParents(room.roomId))
|
||||
.map(roomId => cli.getRoom(roomId))
|
||||
.filter(parent => parent?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()));
|
||||
.map((roomId) => cli.getRoom(roomId))
|
||||
.filter((parent) => parent?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()));
|
||||
}
|
||||
|
||||
const progress: IProgress = {
|
||||
roomUpgraded: false,
|
||||
roomSynced: (awaitRoom || inviteUsers) ? false : undefined,
|
||||
roomSynced: awaitRoom || inviteUsers ? false : undefined,
|
||||
inviteUsersProgress: inviteUsers ? 0 : undefined,
|
||||
inviteUsersTotal: toInvite.length,
|
||||
updateSpacesProgress: updateSpaces ? 0 : undefined,
|
||||
|
@ -100,8 +99,8 @@ export async function upgradeRoom(
|
|||
logger.error(e);
|
||||
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t('Error upgrading room'),
|
||||
description: _t('Double check that your server supports the room version chosen and try again.'),
|
||||
title: _t("Error upgrading room"),
|
||||
description: _t("Double check that your server supports the room version chosen and try again."),
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
|
@ -127,10 +126,15 @@ export async function upgradeRoom(
|
|||
try {
|
||||
for (const parent of parentsToRelink) {
|
||||
const currentEv = parent.currentState.getStateEvents(EventType.SpaceChild, room.roomId);
|
||||
await cli.sendStateEvent(parent.roomId, EventType.SpaceChild, {
|
||||
...(currentEv?.getContent() || {}), // copy existing attributes like suggested
|
||||
via: [cli.getDomain()],
|
||||
}, newRoomId);
|
||||
await cli.sendStateEvent(
|
||||
parent.roomId,
|
||||
EventType.SpaceChild,
|
||||
{
|
||||
...(currentEv?.getContent() || {}), // copy existing attributes like suggested
|
||||
via: [cli.getDomain()],
|
||||
},
|
||||
newRoomId,
|
||||
);
|
||||
await cli.sendStateEvent(parent.roomId, EventType.SpaceChild, {}, room.roomId);
|
||||
|
||||
progress.updateSpacesProgress++;
|
||||
|
|
|
@ -17,12 +17,12 @@ 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';
|
||||
import DMRoomMap from "./DMRoomMap";
|
||||
|
||||
export enum E2EStatus {
|
||||
Warning = "warning",
|
||||
Verified = "verified",
|
||||
Normal = "normal"
|
||||
Normal = "normal",
|
||||
}
|
||||
|
||||
export async function shieldStatusForRoom(client: MatrixClient, room: Room): Promise<E2EStatus> {
|
||||
|
@ -31,10 +31,10 @@ export async function shieldStatusForRoom(client: MatrixClient, room: Room): Pro
|
|||
|
||||
const verified: string[] = [];
|
||||
const unverified: string[] = [];
|
||||
members.filter((userId) => userId !== client.getUserId())
|
||||
members
|
||||
.filter((userId) => userId !== client.getUserId())
|
||||
.forEach((userId) => {
|
||||
(client.checkUserTrust(userId).isCrossSigningVerified() ?
|
||||
verified : unverified).push(userId);
|
||||
(client.checkUserTrust(userId).isCrossSigningVerified() ? verified : unverified).push(userId);
|
||||
});
|
||||
|
||||
/* Alarm if any unverified users were verified before. */
|
||||
|
@ -46,10 +46,11 @@ export async function shieldStatusForRoom(client: MatrixClient, room: Room): Pro
|
|||
|
||||
/* Check all verified user devices. */
|
||||
/* Don't alarm if no other users are verified */
|
||||
const includeUser = (verified.length > 0) && // Don't alarm for self in rooms where nobody else is verified
|
||||
!inDMMap && // Don't alarm for self in DMs with other users
|
||||
(members.length !== 2) || // Don't alarm for self in 1:1 chats with other users
|
||||
(members.length === 1); // Do alarm for self if we're alone in a room
|
||||
const includeUser =
|
||||
(verified.length > 0 && // Don't alarm for self in rooms where nobody else is verified
|
||||
!inDMMap && // Don't alarm for self in DMs with other users
|
||||
members.length !== 2) || // Don't alarm for self in 1:1 chats with other users
|
||||
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 = client.getStoredDevicesForUser(userId);
|
||||
|
|
|
@ -41,8 +41,7 @@ const keyMap = new EnhancedMap<Object, EnhancedMap<string, unknown>>();
|
|||
* variables to strings to essentially namespace the field, for most cases.
|
||||
*/
|
||||
export class Singleflight {
|
||||
private constructor() {
|
||||
}
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* A void marker to help with returning a value in a singleflight context.
|
||||
|
@ -80,8 +79,7 @@ export class Singleflight {
|
|||
}
|
||||
|
||||
class SingleflightContext {
|
||||
public constructor(private instance: Object, private key: string) {
|
||||
}
|
||||
public constructor(private instance: Object, private key: string) {}
|
||||
|
||||
/**
|
||||
* Forget this particular instance and key combination, discarding the result.
|
||||
|
|
|
@ -15,12 +15,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
export function snakeToCamel(s: string): string {
|
||||
return s.replace(/._./g, v => `${v[0]}${v[2].toUpperCase()}`);
|
||||
return s.replace(/._./g, (v) => `${v[0]}${v[2].toUpperCase()}`);
|
||||
}
|
||||
|
||||
export class SnakedObject<T = Record<string, any>> {
|
||||
public constructor(private obj: T) {
|
||||
}
|
||||
public constructor(private obj: T) {}
|
||||
|
||||
public get<K extends string & keyof T>(key: K, altCaseName?: string): T[K] {
|
||||
const val = this.obj[key];
|
||||
|
|
|
@ -21,36 +21,38 @@ import { compare } from "matrix-js-sdk/src/utils";
|
|||
import { Member } from "./direct-messages";
|
||||
import DMRoomMap from "./DMRoomMap";
|
||||
|
||||
export const compareMembers = (
|
||||
activityScores: Record<string, IActivityScore>,
|
||||
memberScores: Record<string, IMemberScore>,
|
||||
) => (a: Member | RoomMember, b: Member | RoomMember): number => {
|
||||
const aActivityScore = activityScores[a.userId]?.score ?? 0;
|
||||
const aMemberScore = memberScores[a.userId]?.score ?? 0;
|
||||
const aScore = aActivityScore + aMemberScore;
|
||||
const aNumRooms = memberScores[a.userId]?.numRooms ?? 0;
|
||||
export const compareMembers =
|
||||
(activityScores: Record<string, IActivityScore>, memberScores: Record<string, IMemberScore>) =>
|
||||
(a: Member | RoomMember, b: Member | RoomMember): number => {
|
||||
const aActivityScore = activityScores[a.userId]?.score ?? 0;
|
||||
const aMemberScore = memberScores[a.userId]?.score ?? 0;
|
||||
const aScore = aActivityScore + aMemberScore;
|
||||
const aNumRooms = memberScores[a.userId]?.numRooms ?? 0;
|
||||
|
||||
const bActivityScore = activityScores[b.userId]?.score ?? 0;
|
||||
const bMemberScore = memberScores[b.userId]?.score ?? 0;
|
||||
const bScore = bActivityScore + bMemberScore;
|
||||
const bNumRooms = memberScores[b.userId]?.numRooms ?? 0;
|
||||
const bActivityScore = activityScores[b.userId]?.score ?? 0;
|
||||
const bMemberScore = memberScores[b.userId]?.score ?? 0;
|
||||
const bScore = bActivityScore + bMemberScore;
|
||||
const bNumRooms = memberScores[b.userId]?.numRooms ?? 0;
|
||||
|
||||
if (aScore === bScore) {
|
||||
if (aNumRooms === bNumRooms) {
|
||||
return compare(a.userId, b.userId);
|
||||
if (aScore === bScore) {
|
||||
if (aNumRooms === bNumRooms) {
|
||||
return compare(a.userId, b.userId);
|
||||
}
|
||||
|
||||
return bNumRooms - aNumRooms;
|
||||
}
|
||||
|
||||
return bNumRooms - aNumRooms;
|
||||
}
|
||||
return bScore - aScore;
|
||||
};
|
||||
return bScore - aScore;
|
||||
};
|
||||
|
||||
function joinedRooms(cli: MatrixClient): Room[] {
|
||||
return cli.getRooms()
|
||||
.filter(r => r.getMyMembership() === 'join')
|
||||
// Skip low priority rooms and DMs
|
||||
.filter(r => !DMRoomMap.shared().getUserIdForRoomId(r.roomId))
|
||||
.filter(r => !Object.keys(r.tags).includes("m.lowpriority"));
|
||||
return (
|
||||
cli
|
||||
.getRooms()
|
||||
.filter((r) => r.getMyMembership() === "join")
|
||||
// Skip low priority rooms and DMs
|
||||
.filter((r) => !DMRoomMap.shared().getUserIdForRoomId(r.roomId))
|
||||
.filter((r) => !Object.keys(r.tags).includes("m.lowpriority"))
|
||||
);
|
||||
}
|
||||
|
||||
interface IActivityScore {
|
||||
|
@ -64,16 +66,16 @@ interface IActivityScore {
|
|||
// which are closer to "continue this conversation" rather than "this person exists".
|
||||
export function buildActivityScores(cli: MatrixClient): { [key: string]: IActivityScore } {
|
||||
const now = new Date().getTime();
|
||||
const earliestAgeConsidered = now - (60 * 60 * 1000); // 1 hour ago
|
||||
const earliestAgeConsidered = now - 60 * 60 * 1000; // 1 hour ago
|
||||
const maxMessagesConsidered = 50; // so we don't iterate over a huge amount of traffic
|
||||
const events = joinedRooms(cli)
|
||||
.flatMap(room => takeRight(room.getLiveTimeline().getEvents(), maxMessagesConsidered))
|
||||
.filter(ev => ev.getTs() > earliestAgeConsidered);
|
||||
const senderEvents = groupBy(events, ev => ev.getSender());
|
||||
return mapValues(senderEvents, events => {
|
||||
const lastEvent = maxBy(events, ev => ev.getTs());
|
||||
.flatMap((room) => takeRight(room.getLiveTimeline().getEvents(), maxMessagesConsidered))
|
||||
.filter((ev) => ev.getTs() > earliestAgeConsidered);
|
||||
const senderEvents = groupBy(events, (ev) => ev.getSender());
|
||||
return mapValues(senderEvents, (events) => {
|
||||
const lastEvent = maxBy(events, (ev) => ev.getTs());
|
||||
const distanceFromNow = Math.abs(now - lastEvent.getTs()); // abs to account for slight future messages
|
||||
const inverseTime = (now - earliestAgeConsidered) - distanceFromNow;
|
||||
const inverseTime = now - earliestAgeConsidered - distanceFromNow;
|
||||
return {
|
||||
lastSpoke: lastEvent.getTs(),
|
||||
// Scores from being in a room give a 'good' score of about 1.0-1.5, so for our
|
||||
|
@ -92,19 +94,18 @@ interface IMemberScore {
|
|||
|
||||
export function buildMemberScores(cli: MatrixClient): { [key: string]: IMemberScore } {
|
||||
const maxConsideredMembers = 200;
|
||||
const consideredRooms = joinedRooms(cli).filter(room => room.getJoinedMemberCount() < maxConsideredMembers);
|
||||
const memberPeerEntries = consideredRooms
|
||||
.flatMap(room =>
|
||||
room.getJoinedMembers().map(member =>
|
||||
({ member, roomSize: room.getJoinedMemberCount() })));
|
||||
const consideredRooms = joinedRooms(cli).filter((room) => room.getJoinedMemberCount() < maxConsideredMembers);
|
||||
const memberPeerEntries = consideredRooms.flatMap((room) =>
|
||||
room.getJoinedMembers().map((member) => ({ member, roomSize: room.getJoinedMemberCount() })),
|
||||
);
|
||||
const userMeta = groupBy(memberPeerEntries, ({ member }) => member.userId);
|
||||
return mapValues(userMeta, roomMemberships => {
|
||||
return mapValues(userMeta, (roomMemberships) => {
|
||||
const maximumPeers = maxConsideredMembers * roomMemberships.length;
|
||||
const totalPeers = sumBy(roomMemberships, entry => entry.roomSize);
|
||||
const totalPeers = sumBy(roomMemberships, (entry) => entry.roomSize);
|
||||
return {
|
||||
member: minBy(roomMemberships, entry => entry.roomSize).member,
|
||||
member: minBy(roomMemberships, (entry) => entry.roomSize).member,
|
||||
numRooms: roomMemberships.length,
|
||||
score: Math.max(0, Math.pow(1 - (totalPeers / maximumPeers), 5)),
|
||||
score: Math.max(0, Math.pow(1 - totalPeers / maximumPeers, 5)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
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 { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb";
|
||||
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
@ -42,10 +42,11 @@ function error(msg: string, ...args: string[]) {
|
|||
|
||||
export function tryPersistStorage() {
|
||||
if (navigator.storage && navigator.storage.persist) {
|
||||
navigator.storage.persist().then(persistent => {
|
||||
navigator.storage.persist().then((persistent) => {
|
||||
logger.log("StorageManager: Persistent?", persistent);
|
||||
});
|
||||
} else if (document.requestStorageAccess) { // Safari
|
||||
} else if (document.requestStorageAccess) {
|
||||
// Safari
|
||||
document.requestStorageAccess().then(
|
||||
() => logger.log("StorageManager: Persistent?", true),
|
||||
() => logger.log("StorageManager: Persistent?", false),
|
||||
|
@ -101,8 +102,8 @@ export async function checkConsistency() {
|
|||
healthy = false;
|
||||
error(
|
||||
"Data exists in local storage and crypto is marked as initialised " +
|
||||
" but no data found in crypto store. " +
|
||||
"IndexedDB storage has likely been evicted by the browser!",
|
||||
" but no data found in crypto store. " +
|
||||
"IndexedDB storage has likely been evicted by the browser!",
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -123,9 +124,7 @@ export async function checkConsistency() {
|
|||
async function checkSyncStore() {
|
||||
let exists = false;
|
||||
try {
|
||||
exists = await IndexedDBStore.exists(
|
||||
indexedDB, SYNC_STORE_NAME,
|
||||
);
|
||||
exists = await IndexedDBStore.exists(indexedDB, SYNC_STORE_NAME);
|
||||
log(`Sync store using IndexedDB contains data? ${exists}`);
|
||||
return { exists, healthy: true };
|
||||
} catch (e) {
|
||||
|
@ -138,9 +137,7 @@ async function checkSyncStore() {
|
|||
async function checkCryptoStore() {
|
||||
let exists = false;
|
||||
try {
|
||||
exists = await IndexedDBCryptoStore.exists(
|
||||
indexedDB, CRYPTO_STORE_NAME,
|
||||
);
|
||||
exists = await IndexedDBCryptoStore.exists(indexedDB, CRYPTO_STORE_NAME);
|
||||
log(`Crypto store using IndexedDB contains data? ${exists}`);
|
||||
return { exists, healthy: true };
|
||||
} catch (e) {
|
||||
|
@ -183,7 +180,9 @@ async function idbInit(): Promise<void> {
|
|||
idb = await new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open("matrix-react-sdk", 1);
|
||||
request.onerror = reject;
|
||||
request.onsuccess = () => { resolve(request.result); };
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result);
|
||||
};
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
db.createObjectStore("pickleKey");
|
||||
|
@ -192,10 +191,7 @@ async function idbInit(): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
export async function idbLoad(
|
||||
table: string,
|
||||
key: string | string[],
|
||||
): Promise<any> {
|
||||
export async function idbLoad(table: string, key: string | string[]): Promise<any> {
|
||||
if (!idb) {
|
||||
await idbInit();
|
||||
}
|
||||
|
@ -206,15 +202,13 @@ export async function idbLoad(
|
|||
const objectStore = txn.objectStore(table);
|
||||
const request = objectStore.get(key);
|
||||
request.onerror = reject;
|
||||
request.onsuccess = (event) => { resolve(request.result); };
|
||||
request.onsuccess = (event) => {
|
||||
resolve(request.result);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function idbSave(
|
||||
table: string,
|
||||
key: string | string[],
|
||||
data: any,
|
||||
): Promise<void> {
|
||||
export async function idbSave(table: string, key: string | string[], data: any): Promise<void> {
|
||||
if (!idb) {
|
||||
await idbInit();
|
||||
}
|
||||
|
@ -225,14 +219,13 @@ export async function idbSave(
|
|||
const objectStore = txn.objectStore(table);
|
||||
const request = objectStore.put(data, key);
|
||||
request.onerror = reject;
|
||||
request.onsuccess = (event) => { resolve(); };
|
||||
request.onsuccess = (event) => {
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function idbDelete(
|
||||
table: string,
|
||||
key: string | string[],
|
||||
): Promise<void> {
|
||||
export async function idbDelete(table: string, key: string | string[]): Promise<void> {
|
||||
if (!idb) {
|
||||
await idbInit();
|
||||
}
|
||||
|
@ -243,6 +236,8 @@ export async function idbDelete(
|
|||
const objectStore = txn.objectStore(table);
|
||||
const request = objectStore.delete(key);
|
||||
request.onerror = reject;
|
||||
request.onsuccess = () => { resolve(); };
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -23,13 +23,13 @@ import url from "url";
|
|||
* @returns {string} The abbreviated url
|
||||
*/
|
||||
export function abbreviateUrl(u: string): string {
|
||||
if (!u) return '';
|
||||
if (!u) return "";
|
||||
|
||||
const parsedUrl = url.parse(u);
|
||||
// if it's something we can't parse as a url then just return it
|
||||
if (!parsedUrl) return u;
|
||||
|
||||
if (parsedUrl.path === '/') {
|
||||
if (parsedUrl.path === "/") {
|
||||
// we ignore query / hash parts: these aren't relevant for IS server URLs
|
||||
return parsedUrl.host;
|
||||
}
|
||||
|
@ -38,10 +38,10 @@ export function abbreviateUrl(u: string): string {
|
|||
}
|
||||
|
||||
export function unabbreviateUrl(u: string): string {
|
||||
if (!u) return '';
|
||||
if (!u) return "";
|
||||
|
||||
let longUrl = u;
|
||||
if (!u.startsWith('https://')) longUrl = 'https://' + u;
|
||||
if (!u.startsWith("https://")) longUrl = "https://" + u;
|
||||
const parsed = url.parse(longUrl);
|
||||
if (parsed.hostname === null) return u;
|
||||
|
||||
|
|
|
@ -25,13 +25,13 @@ type FunctionWithUIA<R, A> = (auth?: IAuthData, ...args: A[]) => Promise<UIAResp
|
|||
export function wrapRequestWithDialog<R, A = any>(
|
||||
requestFunction: FunctionWithUIA<R, A>,
|
||||
opts: Omit<InteractiveAuthDialogProps, "makeRequest" | "onFinished">,
|
||||
): ((...args: A[]) => Promise<R>) {
|
||||
return async function(...args): Promise<R> {
|
||||
): (...args: A[]) => Promise<R> {
|
||||
return async function (...args): Promise<R> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const boundFunction = requestFunction.bind(opts.matrixClient) as FunctionWithUIA<R, A>;
|
||||
boundFunction(undefined, ...args)
|
||||
.then((res) => resolve(res as R))
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
if (error.httpStatus !== 401 || !error.data?.flows) {
|
||||
// doesn't look like an interactive-auth failure
|
||||
return reject(error);
|
||||
|
|
|
@ -14,16 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IClientWellKnown } from 'matrix-js-sdk/src/client';
|
||||
import { UnstableValue } from 'matrix-js-sdk/src/NamespacedValue';
|
||||
import { IClientWellKnown } from "matrix-js-sdk/src/client";
|
||||
import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue";
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
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";
|
||||
export const TILE_SERVER_WK_KEY = new UnstableValue(
|
||||
"m.tile_server", "org.matrix.msc3488.tile_server");
|
||||
export const TILE_SERVER_WK_KEY = new UnstableValue("m.tile_server", "org.matrix.msc3488.tile_server");
|
||||
const EMBEDDED_PAGES_WK_PROPERTY = "io.element.embedded_pages";
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
|
@ -66,23 +65,16 @@ export function getTileServerWellKnown(): ITileServerWellKnown | undefined {
|
|||
return tileServerFromWellKnown(MatrixClientPeg.get().getClientWellKnown());
|
||||
}
|
||||
|
||||
export function tileServerFromWellKnown(
|
||||
clientWellKnown?: IClientWellKnown | undefined,
|
||||
): ITileServerWellKnown {
|
||||
return (
|
||||
clientWellKnown?.[TILE_SERVER_WK_KEY.name] ??
|
||||
clientWellKnown?.[TILE_SERVER_WK_KEY.altName]
|
||||
);
|
||||
export function tileServerFromWellKnown(clientWellKnown?: IClientWellKnown | undefined): ITileServerWellKnown {
|
||||
return clientWellKnown?.[TILE_SERVER_WK_KEY.name] ?? clientWellKnown?.[TILE_SERVER_WK_KEY.altName];
|
||||
}
|
||||
|
||||
export function getEmbeddedPagesWellKnown(): IEmbeddedPagesWellKnown | undefined {
|
||||
return embeddedPagesFromWellKnown(MatrixClientPeg.get()?.getClientWellKnown());
|
||||
}
|
||||
|
||||
export function embeddedPagesFromWellKnown(
|
||||
clientWellKnown?: IClientWellKnown,
|
||||
): IEmbeddedPagesWellKnown {
|
||||
return (clientWellKnown?.[EMBEDDED_PAGES_WK_PROPERTY]);
|
||||
export function embeddedPagesFromWellKnown(clientWellKnown?: IClientWellKnown): IEmbeddedPagesWellKnown {
|
||||
return clientWellKnown?.[EMBEDDED_PAGES_WK_PROPERTY];
|
||||
}
|
||||
|
||||
export function isSecureBackupRequired(): boolean {
|
||||
|
@ -106,10 +98,7 @@ export function getSecureBackupSetupMethods(): SecureBackupSetupMethod[] {
|
|||
wellKnown["secure_backup_setup_methods"].includes(SecureBackupSetupMethod.Passphrase)
|
||||
)
|
||||
) {
|
||||
return [
|
||||
SecureBackupSetupMethod.Key,
|
||||
SecureBackupSetupMethod.Passphrase,
|
||||
];
|
||||
return [SecureBackupSetupMethod.Key, SecureBackupSetupMethod.Passphrase];
|
||||
}
|
||||
return wellKnown["secure_backup_setup_methods"];
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ export type WhenFn<T> = (w: Whenable<T>) => void;
|
|||
* the consumer needs to know *when* that happens.
|
||||
*/
|
||||
export abstract class Whenable<T> implements IDestroyable {
|
||||
private listeners: {condition: T | null, fn: WhenFn<T>}[] = [];
|
||||
private listeners: { condition: T | null; fn: WhenFn<T> }[] = [];
|
||||
|
||||
/**
|
||||
* Sets up a call to `fn` *when* the `condition` is met.
|
||||
|
|
|
@ -25,11 +25,11 @@ import { ClientEvent, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
|||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { randomString, randomLowercaseString, randomUppercaseString } from "matrix-js-sdk/src/randomstring";
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import PlatformPeg from '../PlatformPeg';
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import PlatformPeg from "../PlatformPeg";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import dis from '../dispatcher/dispatcher';
|
||||
import WidgetEchoStore from '../stores/WidgetEchoStore';
|
||||
import dis from "../dispatcher/dispatcher";
|
||||
import WidgetEchoStore from "../stores/WidgetEchoStore";
|
||||
import { IntegrationManagers } from "../integrations/IntegrationManagers";
|
||||
import { WidgetType } from "../widgets/WidgetType";
|
||||
import { Jitsi } from "../widgets/Jitsi";
|
||||
|
@ -59,13 +59,13 @@ export default class WidgetUtils {
|
|||
*/
|
||||
static canUserModifyWidgets(roomId: string): boolean {
|
||||
if (!roomId) {
|
||||
logger.warn('No room ID specified');
|
||||
logger.warn("No room ID specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
logger.warn('User must be be logged in');
|
||||
logger.warn("User must be be logged in");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -77,7 +77,7 @@ export default class WidgetUtils {
|
|||
|
||||
const me = client.credentials.userId;
|
||||
if (!me) {
|
||||
logger.warn('Failed to get user ID');
|
||||
logger.warn("Failed to get user ID");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -87,7 +87,7 @@ export default class WidgetUtils {
|
|||
}
|
||||
|
||||
// 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);
|
||||
return room.currentState.maySendStateEvent("im.vector.modular.widgets", me);
|
||||
}
|
||||
|
||||
// TODO: Generify the name of this function. It's not just scalar.
|
||||
|
@ -98,7 +98,7 @@ export default class WidgetUtils {
|
|||
*/
|
||||
static isScalarUrl(testUrlString: string): boolean {
|
||||
if (!testUrlString) {
|
||||
logger.error('Scalar URL check failed. No URL specified');
|
||||
logger.error("Scalar URL check failed. No URL specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -152,14 +152,14 @@ export default class WidgetUtils {
|
|||
}
|
||||
}
|
||||
|
||||
const startingAccountDataEvent = MatrixClientPeg.get().getAccountData('m.widgets');
|
||||
const startingAccountDataEvent = MatrixClientPeg.get().getAccountData("m.widgets");
|
||||
if (eventInIntendedState(startingAccountDataEvent)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
function onAccountData(ev) {
|
||||
const currentAccountDataEvent = MatrixClientPeg.get().getAccountData('m.widgets');
|
||||
const currentAccountDataEvent = MatrixClientPeg.get().getAccountData("m.widgets");
|
||||
if (eventInIntendedState(currentAccountDataEvent)) {
|
||||
MatrixClientPeg.get().removeListener(ClientEvent.AccountData, onAccountData);
|
||||
clearTimeout(timerId);
|
||||
|
@ -192,7 +192,7 @@ export default class WidgetUtils {
|
|||
// we're waiting for it to be in
|
||||
function eventsInIntendedState(evList) {
|
||||
const widgetPresent = evList.some((ev) => {
|
||||
return ev.getContent() && ev.getContent()['id'] === widgetId;
|
||||
return ev.getContent() && ev.getContent()["id"] === widgetId;
|
||||
});
|
||||
if (add) {
|
||||
return widgetPresent;
|
||||
|
@ -203,7 +203,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');
|
||||
const startingWidgetEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
|
||||
if (eventsInIntendedState(startingWidgetEvents)) {
|
||||
resolve();
|
||||
return;
|
||||
|
@ -213,7 +213,7 @@ export default class WidgetUtils {
|
|||
if (ev.getRoomId() !== roomId || ev.getType() !== "im.vector.modular.widgets") 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');
|
||||
const currentWidgetEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
|
||||
|
||||
if (eventsInIntendedState(currentWidgetEvents)) {
|
||||
MatrixClientPeg.get().removeListener(RoomStateEvent.Events, onRoomStateEvents);
|
||||
|
@ -263,7 +263,7 @@ export default class WidgetUtils {
|
|||
content: content,
|
||||
sender: client.getUserId(),
|
||||
state_key: widgetId,
|
||||
type: 'm.widget',
|
||||
type: "m.widget",
|
||||
id: widgetId,
|
||||
};
|
||||
}
|
||||
|
@ -272,11 +272,14 @@ export default class WidgetUtils {
|
|||
// since the widget won't appear added until this happens. If we don't
|
||||
// wait for this, the action will complete but if the user is fast enough,
|
||||
// the widget still won't actually be there.
|
||||
return client.setAccountData('m.widgets', userWidgets).then(() => {
|
||||
return WidgetUtils.waitForUserWidget(widgetId, addingWidget);
|
||||
}).then(() => {
|
||||
dis.dispatch({ action: "user_widget_updated" });
|
||||
});
|
||||
return client
|
||||
.setAccountData("m.widgets", userWidgets)
|
||||
.then(() => {
|
||||
return WidgetUtils.waitForUserWidget(widgetId, addingWidget);
|
||||
})
|
||||
.then(() => {
|
||||
dis.dispatch({ action: "user_widget_updated" });
|
||||
});
|
||||
}
|
||||
|
||||
static setRoomWidget(
|
||||
|
@ -309,22 +312,21 @@ export default class WidgetUtils {
|
|||
return WidgetUtils.setRoomWidgetContent(roomId, widgetId, content);
|
||||
}
|
||||
|
||||
static setRoomWidgetContent(
|
||||
roomId: string,
|
||||
widgetId: string,
|
||||
content: IWidget,
|
||||
) {
|
||||
static setRoomWidgetContent(roomId: string, widgetId: string, content: IWidget) {
|
||||
const addingWidget = !!content.url;
|
||||
|
||||
WidgetEchoStore.setRoomWidgetEcho(roomId, widgetId, content);
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
// 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(() => {
|
||||
WidgetEchoStore.removeRoomWidgetEcho(roomId, widgetId);
|
||||
});
|
||||
return client
|
||||
.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId)
|
||||
.then(() => {
|
||||
return WidgetUtils.waitForRoomWidget(widgetId, roomId, addingWidget);
|
||||
})
|
||||
.finally(() => {
|
||||
WidgetEchoStore.removeRoomWidgetEcho(roomId, widgetId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -334,7 +336,7 @@ export default class WidgetUtils {
|
|||
*/
|
||||
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');
|
||||
const appsStateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
|
||||
if (!appsStateEvents) {
|
||||
return [];
|
||||
}
|
||||
|
@ -351,9 +353,9 @@ export default class WidgetUtils {
|
|||
static getUserWidgets(): Record<string, IWidgetEvent> {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
throw new Error('User not logged in');
|
||||
throw new Error("User not logged in");
|
||||
}
|
||||
const userWidgets = client.getAccountData('m.widgets');
|
||||
const userWidgets = client.getAccountData("m.widgets");
|
||||
if (userWidgets && userWidgets.getContent()) {
|
||||
return userWidgets.getContent();
|
||||
}
|
||||
|
@ -383,12 +385,12 @@ export default class WidgetUtils {
|
|||
*/
|
||||
static getIntegrationManagerWidgets(): IWidgetEvent[] {
|
||||
const widgets = WidgetUtils.getUserWidgetsArray();
|
||||
return widgets.filter(w => w.content && w.content.type === "m.integration_manager");
|
||||
return widgets.filter((w) => w.content && w.content.type === "m.integration_manager");
|
||||
}
|
||||
|
||||
static getRoomWidgetsOfType(room: Room, type: WidgetType): MatrixEvent[] {
|
||||
const widgets = WidgetUtils.getRoomWidgets(room) || [];
|
||||
return widgets.filter(w => {
|
||||
return widgets.filter((w) => {
|
||||
const content = w.getContent();
|
||||
return content.url && type.matches(content.type);
|
||||
});
|
||||
|
@ -397,9 +399,9 @@ export default class WidgetUtils {
|
|||
static async removeIntegrationManagerWidgets(): Promise<void> {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
throw new Error('User not logged in');
|
||||
throw new Error("User not logged in");
|
||||
}
|
||||
const widgets = client.getAccountData('m.widgets');
|
||||
const widgets = client.getAccountData("m.widgets");
|
||||
if (!widgets) return;
|
||||
const userWidgets: Record<string, IWidgetEvent> = widgets.getContent() || {};
|
||||
Object.entries(userWidgets).forEach(([key, widget]) => {
|
||||
|
@ -407,16 +409,16 @@ export default class WidgetUtils {
|
|||
delete userWidgets[key];
|
||||
}
|
||||
});
|
||||
await client.setAccountData('m.widgets', userWidgets);
|
||||
await client.setAccountData("m.widgets", userWidgets);
|
||||
}
|
||||
|
||||
static addIntegrationManagerWidget(name: string, uiUrl: string, apiUrl: string): Promise<void> {
|
||||
return WidgetUtils.setUserWidget(
|
||||
"integration_manager_" + (new Date().getTime()),
|
||||
"integration_manager_" + new Date().getTime(),
|
||||
WidgetType.INTEGRATION_MANAGER,
|
||||
uiUrl,
|
||||
"Integration manager: " + name,
|
||||
{ "api_url": apiUrl },
|
||||
{ api_url: apiUrl },
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -427,17 +429,17 @@ export default class WidgetUtils {
|
|||
static async removeStickerpickerWidgets(): Promise<void> {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
throw new Error('User not logged in');
|
||||
throw new Error("User not logged in");
|
||||
}
|
||||
const widgets = client.getAccountData('m.widgets');
|
||||
const widgets = client.getAccountData("m.widgets");
|
||||
if (!widgets) return;
|
||||
const userWidgets: Record<string, IWidgetEvent> = widgets.getContent() || {};
|
||||
Object.entries(userWidgets).forEach(([key, widget]) => {
|
||||
if (widget.content && widget.content.type === 'm.stickerpicker') {
|
||||
if (widget.content && widget.content.type === "m.stickerpicker") {
|
||||
delete userWidgets[key];
|
||||
}
|
||||
});
|
||||
await client.setAccountData('m.widgets', userWidgets);
|
||||
await client.setAccountData("m.widgets", userWidgets);
|
||||
}
|
||||
|
||||
static async addJitsiWidget(
|
||||
|
@ -452,7 +454,7 @@ export default class WidgetUtils {
|
|||
const widgetId = randomString(24); // Must be globally unique
|
||||
|
||||
let confId;
|
||||
if (auth === 'openidtoken-jwt') {
|
||||
if (auth === "openidtoken-jwt") {
|
||||
// Create conference ID from room ID
|
||||
// For compatibility with Jitsi, use base32 without padding.
|
||||
// More details here:
|
||||
|
@ -465,8 +467,8 @@ export default class WidgetUtils {
|
|||
|
||||
// TODO: Remove URL hacks when the mobile clients eventually support v2 widgets
|
||||
const widgetUrl = new URL(WidgetUtils.getLocalJitsiWrapperUrl({ auth }));
|
||||
widgetUrl.search = ''; // Causes the URL class use searchParams instead
|
||||
widgetUrl.searchParams.set('confId', confId);
|
||||
widgetUrl.search = ""; // Causes the URL class use searchParams instead
|
||||
widgetUrl.searchParams.set("confId", confId);
|
||||
|
||||
await WidgetUtils.setRoomWidget(roomId, widgetId, WidgetType.JITSI, widgetUrl.toString(), name, {
|
||||
conferenceId: confId,
|
||||
|
@ -498,26 +500,26 @@ export default class WidgetUtils {
|
|||
return app as IApp;
|
||||
}
|
||||
|
||||
static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean, auth?: string} = {}) {
|
||||
static getLocalJitsiWrapperUrl(opts: { forLocalRender?: boolean; auth?: string } = {}) {
|
||||
// NB. we can't just encodeURIComponent all of these because the $ signs need to be there
|
||||
const queryStringParts = [
|
||||
'conferenceDomain=$domain',
|
||||
'conferenceId=$conferenceId',
|
||||
'isAudioOnly=$isAudioOnly',
|
||||
'isVideoChannel=$isVideoChannel',
|
||||
'displayName=$matrix_display_name',
|
||||
'avatarUrl=$matrix_avatar_url',
|
||||
'userId=$matrix_user_id',
|
||||
'roomId=$matrix_room_id',
|
||||
'theme=$theme',
|
||||
'roomName=$roomName',
|
||||
"conferenceDomain=$domain",
|
||||
"conferenceId=$conferenceId",
|
||||
"isAudioOnly=$isAudioOnly",
|
||||
"isVideoChannel=$isVideoChannel",
|
||||
"displayName=$matrix_display_name",
|
||||
"avatarUrl=$matrix_avatar_url",
|
||||
"userId=$matrix_user_id",
|
||||
"roomId=$matrix_room_id",
|
||||
"theme=$theme",
|
||||
"roomName=$roomName",
|
||||
`supportsScreensharing=${PlatformPeg.get().supportsJitsiScreensharing()}`,
|
||||
'language=$org.matrix.msc2873.client_language',
|
||||
"language=$org.matrix.msc2873.client_language",
|
||||
];
|
||||
if (opts.auth) {
|
||||
queryStringParts.push(`auth=${opts.auth}`);
|
||||
}
|
||||
const queryString = queryStringParts.join('&');
|
||||
const queryString = queryStringParts.join("&");
|
||||
|
||||
let baseUrl = window.location.href;
|
||||
if (window.location.protocol !== "https:" && !opts.forLocalRender) {
|
||||
|
@ -550,7 +552,9 @@ export default class WidgetUtils {
|
|||
|
||||
static editWidget(room: Room, app: IApp): void {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(room, 'type_' + app.type, app.id);
|
||||
IntegrationManagers.sharedInstance()
|
||||
.getPrimaryManager()
|
||||
.open(room, "type_" + app.type, app.id);
|
||||
}
|
||||
|
||||
static isManagedByManager(app) {
|
||||
|
|
|
@ -70,7 +70,7 @@ export function arraySmoothingResample(input: number[], points: number): number[
|
|||
// 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) {
|
||||
while (samples.length > points * 2 || samples.length === 0) {
|
||||
samples = [];
|
||||
for (let i = 1; i < input.length - 1; i += 2) {
|
||||
const prevPoint = input[i - 1];
|
||||
|
@ -102,7 +102,7 @@ export function arraySmoothingResample(input: number[], points: number): number[
|
|||
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));
|
||||
return input.map((v) => percentageWithin(percentageOf(v, min, max), newMin, newMax));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -174,8 +174,8 @@ 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 (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;
|
||||
|
@ -184,7 +184,7 @@ export function arrayHasDiff(a: any[], b: any[]): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
export type Diff<T> = { added: T[], removed: T[] };
|
||||
export type Diff<T> = { added: T[]; removed: T[] };
|
||||
|
||||
/**
|
||||
* Performs a diff on two arrays. The result is what is different with the
|
||||
|
@ -196,8 +196,8 @@ export type Diff<T> = { added: T[], removed: T[] };
|
|||
*/
|
||||
export function arrayDiff<T>(a: T[], b: T[]): Diff<T> {
|
||||
return {
|
||||
added: b.filter(i => !a.includes(i)),
|
||||
removed: a.filter(i => !b.includes(i)),
|
||||
added: b.filter((i) => !a.includes(i)),
|
||||
removed: a.filter((i) => !b.includes(i)),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -208,7 +208,7 @@ export function arrayDiff<T>(a: T[], b: T[]): Diff<T> {
|
|||
* @returns The intersection of the arrays.
|
||||
*/
|
||||
export function arrayIntersection<T>(a: T[], b: T[]): T[] {
|
||||
return a.filter(i => b.includes(i));
|
||||
return a.filter((i) => b.includes(i));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -217,10 +217,12 @@ export function arrayIntersection<T>(a: T[], b: T[]): T[] {
|
|||
* @returns The union of all given arrays.
|
||||
*/
|
||||
export function arrayUnion<T>(...a: T[][]): T[] {
|
||||
return Array.from(a.reduce((c, v) => {
|
||||
v.forEach(i => c.add(i));
|
||||
return c;
|
||||
}, new Set<T>()));
|
||||
return Array.from(
|
||||
a.reduce((c, v) => {
|
||||
v.forEach((i) => c.add(i));
|
||||
return c;
|
||||
}, new Set<T>()),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -246,8 +248,7 @@ export class ArrayUtil<T> {
|
|||
* Create a new array helper.
|
||||
* @param a The array to help. Can be modified in-place.
|
||||
*/
|
||||
constructor(private a: T[]) {
|
||||
}
|
||||
constructor(private a: T[]) {}
|
||||
|
||||
/**
|
||||
* The value of this array, after all appropriate alterations.
|
||||
|
@ -280,8 +281,7 @@ 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[]>) {
|
||||
}
|
||||
constructor(private val: Map<K, T[]>) {}
|
||||
|
||||
/**
|
||||
* The value of this group, after all applicable alterations.
|
||||
|
|
|
@ -36,8 +36,9 @@ export type Bounds = {
|
|||
* west of Greenwich has a negative longitude, min -180
|
||||
*/
|
||||
export const getBeaconBounds = (beacons: Beacon[]): Bounds | undefined => {
|
||||
const coords = beacons.filter(beacon => !!beacon.latestLocationState)
|
||||
.map(beacon => parseGeoUri(beacon.latestLocationState.uri));
|
||||
const coords = beacons
|
||||
.filter((beacon) => !!beacon.latestLocationState)
|
||||
.map((beacon) => parseGeoUri(beacon.latestLocationState.uri));
|
||||
|
||||
if (!coords.length) {
|
||||
return;
|
||||
|
@ -51,6 +52,6 @@ export const getBeaconBounds = (beacons: Beacon[]): Bounds | undefined => {
|
|||
north: sortedByLat[0].latitude,
|
||||
south: sortedByLat[sortedByLat.length - 1].latitude,
|
||||
east: sortedByLong[0].longitude,
|
||||
west: sortedByLong[sortedByLong.length -1].longitude,
|
||||
west: sortedByLong[sortedByLong.length - 1].longitude,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -25,7 +25,7 @@ import { Beacon } from "matrix-js-sdk/src/matrix";
|
|||
* @returns remainingMs
|
||||
*/
|
||||
export const msUntilExpiry = (startTimestamp: number, durationMs: number): number =>
|
||||
Math.max(0, (startTimestamp + durationMs) - Date.now());
|
||||
Math.max(0, startTimestamp + durationMs - Date.now());
|
||||
|
||||
export const getBeaconMsUntilExpiry = (beaconInfo: BeaconInfoState): number =>
|
||||
msUntilExpiry(beaconInfo.timestamp, beaconInfo.timeout);
|
||||
|
|
|
@ -20,15 +20,15 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
// https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError
|
||||
export enum GeolocationError {
|
||||
// no navigator.geolocation
|
||||
Unavailable = 'Unavailable',
|
||||
Unavailable = "Unavailable",
|
||||
// The acquisition of the geolocation information failed because the page didn't have the permission to do it.
|
||||
PermissionDenied = 'PermissionDenied',
|
||||
PermissionDenied = "PermissionDenied",
|
||||
// The acquisition of the geolocation failed because at least one internal source of position returned an internal error.
|
||||
PositionUnavailable = 'PositionUnavailable',
|
||||
PositionUnavailable = "PositionUnavailable",
|
||||
// The time allowed to acquire the geolocation was reached before the information was obtained.
|
||||
Timeout = 'Timeout',
|
||||
Timeout = "Timeout",
|
||||
// other unexpected failure
|
||||
Default = 'Default'
|
||||
Default = "Default",
|
||||
}
|
||||
|
||||
const GeolocationOptions = {
|
||||
|
@ -37,12 +37,12 @@ const GeolocationOptions = {
|
|||
};
|
||||
|
||||
const isGeolocationPositionError = (error: unknown): error is GeolocationPositionError =>
|
||||
typeof error === 'object' && !!error['PERMISSION_DENIED'];
|
||||
typeof error === "object" && !!error["PERMISSION_DENIED"];
|
||||
/**
|
||||
* Maps GeolocationPositionError to our GeolocationError enum
|
||||
*/
|
||||
export const mapGeolocationError = (error: GeolocationPositionError | Error): GeolocationError => {
|
||||
logger.error('Geolocation failed', error?.message ?? error);
|
||||
logger.error("Geolocation failed", error?.message ?? error);
|
||||
|
||||
if (isGeolocationPositionError(error)) {
|
||||
switch (error?.code) {
|
||||
|
@ -83,9 +83,7 @@ export type TimedGeoUri = {
|
|||
};
|
||||
|
||||
export const genericPositionFromGeolocation = (geoPosition: GeolocationPosition): GenericPosition => {
|
||||
const {
|
||||
latitude, longitude, altitude, accuracy,
|
||||
} = geoPosition.coords;
|
||||
const { latitude, longitude, altitude, accuracy } = geoPosition.coords;
|
||||
|
||||
return {
|
||||
// safari reports geolocation timestamps as Apple Cocoa Core Data timestamp
|
||||
|
@ -93,23 +91,18 @@ export const genericPositionFromGeolocation = (geoPosition: GeolocationPosition)
|
|||
// they also use local time, not utc
|
||||
// to simplify, just use Date.now()
|
||||
timestamp: Date.now(),
|
||||
latitude, longitude, altitude, accuracy,
|
||||
latitude,
|
||||
longitude,
|
||||
altitude,
|
||||
accuracy,
|
||||
};
|
||||
};
|
||||
|
||||
export const getGeoUri = (position: GenericPosition): string => {
|
||||
const lat = position.latitude;
|
||||
const lon = position.longitude;
|
||||
const alt = (
|
||||
Number.isFinite(position.altitude)
|
||||
? `,${position.altitude}`
|
||||
: ""
|
||||
);
|
||||
const acc = (
|
||||
Number.isFinite(position.accuracy)
|
||||
? `;u=${position.accuracy}`
|
||||
: ""
|
||||
);
|
||||
const alt = Number.isFinite(position.altitude) ? `,${position.altitude}` : "";
|
||||
const acc = Number.isFinite(position.accuracy) ? `;u=${position.accuracy}` : "";
|
||||
return `geo:${lat},${lon}${alt}${acc}`;
|
||||
};
|
||||
|
||||
|
|
|
@ -14,11 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
getBeaconInfoIdentifier,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixClient, MatrixEvent, getBeaconInfoIdentifier } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
/**
|
||||
* Beacons should only have shareable locations (open in external mapping tool, forward)
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './duration';
|
||||
export * from './geolocation';
|
||||
export * from './useBeacon';
|
||||
export * from './useOwnLiveBeacons';
|
||||
export * from "./duration";
|
||||
export * from "./geolocation";
|
||||
export * from "./useBeacon";
|
||||
export * from "./useOwnLiveBeacons";
|
||||
|
|
|
@ -21,11 +21,8 @@ import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
|
|||
* beacon_info events without live property set to true
|
||||
* should be displayed in the timeline
|
||||
*/
|
||||
export const shouldDisplayAsBeaconTile = (event: MatrixEvent): boolean => (
|
||||
export const shouldDisplayAsBeaconTile = (event: MatrixEvent): boolean =>
|
||||
M_BEACON_INFO.matches(event.getType()) &&
|
||||
(
|
||||
event.getContent()?.live ||
|
||||
(event.getContent()?.live ||
|
||||
// redacted beacons should show 'message deleted' tile
|
||||
event.isRedacted()
|
||||
)
|
||||
);
|
||||
event.isRedacted());
|
||||
|
|
|
@ -15,12 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import {
|
||||
Beacon,
|
||||
BeaconEvent,
|
||||
MatrixEvent,
|
||||
getBeaconInfoIdentifier,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { Beacon, BeaconEvent, MatrixEvent, getBeaconInfoIdentifier } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { useEventEmitterState } from "../../hooks/useEventEmitter";
|
||||
|
@ -56,11 +51,7 @@ export const useBeacon = (beaconInfoEvent: MatrixEvent): Beacon | undefined => {
|
|||
|
||||
// beacon update will fire when this beacon is superseded
|
||||
// check the updated event id for equality to the matrix event
|
||||
const beaconInstanceEventId = useEventEmitterState(
|
||||
beacon,
|
||||
BeaconEvent.Update,
|
||||
() => beacon?.beaconInfoId,
|
||||
);
|
||||
const beaconInstanceEventId = useEventEmitterState(beacon, BeaconEvent.Update, () => beacon?.beaconInfoId);
|
||||
|
||||
useEffect(() => {
|
||||
if (beaconInstanceEventId && beaconInstanceEventId !== beaconInfoEvent.getId()) {
|
||||
|
|
|
@ -14,12 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Beacon,
|
||||
Room,
|
||||
RoomStateEvent,
|
||||
MatrixClient,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { Beacon, Room, RoomStateEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { useEventEmitterState } from "../../hooks/useEventEmitter";
|
||||
|
||||
|
@ -28,13 +23,11 @@ import { useEventEmitterState } from "../../hooks/useEventEmitter";
|
|||
*
|
||||
* Beacons are removed from array when they become inactive
|
||||
*/
|
||||
export const useLiveBeacons = (roomId: Room['roomId'], matrixClient: MatrixClient): Beacon[] => {
|
||||
export const useLiveBeacons = (roomId: Room["roomId"], matrixClient: MatrixClient): Beacon[] => {
|
||||
const room = matrixClient.getRoom(roomId);
|
||||
|
||||
const liveBeacons = useEventEmitterState(
|
||||
room.currentState,
|
||||
RoomStateEvent.BeaconLiveness,
|
||||
() => room.currentState?.liveBeaconIds.map(beaconIdentifier => room.currentState.beacons.get(beaconIdentifier)),
|
||||
const liveBeacons = useEventEmitterState(room.currentState, RoomStateEvent.BeaconLiveness, () =>
|
||||
room.currentState?.liveBeaconIds.map((beaconIdentifier) => room.currentState.beacons.get(beaconIdentifier)),
|
||||
);
|
||||
|
||||
return liveBeacons;
|
||||
|
|
|
@ -43,15 +43,13 @@ export const useOwnLiveBeacons = (liveBeaconIds: BeaconIdentifier[]): LiveBeacon
|
|||
const hasLocationPublishError = useEventEmitterState(
|
||||
OwnBeaconStore.instance,
|
||||
OwnBeaconStoreEvent.LocationPublishError,
|
||||
() =>
|
||||
liveBeaconIds.some(OwnBeaconStore.instance.beaconHasLocationPublishError),
|
||||
() => liveBeaconIds.some(OwnBeaconStore.instance.beaconHasLocationPublishError),
|
||||
);
|
||||
|
||||
const hasStopSharingError = useEventEmitterState(
|
||||
OwnBeaconStore.instance,
|
||||
OwnBeaconStoreEvent.BeaconUpdateError,
|
||||
() =>
|
||||
liveBeaconIds.some(id => OwnBeaconStore.instance.beaconUpdateErrors.has(id)),
|
||||
() => liveBeaconIds.some((id) => OwnBeaconStore.instance.beaconUpdateErrors.has(id)),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -66,21 +64,22 @@ export const useOwnLiveBeacons = (liveBeaconIds: BeaconIdentifier[]): LiveBeacon
|
|||
}, [liveBeaconIds]);
|
||||
|
||||
// select the beacon with latest expiry to display expiry time
|
||||
const beacon = liveBeaconIds.map(beaconId => OwnBeaconStore.instance.getBeaconById(beaconId))
|
||||
const beacon = liveBeaconIds
|
||||
.map((beaconId) => OwnBeaconStore.instance.getBeaconById(beaconId))
|
||||
.sort(sortBeaconsByLatestExpiry)
|
||||
.shift();
|
||||
|
||||
const onStopSharing = async () => {
|
||||
setStoppingInProgress(true);
|
||||
try {
|
||||
await Promise.all(liveBeaconIds.map(beaconId => OwnBeaconStore.instance.stopBeacon(beaconId)));
|
||||
await Promise.all(liveBeaconIds.map((beaconId) => OwnBeaconStore.instance.stopBeacon(beaconId)));
|
||||
} catch (error) {
|
||||
setStoppingInProgress(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onResetLocationPublishError = () => {
|
||||
liveBeaconIds.forEach(beaconId => {
|
||||
liveBeaconIds.forEach((beaconId) => {
|
||||
OwnBeaconStore.instance.resetLocationPublishError(beaconId);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -49,34 +49,34 @@ limitations under the License.
|
|||
// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar.
|
||||
|
||||
const ALLOWED_BLOB_MIMETYPES = [
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/png',
|
||||
'image/apng',
|
||||
'image/webp',
|
||||
'image/avif',
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
"image/png",
|
||||
"image/apng",
|
||||
"image/webp",
|
||||
"image/avif",
|
||||
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
'video/quicktime',
|
||||
"video/mp4",
|
||||
"video/webm",
|
||||
"video/ogg",
|
||||
"video/quicktime",
|
||||
|
||||
'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',
|
||||
"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 "application/octet-stream";
|
||||
}
|
||||
return mimetype;
|
||||
}
|
||||
|
|
|
@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { split } from 'lodash';
|
||||
import { split } from "lodash";
|
||||
|
||||
export function textToHtmlRainbow(str: string): string {
|
||||
const frequency = (2 * Math.PI) / str.length;
|
||||
|
||||
return split(str, '')
|
||||
return split(str, "")
|
||||
.map((c, i) => {
|
||||
if (c === " ") {
|
||||
return c;
|
||||
|
|
|
@ -62,9 +62,7 @@ export default function createMatrixClient(opts: ICreateClientOpts): MatrixClien
|
|||
}
|
||||
|
||||
if (indexedDB) {
|
||||
storeOpts.cryptoStore = new IndexedDBCryptoStore(
|
||||
indexedDB, "matrix-js-sdk:crypto",
|
||||
);
|
||||
storeOpts.cryptoStore = new IndexedDBCryptoStore(indexedDB, "matrix-js-sdk:crypto");
|
||||
} else if (localStorage) {
|
||||
storeOpts.cryptoStore = new LocalStorageCryptoStore(localStorage);
|
||||
} else {
|
||||
|
|
|
@ -70,9 +70,7 @@ export const recordClientInformation = async (
|
|||
* @todo(kerrya) revisit after MSC3391: account data deletion is done
|
||||
* (PSBE-12)
|
||||
*/
|
||||
export const removeClientInformation = async (
|
||||
matrixClient: MatrixClient,
|
||||
): Promise<void> => {
|
||||
export const removeClientInformation = async (matrixClient: MatrixClient): Promise<void> => {
|
||||
const deviceId = matrixClient.getDeviceId();
|
||||
const type = getClientInformationEventType(deviceId);
|
||||
const clientInformation = getDeviceClientInformation(matrixClient, deviceId);
|
||||
|
@ -84,7 +82,7 @@ export const removeClientInformation = async (
|
|||
};
|
||||
|
||||
const sanitizeContentString = (value: unknown): string | undefined =>
|
||||
value && typeof value === 'string' ? value : undefined;
|
||||
value && typeof value === "string" ? value : undefined;
|
||||
|
||||
export const getDeviceClientInformation = (matrixClient: MatrixClient, deviceId: string): DeviceClientInformation => {
|
||||
const event = matrixClient.getAccountData(getClientInformationEventType(deviceId));
|
||||
|
@ -101,4 +99,3 @@ export const getDeviceClientInformation = (matrixClient: MatrixClient, deviceId:
|
|||
url: sanitizeContentString(url),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import UAParser from 'ua-parser-js';
|
||||
import UAParser from "ua-parser-js";
|
||||
|
||||
export enum DeviceType {
|
||||
Desktop = 'Desktop',
|
||||
Mobile = 'Mobile',
|
||||
Web = 'Web',
|
||||
Unknown = 'Unknown',
|
||||
Desktop = "Desktop",
|
||||
Mobile = "Mobile",
|
||||
Web = "Web",
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
export type ExtendedDeviceInformation = {
|
||||
deviceType: DeviceType;
|
||||
|
@ -42,17 +42,13 @@ const getDeviceType = (
|
|||
browser: UAParser.IBrowser,
|
||||
operatingSystem: UAParser.IOS,
|
||||
): DeviceType => {
|
||||
if (browser.name === 'Electron') {
|
||||
if (browser.name === "Electron") {
|
||||
return DeviceType.Desktop;
|
||||
}
|
||||
if (!!browser.name) {
|
||||
return DeviceType.Web;
|
||||
}
|
||||
if (
|
||||
device.type === 'mobile' ||
|
||||
operatingSystem.name?.includes('Android') ||
|
||||
userAgent.indexOf(IOS_KEYWORD) > -1
|
||||
) {
|
||||
if (device.type === "mobile" || operatingSystem.name?.includes("Android") || userAgent.indexOf(IOS_KEYWORD) > -1) {
|
||||
return DeviceType.Mobile;
|
||||
}
|
||||
return DeviceType.Unknown;
|
||||
|
@ -72,18 +68,18 @@ const checkForCustomValues = (userAgent: string): CustomValues => {
|
|||
return {};
|
||||
}
|
||||
|
||||
const mightHaveDevice = userAgent.includes('(');
|
||||
const mightHaveDevice = userAgent.includes("(");
|
||||
if (!mightHaveDevice) {
|
||||
return {};
|
||||
}
|
||||
const deviceInfoSegments = userAgent.substring(userAgent.indexOf('(') + 1).split('; ');
|
||||
const deviceInfoSegments = userAgent.substring(userAgent.indexOf("(") + 1).split("; ");
|
||||
const customDeviceModel = deviceInfoSegments[0] || undefined;
|
||||
const customDeviceOS = deviceInfoSegments[1] || undefined;
|
||||
return { customDeviceModel, customDeviceOS };
|
||||
};
|
||||
|
||||
const concatenateNameAndVersion = (name?: string, version?: string): string | undefined =>
|
||||
name && [name, version].filter(Boolean).join(' ');
|
||||
name && [name, version].filter(Boolean).join(" ");
|
||||
|
||||
export const parseUserAgent = (userAgent?: string): ExtendedDeviceInformation => {
|
||||
if (!userAgent) {
|
||||
|
@ -111,9 +107,8 @@ export const parseUserAgent = (userAgent?: string): ExtendedDeviceInformation =>
|
|||
const client = concatenateNameAndVersion(browser.name, browser.version);
|
||||
|
||||
// only try to parse custom model and OS when device type is known
|
||||
const { customDeviceModel, customDeviceOS } = deviceType !== DeviceType.Unknown
|
||||
? checkForCustomValues(userAgent)
|
||||
: {} as CustomValues;
|
||||
const { customDeviceModel, customDeviceOS } =
|
||||
deviceType !== DeviceType.Unknown ? checkForCustomValues(userAgent) : ({} as CustomValues);
|
||||
|
||||
return {
|
||||
deviceType,
|
||||
|
|
|
@ -16,14 +16,14 @@ limitations under the License.
|
|||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
const SNOOZE_KEY = 'mx_snooze_bulk_unverified_device_nag';
|
||||
const SNOOZE_KEY = "mx_snooze_bulk_unverified_device_nag";
|
||||
// one week
|
||||
const snoozePeriod = 1000 * 60 * 60 * 24 * 7;
|
||||
export const snoozeBulkUnverifiedDeviceReminder = () => {
|
||||
try {
|
||||
localStorage.setItem(SNOOZE_KEY, String(Date.now()));
|
||||
} catch (error) {
|
||||
logger.error('Failed to persist bulk unverified device nag snooze', error);
|
||||
logger.error("Failed to persist bulk unverified device nag snooze", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -31,9 +31,9 @@ export const isBulkUnverifiedDeviceReminderSnoozed = () => {
|
|||
try {
|
||||
const snoozedTimestamp = localStorage.getItem(SNOOZE_KEY);
|
||||
|
||||
const parsedTimestamp = Number.parseInt(snoozedTimestamp || '', 10);
|
||||
const parsedTimestamp = Number.parseInt(snoozedTimestamp || "", 10);
|
||||
|
||||
return Number.isInteger(parsedTimestamp) && (parsedTimestamp + snoozePeriod) > Date.now();
|
||||
return Number.isInteger(parsedTimestamp) && parsedTimestamp + snoozePeriod > Date.now();
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -29,10 +29,7 @@ import { privateShouldBeEncrypted } from "./rooms";
|
|||
import { createDmLocalRoom } from "./dm/createDmLocalRoom";
|
||||
import { startDm } from "./dm/startDm";
|
||||
|
||||
export async function startDmOnFirstMessage(
|
||||
client: MatrixClient,
|
||||
targets: Member[],
|
||||
): Promise<Room> {
|
||||
export async function startDmOnFirstMessage(client: MatrixClient, targets: Member[]): Promise<Room> {
|
||||
const existingRoom = findDMRoom(client, targets);
|
||||
if (existingRoom) {
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
|
@ -114,7 +111,7 @@ export class DirectoryMember extends Member {
|
|||
private readonly avatarUrl?: string;
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
constructor(userDirResult: { user_id: string, display_name?: string, avatar_url?: string }) {
|
||||
constructor(userDirResult: { user_id: string; display_name?: string; avatar_url?: string }) {
|
||||
super();
|
||||
this._userId = userDirResult.user_id;
|
||||
this.displayName = userDirResult.display_name;
|
||||
|
@ -147,7 +144,7 @@ export class ThreepidMember extends Member {
|
|||
// better type support in the react-sdk we can use this trick to determine the kind
|
||||
// of 3PID we're dealing with, if any.
|
||||
get isEmail(): boolean {
|
||||
return this.id.includes('@');
|
||||
return this.id.includes("@");
|
||||
}
|
||||
|
||||
// These next class members are for the Member interface
|
||||
|
@ -181,9 +178,9 @@ export async function determineCreateRoomEncryptionOption(client: MatrixClient,
|
|||
if (privateShouldBeEncrypted()) {
|
||||
// Check whether all users have uploaded device keys before.
|
||||
// If so, enable encryption in the new room.
|
||||
const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
|
||||
const has3PidMembers = targets.some((t) => t instanceof ThreepidMember);
|
||||
if (!has3PidMembers) {
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
const targetIds = targets.map((t) => t.userId);
|
||||
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
|
||||
if (allHaveDeviceKeys) {
|
||||
return true;
|
||||
|
|
|
@ -30,28 +30,27 @@ import { determineCreateRoomEncryptionOption, Member } from "../../../src/utils/
|
|||
* @param {Member[]} targets DM partners
|
||||
* @returns {Promise<LocalRoom>} Resolves to the new local room
|
||||
*/
|
||||
export async function createDmLocalRoom(
|
||||
client: MatrixClient,
|
||||
targets: Member[],
|
||||
): Promise<LocalRoom> {
|
||||
export async function createDmLocalRoom(client: MatrixClient, targets: Member[]): Promise<LocalRoom> {
|
||||
const userId = client.getUserId();
|
||||
|
||||
const localRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + client.makeTxnId(), client, userId);
|
||||
const events = [];
|
||||
|
||||
events.push(new MatrixEvent({
|
||||
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
|
||||
type: EventType.RoomCreate,
|
||||
content: {
|
||||
creator: userId,
|
||||
room_version: KNOWN_SAFE_ROOM_VERSION,
|
||||
},
|
||||
state_key: "",
|
||||
user_id: userId,
|
||||
sender: userId,
|
||||
room_id: localRoom.roomId,
|
||||
origin_server_ts: Date.now(),
|
||||
}));
|
||||
events.push(
|
||||
new MatrixEvent({
|
||||
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
|
||||
type: EventType.RoomCreate,
|
||||
content: {
|
||||
creator: userId,
|
||||
room_version: KNOWN_SAFE_ROOM_VERSION,
|
||||
},
|
||||
state_key: "",
|
||||
user_id: userId,
|
||||
sender: userId,
|
||||
room_id: localRoom.roomId,
|
||||
origin_server_ts: Date.now(),
|
||||
}),
|
||||
);
|
||||
|
||||
if (await determineCreateRoomEncryptionOption(client, targets)) {
|
||||
localRoom.encrypted = true;
|
||||
|
@ -71,45 +70,51 @@ export async function createDmLocalRoom(
|
|||
);
|
||||
}
|
||||
|
||||
events.push(new MatrixEvent({
|
||||
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
|
||||
type: EventType.RoomMember,
|
||||
content: {
|
||||
displayname: userId,
|
||||
membership: "join",
|
||||
},
|
||||
state_key: userId,
|
||||
user_id: userId,
|
||||
sender: userId,
|
||||
room_id: localRoom.roomId,
|
||||
}));
|
||||
|
||||
targets.forEach((target: Member) => {
|
||||
events.push(new MatrixEvent({
|
||||
events.push(
|
||||
new MatrixEvent({
|
||||
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
|
||||
type: EventType.RoomMember,
|
||||
content: {
|
||||
displayname: target.name,
|
||||
avatar_url: target.getMxcAvatarUrl(),
|
||||
membership: "invite",
|
||||
isDirect: true,
|
||||
},
|
||||
state_key: target.userId,
|
||||
sender: userId,
|
||||
room_id: localRoom.roomId,
|
||||
}));
|
||||
events.push(new MatrixEvent({
|
||||
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
|
||||
type: EventType.RoomMember,
|
||||
content: {
|
||||
displayname: target.name,
|
||||
avatar_url: target.getMxcAvatarUrl(),
|
||||
displayname: userId,
|
||||
membership: "join",
|
||||
},
|
||||
state_key: target.userId,
|
||||
sender: target.userId,
|
||||
state_key: userId,
|
||||
user_id: userId,
|
||||
sender: userId,
|
||||
room_id: localRoom.roomId,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
targets.forEach((target: Member) => {
|
||||
events.push(
|
||||
new MatrixEvent({
|
||||
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
|
||||
type: EventType.RoomMember,
|
||||
content: {
|
||||
displayname: target.name,
|
||||
avatar_url: target.getMxcAvatarUrl(),
|
||||
membership: "invite",
|
||||
isDirect: true,
|
||||
},
|
||||
state_key: target.userId,
|
||||
sender: userId,
|
||||
room_id: localRoom.roomId,
|
||||
}),
|
||||
);
|
||||
events.push(
|
||||
new MatrixEvent({
|
||||
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
|
||||
type: EventType.RoomMember,
|
||||
content: {
|
||||
displayname: target.name,
|
||||
avatar_url: target.getMxcAvatarUrl(),
|
||||
membership: "join",
|
||||
},
|
||||
state_key: target.userId,
|
||||
sender: target.userId,
|
||||
room_id: localRoom.roomId,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
localRoom.targets = targets;
|
||||
|
|
|
@ -30,29 +30,30 @@ import { getFunctionalMembers } from "../room/getFunctionalMembers";
|
|||
*/
|
||||
export function findDMForUser(client: MatrixClient, userId: string): Room {
|
||||
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
||||
const rooms = roomIds.map(id => client.getRoom(id));
|
||||
const suitableDMRooms = rooms.filter(r => {
|
||||
// Validate that we are joined and the other person is also joined. We'll also make sure
|
||||
// that the room also looks like a DM (until we have canonical DMs to tell us). For now,
|
||||
// a DM is a room of two people that contains those two people exactly. This does mean
|
||||
// that bots, assistants, etc will ruin a room's DM-ness, though this is a problem for
|
||||
// canonical DMs to solve.
|
||||
if (r && r.getMyMembership() === "join") {
|
||||
if (isLocalRoom(r)) return false;
|
||||
const rooms = roomIds.map((id) => client.getRoom(id));
|
||||
const suitableDMRooms = rooms
|
||||
.filter((r) => {
|
||||
// Validate that we are joined and the other person is also joined. We'll also make sure
|
||||
// that the room also looks like a DM (until we have canonical DMs to tell us). For now,
|
||||
// a DM is a room of two people that contains those two people exactly. This does mean
|
||||
// that bots, assistants, etc will ruin a room's DM-ness, though this is a problem for
|
||||
// canonical DMs to solve.
|
||||
if (r && r.getMyMembership() === "join") {
|
||||
if (isLocalRoom(r)) return false;
|
||||
|
||||
const functionalUsers = getFunctionalMembers(r);
|
||||
const members = r.currentState.getMembers();
|
||||
const joinedMembers = members.filter(
|
||||
m => !functionalUsers.includes(m.userId) && isJoinedOrNearlyJoined(m.membership),
|
||||
);
|
||||
const otherMember = joinedMembers.find(m => m.userId === userId);
|
||||
return otherMember && joinedMembers.length === 2;
|
||||
}
|
||||
return false;
|
||||
}).sort((r1, r2) => {
|
||||
return r2.getLastActiveTimestamp() -
|
||||
r1.getLastActiveTimestamp();
|
||||
});
|
||||
const functionalUsers = getFunctionalMembers(r);
|
||||
const members = r.currentState.getMembers();
|
||||
const joinedMembers = members.filter(
|
||||
(m) => !functionalUsers.includes(m.userId) && isJoinedOrNearlyJoined(m.membership),
|
||||
);
|
||||
const otherMember = joinedMembers.find((m) => m.userId === userId);
|
||||
return otherMember && joinedMembers.length === 2;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.sort((r1, r2) => {
|
||||
return r2.getLastActiveTimestamp() - r1.getLastActiveTimestamp();
|
||||
});
|
||||
if (suitableDMRooms.length) {
|
||||
return suitableDMRooms[0];
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import { findDMForUser } from "./findDMForUser";
|
|||
* @returns {Room | null} Resolved so the room if found, else null
|
||||
*/
|
||||
export function findDMRoom(client: MatrixClient, targets: Member[]): Room | null {
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
const targetIds = targets.map((t) => t.userId);
|
||||
let existingRoom: Room;
|
||||
if (targetIds.length === 1) {
|
||||
existingRoom = findDMForUser(client, targetIds[0]);
|
||||
|
|
|
@ -32,7 +32,7 @@ import createRoom from "../../createRoom";
|
|||
* @returns {Promise<string | null} Resolves to the room id.
|
||||
*/
|
||||
export async function startDm(client: MatrixClient, targets: Member[], showSpinner = true): Promise<string | null> {
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
const targetIds = targets.map((t) => t.userId);
|
||||
|
||||
// Check if there is already a DM with these people and reuse it if possible.
|
||||
let existingRoom: Room;
|
||||
|
@ -69,14 +69,14 @@ export async function startDm(client: MatrixClient, targets: Member[], showSpinn
|
|||
createRoomOptions.createOpts = targetIds.reduce(
|
||||
(roomOptions, address) => {
|
||||
const type = getAddressType(address);
|
||||
if (type === 'email') {
|
||||
if (type === "email") {
|
||||
const invite: IInvite3PID = {
|
||||
id_server: client.getIdentityServerUrl(true),
|
||||
medium: 'email',
|
||||
medium: "email",
|
||||
address,
|
||||
};
|
||||
roomOptions.invite_3pid.push(invite);
|
||||
} else if (type === 'mx-user-id') {
|
||||
} else if (type === "mx-user-id") {
|
||||
roomOptions.invite.push(address);
|
||||
}
|
||||
return roomOptions;
|
||||
|
|
|
@ -15,10 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
export class GenericError extends Error {
|
||||
constructor(
|
||||
public readonly message: string,
|
||||
public readonly description?: string | undefined,
|
||||
) {
|
||||
constructor(public readonly message: string, public readonly description?: string | undefined) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,9 +48,10 @@ export default abstract class Exporter {
|
|||
protected exportOptions: IExportOptions,
|
||||
protected setProgressText: React.Dispatch<React.SetStateAction<string>>,
|
||||
) {
|
||||
if (exportOptions.maxSize < 1 * 1024 * 1024|| // Less than 1 MB
|
||||
if (
|
||||
exportOptions.maxSize < 1 * 1024 * 1024 || // Less than 1 MB
|
||||
exportOptions.maxSize > 8000 * 1024 * 1024 || // More than 8 GB
|
||||
exportOptions.numberOfMessages > 10**8
|
||||
exportOptions.numberOfMessages > 10 ** 8
|
||||
) {
|
||||
throw new Error("Invalid export options");
|
||||
}
|
||||
|
@ -64,7 +65,7 @@ export default abstract class Exporter {
|
|||
|
||||
protected onBeforeUnload(e: BeforeUnloadEvent): string {
|
||||
e.preventDefault();
|
||||
return e.returnValue = _t("Are you sure you want to exit during this export?");
|
||||
return (e.returnValue = _t("Are you sure you want to exit during this export?"));
|
||||
}
|
||||
|
||||
protected updateProgress(progress: string, log = true, show = true): void {
|
||||
|
@ -84,8 +85,7 @@ export default abstract class Exporter {
|
|||
// First try to use the real name of the room, then a translated copy of a generic name,
|
||||
// then finally hardcoded default to guarantee we'll have a name.
|
||||
const safeRoomName = sanitizeFilename(this.room.name ?? _t("Unnamed Room")).trim() || "Unnamed Room";
|
||||
const safeDate = formatFullDateNoDayISO(new Date())
|
||||
.replace(/:/g, '-'); // ISO format automatically removes a lot of stuff for us
|
||||
const safeDate = formatFullDateNoDayISO(new Date()).replace(/:/g, "-"); // ISO format automatically removes a lot of stuff for us
|
||||
const safeBrand = sanitizeFilename(brand);
|
||||
return `${safeBrand} - ${safeRoomName} - Chat Export - ${safeDate}`;
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ export default abstract class Exporter {
|
|||
protected async downloadZIP(): Promise<string | void> {
|
||||
const filename = this.destinationFileName;
|
||||
const filenameWithoutExt = filename.substring(0, filename.length - 4); // take off the .zip
|
||||
const { default: JSZip } = await import('jszip');
|
||||
const { default: JSZip } = await import("jszip");
|
||||
|
||||
const zip = new JSZip();
|
||||
// Create a writable stream to the directory
|
||||
|
@ -125,13 +125,9 @@ export default abstract class Exporter {
|
|||
|
||||
protected setEventMetadata(event: MatrixEvent): MatrixEvent {
|
||||
const roomState = this.client.getRoom(this.room.roomId).currentState;
|
||||
event.sender = roomState.getSentinelMember(
|
||||
event.getSender(),
|
||||
);
|
||||
event.sender = roomState.getSentinelMember(event.getSender());
|
||||
if (event.getType() === "m.room.member") {
|
||||
event.target = roomState.getSentinelMember(
|
||||
event.getStateKey(),
|
||||
);
|
||||
event.target = roomState.getSentinelMember(event.getStateKey());
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
@ -146,7 +142,7 @@ export default abstract class Exporter {
|
|||
limit = 40;
|
||||
break;
|
||||
default:
|
||||
limit = 10**8;
|
||||
limit = 10 ** 8;
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
|
@ -154,7 +150,7 @@ export default abstract class Exporter {
|
|||
protected async getRequiredEvents(): Promise<MatrixEvent[]> {
|
||||
const eventMapper = this.client.getEventMapper();
|
||||
|
||||
let prevToken: string|null = null;
|
||||
let prevToken: string | null = null;
|
||||
let limit = this.getLimit();
|
||||
const events: MatrixEvent[] = [];
|
||||
|
||||
|
@ -188,26 +184,30 @@ export default abstract class Exporter {
|
|||
}
|
||||
|
||||
if (this.exportType === ExportType.LastNMessages) {
|
||||
this.updateProgress(_t("Fetched %(count)s events out of %(total)s", {
|
||||
count: events.length,
|
||||
total: this.exportOptions.numberOfMessages,
|
||||
}));
|
||||
this.updateProgress(
|
||||
_t("Fetched %(count)s events out of %(total)s", {
|
||||
count: events.length,
|
||||
total: this.exportOptions.numberOfMessages,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
this.updateProgress(_t("Fetched %(count)s events so far", {
|
||||
count: events.length,
|
||||
}));
|
||||
this.updateProgress(
|
||||
_t("Fetched %(count)s events so far", {
|
||||
count: events.length,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
prevToken = res.end;
|
||||
}
|
||||
// Reverse the events so that we preserve the order
|
||||
for (let i = 0; i < Math.floor(events.length/2); i++) {
|
||||
for (let i = 0; i < Math.floor(events.length / 2); i++) {
|
||||
[events[i], events[events.length - i - 1]] = [events[events.length - i - 1], events[i]];
|
||||
}
|
||||
|
||||
const decryptionPromises = events
|
||||
.filter(event => event.isEncrypted())
|
||||
.map(event => {
|
||||
.filter((event) => event.isEncrypted())
|
||||
.map((event) => {
|
||||
return this.client.decryptEventIfNeeded(event, {
|
||||
isRetry: true,
|
||||
emit: false,
|
||||
|
@ -242,11 +242,11 @@ export default abstract class Exporter {
|
|||
}
|
||||
|
||||
public splitFileName(file: string): string[] {
|
||||
const lastDot = file.lastIndexOf('.');
|
||||
const lastDot = file.lastIndexOf(".");
|
||||
if (lastDot === -1) return [file, ""];
|
||||
const fileName = file.slice(0, lastDot);
|
||||
const ext = file.slice(lastDot + 1);
|
||||
return [fileName, '.' + ext];
|
||||
return [fileName, "." + ext];
|
||||
}
|
||||
|
||||
public getFilePath(event: MatrixEvent): string {
|
||||
|
@ -271,7 +271,7 @@ export default abstract class Exporter {
|
|||
if (event.getType() === "m.sticker") fileExt = ".png";
|
||||
if (isVoiceMessage(event)) fileExt = ".ogg";
|
||||
|
||||
return fileDirectory + "/" + fileName + '-' + fileDate + fileExt;
|
||||
return fileDirectory + "/" + fileName + "-" + fileDate + fileExt;
|
||||
}
|
||||
|
||||
protected isReply(event: MatrixEvent): boolean {
|
||||
|
|
|
@ -106,31 +106,27 @@ export default class HTMLExporter extends Exporter {
|
|||
|
||||
const exportedText = renderToStaticMarkup(
|
||||
<p>
|
||||
{ _t(
|
||||
{_t(
|
||||
"This is the start of export of <roomName/>. Exported by <exporterDetails/> at %(exportDate)s.",
|
||||
{
|
||||
exportDate,
|
||||
},
|
||||
{
|
||||
roomName: () => <b>{ this.room.name }</b>,
|
||||
roomName: () => <b>{this.room.name}</b>,
|
||||
exporterDetails: () => (
|
||||
<a
|
||||
href={`https://matrix.to/#/${exporter}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{ exporterName ? (
|
||||
<a href={`https://matrix.to/#/${exporter}`} target="_blank" rel="noopener noreferrer">
|
||||
{exporterName ? (
|
||||
<>
|
||||
<b>{ exporterName }</b>
|
||||
{ " (" + exporter + ")" }
|
||||
<b>{exporterName}</b>
|
||||
{" (" + exporter + ")"}
|
||||
</>
|
||||
) : (
|
||||
<b>{ exporter }</b>
|
||||
) }
|
||||
<b>{exporter}</b>
|
||||
)}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
) }
|
||||
)}
|
||||
</p>,
|
||||
);
|
||||
|
||||
|
@ -224,12 +220,7 @@ export default class HTMLExporter extends Exporter {
|
|||
protected getAvatarURL(event: MatrixEvent): string {
|
||||
const member = event.sender;
|
||||
return (
|
||||
member.getMxcAvatarUrl() &&
|
||||
mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(
|
||||
30,
|
||||
30,
|
||||
"crop",
|
||||
)
|
||||
member.getMxcAvatarUrl() && mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(30, 30, "crop")
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -241,7 +232,7 @@ export default class HTMLExporter extends Exporter {
|
|||
this.avatars.set(member.userId, true);
|
||||
const image = await fetch(avatarUrl);
|
||||
const blob = await image.blob();
|
||||
this.addFile(`users/${member.userId.replace(/:/g, '-')}.png`, blob);
|
||||
this.addFile(`users/${member.userId.replace(/:/g, "-")}.png`, blob);
|
||||
} catch (err) {
|
||||
logger.log("Failed to fetch user's avatar" + err);
|
||||
}
|
||||
|
@ -264,32 +255,34 @@ export default class HTMLExporter extends Exporter {
|
|||
}
|
||||
|
||||
public getEventTile(mxEv: MatrixEvent, continuation: boolean) {
|
||||
return <div className="mx_Export_EventWrapper" id={mxEv.getId()}>
|
||||
<MatrixClientContext.Provider value={this.client}>
|
||||
<EventTile
|
||||
mxEvent={mxEv}
|
||||
continuation={continuation}
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
replacingEventId={mxEv.replacingEventId()}
|
||||
forExport={true}
|
||||
readReceipts={null}
|
||||
alwaysShowTimestamps={true}
|
||||
readReceiptMap={null}
|
||||
showUrlPreview={false}
|
||||
checkUnmounting={() => false}
|
||||
isTwelveHour={false}
|
||||
last={false}
|
||||
lastInSection={false}
|
||||
permalinkCreator={this.permalinkCreator}
|
||||
lastSuccessful={false}
|
||||
isSelectedEvent={false}
|
||||
getRelationsForEvent={null}
|
||||
showReactions={false}
|
||||
layout={Layout.Group}
|
||||
showReadReceipts={false}
|
||||
/>
|
||||
</MatrixClientContext.Provider>
|
||||
</div>;
|
||||
return (
|
||||
<div className="mx_Export_EventWrapper" id={mxEv.getId()}>
|
||||
<MatrixClientContext.Provider value={this.client}>
|
||||
<EventTile
|
||||
mxEvent={mxEv}
|
||||
continuation={continuation}
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
replacingEventId={mxEv.replacingEventId()}
|
||||
forExport={true}
|
||||
readReceipts={null}
|
||||
alwaysShowTimestamps={true}
|
||||
readReceiptMap={null}
|
||||
showUrlPreview={false}
|
||||
checkUnmounting={() => false}
|
||||
isTwelveHour={false}
|
||||
last={false}
|
||||
lastInSection={false}
|
||||
permalinkCreator={this.permalinkCreator}
|
||||
lastSuccessful={false}
|
||||
isSelectedEvent={false}
|
||||
getRelationsForEvent={null}
|
||||
showReactions={false}
|
||||
layout={Layout.Group}
|
||||
showReadReceipts={false}
|
||||
/>
|
||||
</MatrixClientContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
protected async getEventTileMarkup(mxEv: MatrixEvent, continuation: boolean, filePath?: string) {
|
||||
|
@ -305,11 +298,8 @@ export default class HTMLExporter extends Exporter {
|
|||
) {
|
||||
// to linkify textual events, we'll need lifecycle methods which won't be invoked in renderToString
|
||||
// So, we'll have to render the component into a temporary root element
|
||||
const tempRoot = document.createElement('div');
|
||||
ReactDOM.render(
|
||||
EventTile,
|
||||
tempRoot,
|
||||
);
|
||||
const tempRoot = document.createElement("div");
|
||||
ReactDOM.render(EventTile, tempRoot);
|
||||
eventTileMarkup = tempRoot.innerHTML;
|
||||
} else {
|
||||
eventTileMarkup = renderToStaticMarkup(EventTile);
|
||||
|
@ -319,17 +309,17 @@ export default class HTMLExporter extends Exporter {
|
|||
const mxc = mxEv.getContent().url ?? mxEv.getContent().file?.url;
|
||||
eventTileMarkup = eventTileMarkup.split(mxc).join(filePath);
|
||||
}
|
||||
eventTileMarkup = eventTileMarkup.replace(/<span class="mx_MFileBody_info_icon".*?>.*?<\/span>/, '');
|
||||
eventTileMarkup = eventTileMarkup.replace(/<span class="mx_MFileBody_info_icon".*?>.*?<\/span>/, "");
|
||||
if (hasAvatar) {
|
||||
eventTileMarkup = eventTileMarkup.replace(
|
||||
encodeURI(this.getAvatarURL(mxEv)).replace(/&/g, '&'),
|
||||
encodeURI(this.getAvatarURL(mxEv)).replace(/&/g, "&"),
|
||||
`users/${mxEv.sender.userId.replace(/:/g, "-")}.png`,
|
||||
);
|
||||
}
|
||||
return eventTileMarkup;
|
||||
}
|
||||
|
||||
protected createModifiedEvent(text: string, mxEv: MatrixEvent, italic=true) {
|
||||
protected createModifiedEvent(text: string, mxEv: MatrixEvent, italic = true) {
|
||||
const modifiedContent = {
|
||||
msgtype: "m.text",
|
||||
body: `${text}`,
|
||||
|
@ -337,8 +327,8 @@ export default class HTMLExporter extends Exporter {
|
|||
formatted_body: `${text}`,
|
||||
};
|
||||
if (italic) {
|
||||
modifiedContent.formatted_body = '<em>' + modifiedContent.formatted_body + '</em>';
|
||||
modifiedContent.body = '*' + modifiedContent.body + '*';
|
||||
modifiedContent.formatted_body = "<em>" + modifiedContent.formatted_body + "</em>";
|
||||
modifiedContent.body = "*" + modifiedContent.body + "*";
|
||||
}
|
||||
const modifiedEvent = new MatrixEvent();
|
||||
modifiedEvent.event = mxEv.event;
|
||||
|
@ -402,15 +392,20 @@ export default class HTMLExporter extends Exporter {
|
|||
let prevEvent = null;
|
||||
for (let i = start; i < Math.min(start + 1000, events.length); i++) {
|
||||
const event = events[i];
|
||||
this.updateProgress(_t("Processing event %(number)s out of %(total)s", {
|
||||
number: i + 1,
|
||||
total: events.length,
|
||||
}), false, true);
|
||||
this.updateProgress(
|
||||
_t("Processing event %(number)s out of %(total)s", {
|
||||
number: i + 1,
|
||||
total: events.length,
|
||||
}),
|
||||
false,
|
||||
true,
|
||||
);
|
||||
if (this.cancelled) return this.cleanUp();
|
||||
if (!haveRendererForEvent(event, false)) continue;
|
||||
|
||||
content += this.needsDateSeparator(event, prevEvent) ? this.getDateSeparator(event) : "";
|
||||
const shouldBeJoined = !this.needsDateSeparator(event, prevEvent) &&
|
||||
const shouldBeJoined =
|
||||
!this.needsDateSeparator(event, prevEvent) &&
|
||||
shouldFormContinuation(prevEvent, event, false, this.threadsEnabled);
|
||||
const body = await this.createMessageBody(event, shouldBeJoined);
|
||||
this.totalSize += Buffer.byteLength(body);
|
||||
|
@ -427,10 +422,14 @@ export default class HTMLExporter extends Exporter {
|
|||
const res = await this.getRequiredEvents();
|
||||
const fetchEnd = performance.now();
|
||||
|
||||
this.updateProgress(_t("Fetched %(count)s events in %(seconds)ss", {
|
||||
count: res.length,
|
||||
seconds: (fetchEnd - fetchStart) / 1000,
|
||||
}), true, false);
|
||||
this.updateProgress(
|
||||
_t("Fetched %(count)s events in %(seconds)ss", {
|
||||
count: res.length,
|
||||
seconds: (fetchEnd - fetchStart) / 1000,
|
||||
}),
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
this.updateProgress(_t("Creating HTML..."));
|
||||
|
||||
|
@ -438,8 +437,8 @@ export default class HTMLExporter extends Exporter {
|
|||
for (let page = 0; page < res.length / 1000; page++) {
|
||||
const html = await this.createHTML(res, page * 1000);
|
||||
const document = new DOMParser().parseFromString(html, "text/html");
|
||||
document.querySelectorAll("*").forEach(element => {
|
||||
element.classList.forEach(c => usedClasses.add(c));
|
||||
document.querySelectorAll("*").forEach((element) => {
|
||||
element.classList.forEach((c) => usedClasses.add(c));
|
||||
});
|
||||
this.addFile(`messages${page ? page + 1 : ""}.html`, new Blob([html]));
|
||||
}
|
||||
|
@ -456,10 +455,12 @@ export default class HTMLExporter extends Exporter {
|
|||
logger.info("Export cancelled successfully");
|
||||
} else {
|
||||
this.updateProgress(_t("Export successful!"));
|
||||
this.updateProgress(_t("Exported %(count)s events in %(seconds)s seconds", {
|
||||
count: res.length,
|
||||
seconds: (exportEnd - fetchStart) / 1000,
|
||||
}));
|
||||
this.updateProgress(
|
||||
_t("Exported %(count)s events in %(seconds)s seconds", {
|
||||
count: res.length,
|
||||
seconds: (exportEnd - fetchStart) / 1000,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
this.cleanUp();
|
||||
|
|
|
@ -84,10 +84,14 @@ export default class JSONExporter extends Exporter {
|
|||
protected async createOutput(events: MatrixEvent[]) {
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
const event = events[i];
|
||||
this.updateProgress(_t("Processing event %(number)s out of %(total)s", {
|
||||
number: i + 1,
|
||||
total: events.length,
|
||||
}), false, true);
|
||||
this.updateProgress(
|
||||
_t("Processing event %(number)s out of %(total)s", {
|
||||
number: i + 1,
|
||||
total: events.length,
|
||||
}),
|
||||
false,
|
||||
true,
|
||||
);
|
||||
if (this.cancelled) return this.cleanUp();
|
||||
if (!haveRendererForEvent(event, false)) continue;
|
||||
this.messages.push(await this.getJSONString(event));
|
||||
|
@ -103,7 +107,7 @@ export default class JSONExporter extends Exporter {
|
|||
const res = await this.getRequiredEvents();
|
||||
const fetchEnd = performance.now();
|
||||
|
||||
logger.log(`Fetched ${res.length} events in ${(fetchEnd - fetchStart)/1000}s`);
|
||||
logger.log(`Fetched ${res.length} events in ${(fetchEnd - fetchStart) / 1000}s`);
|
||||
|
||||
logger.info("Creating output...");
|
||||
const text = await this.createOutput(res);
|
||||
|
@ -122,10 +126,9 @@ export default class JSONExporter extends Exporter {
|
|||
logger.info("Export cancelled successfully");
|
||||
} else {
|
||||
logger.info("Export successful!");
|
||||
logger.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`);
|
||||
logger.log(`Exported ${res.length} events in ${(exportEnd - fetchStart) / 1000} seconds`);
|
||||
}
|
||||
|
||||
this.cleanUp();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ export default class PlainTextExporter extends Exporter {
|
|||
|
||||
rplSource = match[2].substring(1);
|
||||
// Get the first non-blank line from the source.
|
||||
const lines = rplSource.split('\n').filter((line) => !/^\s*$/.test(line));
|
||||
const lines = rplSource.split("\n").filter((line) => !/^\s*$/.test(line));
|
||||
if (lines.length > 0) {
|
||||
// Cut to a maximum length.
|
||||
rplSource = lines[0].substring(0, REPLY_SOURCE_MAX_LENGTH);
|
||||
|
@ -111,10 +111,14 @@ export default class PlainTextExporter extends Exporter {
|
|||
let content = "";
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
const event = events[i];
|
||||
this.updateProgress(_t("Processing event %(number)s out of %(total)s", {
|
||||
number: i + 1,
|
||||
total: events.length,
|
||||
}), false, true);
|
||||
this.updateProgress(
|
||||
_t("Processing event %(number)s out of %(total)s", {
|
||||
number: i + 1,
|
||||
total: events.length,
|
||||
}),
|
||||
false,
|
||||
true,
|
||||
);
|
||||
if (this.cancelled) return this.cleanUp();
|
||||
if (!haveRendererForEvent(event, false)) continue;
|
||||
const textForEvent = await this.plainTextForEvent(event);
|
||||
|
@ -131,7 +135,7 @@ export default class PlainTextExporter extends Exporter {
|
|||
const res = await this.getRequiredEvents();
|
||||
const fetchEnd = performance.now();
|
||||
|
||||
logger.log(`Fetched ${res.length} events in ${(fetchEnd - fetchStart)/1000}s`);
|
||||
logger.log(`Fetched ${res.length} events in ${(fetchEnd - fetchStart) / 1000}s`);
|
||||
|
||||
this.updateProgress(_t("Creating output..."));
|
||||
const text = await this.createOutput(res);
|
||||
|
@ -150,10 +154,9 @@ export default class PlainTextExporter extends Exporter {
|
|||
logger.info("Export cancelled successfully");
|
||||
} else {
|
||||
logger.info("Export successful!");
|
||||
logger.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`);
|
||||
logger.log(`Exported ${res.length} events in ${(exportEnd - fetchStart) / 1000} seconds`);
|
||||
}
|
||||
|
||||
this.cleanUp();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ async function getRulesFromCssFile(path: string): Promise<CSSStyleSheet> {
|
|||
// doesn't cull rules which won't apply due to the full selector not matching but gets rid of a LOT of cruft anyway.
|
||||
const getExportCSS = async (usedClasses: Set<string>): Promise<string> => {
|
||||
// only include bundle.css and the data-mx-theme=light styling
|
||||
const stylesheets = Array.from(document.styleSheets).filter(s => {
|
||||
const stylesheets = Array.from(document.styleSheets).filter((s) => {
|
||||
return s.href?.endsWith("bundle.css") || isLightTheme(s);
|
||||
});
|
||||
|
||||
|
@ -70,12 +70,14 @@ const getExportCSS = async (usedClasses: Set<string>): Promise<string> => {
|
|||
const selectorText = (rule as CSSStyleRule).selectorText;
|
||||
|
||||
// only skip the rule if all branches (,) of the selector are redundant
|
||||
if (selectorText?.split(",").every(selector => {
|
||||
const classes = selector.match(cssSelectorTextClassesRegex);
|
||||
if (classes && !classes.every(c => usedClasses.has(c.substring(1)))) {
|
||||
return true; // signal as a redundant selector
|
||||
}
|
||||
})) {
|
||||
if (
|
||||
selectorText?.split(",").every((selector) => {
|
||||
const classes = selector.match(cssSelectorTextClassesRegex);
|
||||
if (classes && !classes.every((c) => usedClasses.has(c.substring(1)))) {
|
||||
return true; // signal as a redundant selector
|
||||
}
|
||||
})
|
||||
) {
|
||||
continue; // skip this rule as it is redundant
|
||||
}
|
||||
|
||||
|
|
|
@ -32,9 +32,8 @@ limitations under the License.
|
|||
bottom: 30px;
|
||||
font-size: 17px;
|
||||
padding: 6px 16px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir,
|
||||
segoe ui, helvetica neue, helvetica, Ubuntu, roboto, noto, arial,
|
||||
sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Ubuntu,
|
||||
roboto, noto, arial, sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.43;
|
||||
border-radius: 4px;
|
||||
|
@ -126,7 +125,6 @@ a.mx_reply_anchor:hover {
|
|||
|
||||
.mx_RedactedBody,
|
||||
.mx_HiddenBody {
|
||||
|
||||
padding-left: unset;
|
||||
}
|
||||
|
||||
|
|
|
@ -33,10 +33,9 @@ function showToast(text) {
|
|||
}
|
||||
|
||||
window.onload = () => {
|
||||
document.querySelectorAll('.mx_reply_anchor').forEach(element => {
|
||||
element.addEventListener('click', event => {
|
||||
document.querySelectorAll(".mx_reply_anchor").forEach((element) => {
|
||||
element.addEventListener("click", (event) => {
|
||||
showToastIfNeeded(event.target.dataset.scrollTo);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -36,7 +36,8 @@ export function humanizeTime(timeMillis: number): string {
|
|||
const hours = Math.ceil(minutes / 60);
|
||||
const days = Math.ceil(hours / 24);
|
||||
|
||||
if (msAgo >= 0) { // Past
|
||||
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 });
|
||||
|
@ -44,7 +45,8 @@ export function humanizeTime(timeMillis: number): string {
|
|||
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 });
|
||||
} else { // Future
|
||||
} 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");
|
||||
|
|
|
@ -95,7 +95,7 @@ export async function createThumbnail(
|
|||
if (window.OffscreenCanvas && canvas instanceof OffscreenCanvas) {
|
||||
thumbnailPromise = canvas.convertToBlob({ type: mimeType });
|
||||
} else {
|
||||
thumbnailPromise = new Promise<Blob>(resolve => (canvas as HTMLCanvasElement).toBlob(resolve, mimeType));
|
||||
thumbnailPromise = new Promise<Blob>((resolve) => (canvas as HTMLCanvasElement).toBlob(resolve, mimeType));
|
||||
}
|
||||
|
||||
const imageData = context.getImageData(0, 0, targetWidth, targetHeight);
|
||||
|
|
|
@ -20,6 +20,6 @@ export function iterableIntersection<T>(a: Iterable<T>, b: Iterable<T>): Iterabl
|
|||
return arrayIntersection(Array.from(a), Array.from(b));
|
||||
}
|
||||
|
||||
export function iterableDiff<T>(a: Iterable<T>, b: Iterable<T>): { added: Iterable<T>, removed: Iterable<T> } {
|
||||
export function iterableDiff<T>(a: Iterable<T>, b: Iterable<T>): { added: Iterable<T>; removed: Iterable<T> } {
|
||||
return arrayDiff(Array.from(a), Array.from(b));
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ import { SdkContextClass } from "../contexts/SDKContext";
|
|||
export async function leaveRoomBehaviour(roomId: string, retry = true, spinner = true) {
|
||||
let spinnerModal: IHandle<any>;
|
||||
if (spinner) {
|
||||
spinnerModal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
|
||||
spinnerModal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner");
|
||||
}
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -56,25 +56,33 @@ export async function leaveRoomBehaviour(roomId: string, retry = true, spinner =
|
|||
|
||||
const room = cli.getRoom(roomId);
|
||||
// await any queued messages being sent so that they do not fail
|
||||
await Promise.all(room.getPendingEvents().filter(ev => {
|
||||
return [EventStatus.QUEUED, EventStatus.ENCRYPTING, EventStatus.SENDING].includes(ev.status);
|
||||
}).map(ev => new Promise<void>((resolve, reject) => {
|
||||
const handler = () => {
|
||||
if (ev.status === EventStatus.NOT_SENT) {
|
||||
spinnerModal?.close();
|
||||
reject(ev.error);
|
||||
}
|
||||
await Promise.all(
|
||||
room
|
||||
.getPendingEvents()
|
||||
.filter((ev) => {
|
||||
return [EventStatus.QUEUED, EventStatus.ENCRYPTING, EventStatus.SENDING].includes(ev.status);
|
||||
})
|
||||
.map(
|
||||
(ev) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const handler = () => {
|
||||
if (ev.status === EventStatus.NOT_SENT) {
|
||||
spinnerModal?.close();
|
||||
reject(ev.error);
|
||||
}
|
||||
|
||||
if (!ev.status || ev.status === EventStatus.SENT) {
|
||||
ev.off(MatrixEventEvent.Status, handler);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
if (!ev.status || ev.status === EventStatus.SENT) {
|
||||
ev.off(MatrixEventEvent.Status, handler);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
ev.on(MatrixEventEvent.Status, handler);
|
||||
})));
|
||||
ev.on(MatrixEventEvent.Status, handler);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
let results: { [roomId: string]: Error & { errcode?: string, message: string, data?: Record<string, any> } } = {};
|
||||
let results: { [roomId: string]: Error & { errcode?: string; message: string; data?: Record<string, any> } } = {};
|
||||
if (!leavingAllVersions) {
|
||||
try {
|
||||
await cli.leave(roomId);
|
||||
|
@ -91,7 +99,7 @@ export async function leaveRoomBehaviour(roomId: string, retry = true, spinner =
|
|||
}
|
||||
|
||||
if (retry) {
|
||||
const limitExceededError = Object.values(results).find(e => e?.errcode === "M_LIMIT_EXCEEDED");
|
||||
const limitExceededError = Object.values(results).find((e) => e?.errcode === "M_LIMIT_EXCEEDED");
|
||||
if (limitExceededError) {
|
||||
await sleep(limitExceededError.data.retry_after_ms ?? 100);
|
||||
return leaveRoomBehaviour(roomId, false, false);
|
||||
|
@ -100,26 +108,26 @@ export async function leaveRoomBehaviour(roomId: string, retry = true, spinner =
|
|||
|
||||
spinnerModal?.close();
|
||||
|
||||
const errors = Object.entries(results).filter(r => !!r[1]);
|
||||
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') {
|
||||
if (err.errcode === "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM") {
|
||||
Modal.createDialog(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.",
|
||||
"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
|
||||
messages.push(message, React.createElement("BR")); // createElement to avoid using a tsx file in utils
|
||||
}
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("Error leaving room"),
|
||||
|
@ -158,16 +166,20 @@ export async function leaveRoomBehaviour(roomId: string, retry = true, spinner =
|
|||
}
|
||||
|
||||
export const leaveSpace = (space: Room) => {
|
||||
Modal.createDialog(LeaveSpaceDialog, {
|
||||
space,
|
||||
onFinished: async (leave: boolean, rooms: Room[]) => {
|
||||
if (!leave) return;
|
||||
await bulkSpaceBehaviour(space, rooms, room => leaveRoomBehaviour(room.roomId));
|
||||
Modal.createDialog(
|
||||
LeaveSpaceDialog,
|
||||
{
|
||||
space,
|
||||
onFinished: async (leave: boolean, rooms: Room[]) => {
|
||||
if (!leave) return;
|
||||
await bulkSpaceBehaviour(space, rooms, (room) => leaveRoomBehaviour(room.roomId));
|
||||
|
||||
dis.dispatch<AfterLeaveRoomPayload>({
|
||||
action: Action.AfterLeaveRoom,
|
||||
room_id: space.roomId,
|
||||
});
|
||||
dis.dispatch<AfterLeaveRoomPayload>({
|
||||
action: Action.AfterLeaveRoom,
|
||||
room_id: space.roomId,
|
||||
});
|
||||
},
|
||||
},
|
||||
}, "mx_LeaveSpaceDialog_wrapper");
|
||||
"mx_LeaveSpaceDialog_wrapper",
|
||||
);
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/matrix";
|
|||
|
||||
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../models/LocalRoom";
|
||||
|
||||
export function isLocalRoom(roomOrID: Room|string): boolean {
|
||||
export function isLocalRoom(roomOrID: Room | string): boolean {
|
||||
if (typeof roomOrID === "string") {
|
||||
return roomOrID.startsWith(LOCAL_ROOM_ID_PREFIX);
|
||||
}
|
||||
|
|
|
@ -21,10 +21,7 @@ import { LocalRoom } from "../../models/LocalRoom";
|
|||
/**
|
||||
* Tests whether a room created based on a local room is ready.
|
||||
*/
|
||||
export function isRoomReady(
|
||||
client: MatrixClient,
|
||||
localRoom: LocalRoom,
|
||||
): boolean {
|
||||
export function isRoomReady(client: MatrixClient, localRoom: LocalRoom): boolean {
|
||||
// not ready if no actual room id exists
|
||||
if (!localRoom.actualRoomId) return false;
|
||||
|
||||
|
|
|
@ -17,18 +17,20 @@ limitations under the License.
|
|||
import { _t } from "../../languageHandler";
|
||||
|
||||
export enum LocationShareError {
|
||||
MapStyleUrlNotConfigured = 'MapStyleUrlNotConfigured',
|
||||
MapStyleUrlNotReachable = 'MapStyleUrlNotReachable',
|
||||
Default = 'Default'
|
||||
MapStyleUrlNotConfigured = "MapStyleUrlNotConfigured",
|
||||
MapStyleUrlNotReachable = "MapStyleUrlNotReachable",
|
||||
Default = "Default",
|
||||
}
|
||||
|
||||
export const getLocationShareErrorMessage = (errorType?: LocationShareError): string => {
|
||||
switch (errorType) {
|
||||
case LocationShareError.MapStyleUrlNotConfigured:
|
||||
return _t('This homeserver is not configured to display maps.');
|
||||
return _t("This homeserver is not configured to display maps.");
|
||||
case LocationShareError.MapStyleUrlNotReachable:
|
||||
default:
|
||||
return _t(`This homeserver is not configured correctly to display maps, `
|
||||
+ `or the configured map server may be unreachable.`);
|
||||
return _t(
|
||||
`This homeserver is not configured correctly to display maps, ` +
|
||||
`or the configured map server may be unreachable.`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -26,14 +26,12 @@ import { LocationShareError } from "./LocationShareErrors";
|
|||
* that, defaults to the same tile server listed by matrix.org.
|
||||
*/
|
||||
export function findMapStyleUrl(): string {
|
||||
const mapStyleUrl = (
|
||||
getTileServerWellKnown()?.map_style_url ??
|
||||
SdkConfig.get().map_style_url
|
||||
);
|
||||
const mapStyleUrl = getTileServerWellKnown()?.map_style_url ?? SdkConfig.get().map_style_url;
|
||||
|
||||
if (!mapStyleUrl) {
|
||||
logger.error("'map_style_url' missing from homeserver .well-known area, and " +
|
||||
"missing from from config.json.");
|
||||
logger.error(
|
||||
"'map_style_url' missing from homeserver .well-known area, and " + "missing from from config.json.",
|
||||
);
|
||||
throw new Error(LocationShareError.MapStyleUrlNotConfigured);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './findMapStyleUrl';
|
||||
export * from './isSelfLocation';
|
||||
export * from './locationEventGeoUri';
|
||||
export * from './LocationShareErrors';
|
||||
export * from './map';
|
||||
export * from './parseGeoUri';
|
||||
export * from "./findMapStyleUrl";
|
||||
export * from "./isSelfLocation";
|
||||
export * from "./locationEventGeoUri";
|
||||
export * from "./LocationShareErrors";
|
||||
export * from "./map";
|
||||
export * from "./parseGeoUri";
|
||||
|
|
|
@ -27,5 +27,5 @@ export const locationEventGeoUri = (mxEvent: MatrixEvent): string => {
|
|||
// https://github.com/matrix-org/matrix-doc/issues/3516
|
||||
const content = mxEvent.getContent();
|
||||
const loc = M_LOCATION.findIn(content) as { uri?: string };
|
||||
return loc ? loc.uri : content['geo_uri'];
|
||||
return loc ? loc.uri : content["geo_uri"];
|
||||
};
|
||||
|
|
|
@ -24,11 +24,7 @@ import { parseGeoUri } from "./parseGeoUri";
|
|||
import { findMapStyleUrl } from "./findMapStyleUrl";
|
||||
import { LocationShareError } from "./LocationShareErrors";
|
||||
|
||||
export const createMap = (
|
||||
interactive: boolean,
|
||||
bodyId: string,
|
||||
onError: (error: Error) => void,
|
||||
): maplibregl.Map => {
|
||||
export const createMap = (interactive: boolean, bodyId: string, onError: (error: Error) => void): maplibregl.Map => {
|
||||
try {
|
||||
const styleUrl = findMapStyleUrl();
|
||||
|
||||
|
@ -39,24 +35,23 @@ export const createMap = (
|
|||
interactive,
|
||||
attributionControl: false,
|
||||
locale: {
|
||||
'AttributionControl.ToggleAttribution': _t('Toggle attribution'),
|
||||
'AttributionControl.MapFeedback': _t('Map feedback'),
|
||||
'FullscreenControl.Enter': _t('Enter fullscreen'),
|
||||
'FullscreenControl.Exit': _t('Exit fullscreen'),
|
||||
'GeolocateControl.FindMyLocation': _t('Find my location'),
|
||||
'GeolocateControl.LocationNotAvailable': _t('Location not available'),
|
||||
'LogoControl.Title': _t('Mapbox logo'),
|
||||
'NavigationControl.ResetBearing': _t('Reset bearing to north'),
|
||||
'NavigationControl.ZoomIn': _t('Zoom in'),
|
||||
'NavigationControl.ZoomOut': _t('Zoom out'),
|
||||
"AttributionControl.ToggleAttribution": _t("Toggle attribution"),
|
||||
"AttributionControl.MapFeedback": _t("Map feedback"),
|
||||
"FullscreenControl.Enter": _t("Enter fullscreen"),
|
||||
"FullscreenControl.Exit": _t("Exit fullscreen"),
|
||||
"GeolocateControl.FindMyLocation": _t("Find my location"),
|
||||
"GeolocateControl.LocationNotAvailable": _t("Location not available"),
|
||||
"LogoControl.Title": _t("Mapbox logo"),
|
||||
"NavigationControl.ResetBearing": _t("Reset bearing to north"),
|
||||
"NavigationControl.ZoomIn": _t("Zoom in"),
|
||||
"NavigationControl.ZoomOut": _t("Zoom out"),
|
||||
},
|
||||
});
|
||||
map.addControl(new maplibregl.AttributionControl(), 'top-right');
|
||||
map.addControl(new maplibregl.AttributionControl(), "top-right");
|
||||
|
||||
map.on('error', (e) => {
|
||||
map.on("error", (e) => {
|
||||
logger.error(
|
||||
"Failed to load map: check map_style_url in config.json has a "
|
||||
+ "valid URL and API key",
|
||||
"Failed to load map: check map_style_url in config.json has a " + "valid URL and API key",
|
||||
e.error,
|
||||
);
|
||||
onError(new Error(LocationShareError.MapStyleUrlNotReachable));
|
||||
|
@ -72,7 +67,7 @@ export const createMap = (
|
|||
export const createMarker = (coords: GeolocationCoordinates, element: HTMLElement): maplibregl.Marker => {
|
||||
const marker = new maplibregl.Marker({
|
||||
element,
|
||||
anchor: 'bottom',
|
||||
anchor: "bottom",
|
||||
offset: [0, -1],
|
||||
}).setLngLat({ lon: coords.longitude, lat: coords.latitude });
|
||||
return marker;
|
||||
|
|
|
@ -26,8 +26,8 @@ export const parseGeoUri = (uri: string): GeolocationCoordinates => {
|
|||
|
||||
const m = uri.match(/^\s*geo:(.*?)\s*$/);
|
||||
if (!m) return;
|
||||
const parts = m[1].split(';');
|
||||
const coords = parts[0].split(',');
|
||||
const parts = m[1].split(";");
|
||||
const coords = parts[0].split(",");
|
||||
let uncertainty: number;
|
||||
for (const param of parts.slice(1)) {
|
||||
const m = param.match(/u=(.*)/);
|
||||
|
|
|
@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Map as MapLibreMap } from 'maplibre-gl';
|
||||
import { useEffect, useState } from "react";
|
||||
import { Map as MapLibreMap } from "maplibre-gl";
|
||||
|
||||
import { createMap } from "./map";
|
||||
|
||||
|
@ -31,11 +31,7 @@ interface UseMapProps {
|
|||
* Make sure `onError` has a stable reference
|
||||
* As map is recreated on changes to it
|
||||
*/
|
||||
export const useMap = ({
|
||||
interactive,
|
||||
bodyId,
|
||||
onError,
|
||||
}: UseMapProps): MapLibreMap | undefined => {
|
||||
export const useMap = ({ interactive, bodyId, onError }: UseMapProps): MapLibreMap | undefined => {
|
||||
const [map, setMap] = useState<MapLibreMap>();
|
||||
|
||||
useEffect(
|
||||
|
@ -59,4 +55,3 @@ export const useMap = ({
|
|||
|
||||
return map;
|
||||
};
|
||||
|
||||
|
|
|
@ -23,12 +23,12 @@ import { arrayDiff, arrayIntersection } from "./arrays";
|
|||
* @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[] } {
|
||||
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 = arrayIntersection(aKeys, bKeys);
|
||||
const changes = possibleChanges.filter(k => a.get(k) !== b.get(k));
|
||||
const changes = possibleChanges.filter((k) => a.get(k) !== b.get(k));
|
||||
|
||||
return { changed: changes, added: keyDiff.added, removed: keyDiff.removed };
|
||||
}
|
||||
|
|
|
@ -47,11 +47,8 @@ export const requestMediaPermissions = async (video = true): Promise<MediaStream
|
|||
logger.log("Failed to list userMedia devices", error);
|
||||
const brand = SdkConfig.get().brand;
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t('No media permissions'),
|
||||
description: _t(
|
||||
'You may need to manually permit %(brand)s to access your microphone/webcam',
|
||||
{ brand },
|
||||
),
|
||||
title: _t("No media permissions"),
|
||||
description: _t("You may need to manually permit %(brand)s to access your microphone/webcam", { brand }),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -63,9 +63,9 @@ export function splitRoomsByMembership(rooms: Room[]): MembershipSplit {
|
|||
}
|
||||
|
||||
export function getEffectiveMembership(membership: string): EffectiveMembership {
|
||||
if (membership === 'invite') {
|
||||
if (membership === "invite") {
|
||||
return EffectiveMembership.Invite;
|
||||
} else if (membership === 'join') {
|
||||
} 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 {
|
||||
|
@ -88,7 +88,8 @@ export async function waitForMember(client: MatrixClient, roomId: string, userId
|
|||
const { timeout } = opts;
|
||||
let handler;
|
||||
return new Promise((resolve) => {
|
||||
handler = function(_, __, member: RoomMember) { // eslint-disable-line @typescript-eslint/naming-convention
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
handler = function (_, __, member: RoomMember) {
|
||||
if (member.userId !== userId) return;
|
||||
if (member.roomId !== roomId) return;
|
||||
resolve(true);
|
||||
|
|
|
@ -45,7 +45,7 @@ export async function createLocalNotificationSettingsIfNeeded(cli: MatrixClient)
|
|||
if (!event) {
|
||||
// If any of the above is true, we fall in the "backwards compat" case,
|
||||
// and `is_silenced` will be set to `false`
|
||||
const isSilenced = !deviceNotificationSettingsKeys.some(key => SettingsStore.getValue(key));
|
||||
const isSilenced = !deviceNotificationSettingsKeys.some((key) => SettingsStore.getValue(key));
|
||||
|
||||
await cli.setAccountData(eventType, {
|
||||
is_silenced: isSilenced,
|
||||
|
@ -68,9 +68,10 @@ export function clearAllNotifications(client: MatrixClient): Promise<Array<{}>>
|
|||
const lastRoomEvent = roomEvents?.[roomEvents?.length - 1];
|
||||
const lastThreadLastEvent = lastThreadEvents?.[lastThreadEvents?.length - 1];
|
||||
|
||||
const lastEvent = (lastRoomEvent?.getTs() ?? 0) > (lastThreadLastEvent?.getTs() ?? 0)
|
||||
? lastRoomEvent
|
||||
: lastThreadLastEvent;
|
||||
const lastEvent =
|
||||
(lastRoomEvent?.getTs() ?? 0) > (lastThreadLastEvent?.getTs() ?? 0)
|
||||
? lastRoomEvent
|
||||
: lastThreadLastEvent;
|
||||
|
||||
if (lastEvent) {
|
||||
const receiptType = SettingsStore.getValue("sendReadReceipts", room.roomId)
|
||||
|
|
|
@ -34,7 +34,7 @@ export function sum(...i: number[]): number {
|
|||
}
|
||||
|
||||
export function percentageWithin(pct: number, min: number, max: number): number {
|
||||
return (pct * (max - min)) + min;
|
||||
return pct * (max - min) + min;
|
||||
}
|
||||
|
||||
export function percentageOf(val: number, min: number, max: number): number {
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import { arrayDiff, arrayUnion, arrayIntersection } from "./arrays";
|
||||
|
||||
type ObjectExcluding<O extends {}, P extends (keyof O)[]> = {[k in Exclude<keyof O, P[number]>]: O[k]};
|
||||
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.
|
||||
|
@ -45,13 +45,13 @@ export function objectExcluding<O extends {}, P extends Array<keyof O>>(a: O, pr
|
|||
* @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]} {
|
||||
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]};
|
||||
return objectExcluding(a, diff.removed) as { [k in P[number]]: O[k] };
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,10 +94,10 @@ export function objectHasDiff<O extends {}>(a: O, b: O): boolean {
|
|||
// 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]);
|
||||
return possibleChanges.some((k) => a[k] !== b[k]);
|
||||
}
|
||||
|
||||
type Diff<K> = { changed: K[], added: K[], removed: K[] };
|
||||
type Diff<K> = { changed: K[]; added: K[]; removed: K[] };
|
||||
|
||||
/**
|
||||
* Determines the keys added, changed, and removed between two objects.
|
||||
|
@ -112,7 +112,7 @@ export function objectDiff<O extends {}>(a: O, b: O): Diff<keyof O> {
|
|||
const bKeys = Object.keys(b) as (keyof O)[];
|
||||
const keyDiff = arrayDiff(aKeys, bKeys);
|
||||
const possibleChanges = arrayIntersection(aKeys, bKeys);
|
||||
const changes = possibleChanges.filter(k => a[k] !== b[k]);
|
||||
const changes = possibleChanges.filter((k) => a[k] !== b[k]);
|
||||
|
||||
return { changed: changes, added: keyDiff.added, removed: keyDiff.removed };
|
||||
}
|
||||
|
|
|
@ -17,14 +17,14 @@ limitations under the License.
|
|||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { IConfigOptions } from "../IConfigOptions";
|
||||
import { getEmbeddedPagesWellKnown } from '../utils/WellKnownUtils';
|
||||
import { getEmbeddedPagesWellKnown } from "../utils/WellKnownUtils";
|
||||
import { SnakedObject } from "./SnakedObject";
|
||||
|
||||
export function getHomePageUrl(appConfig: IConfigOptions): string | null {
|
||||
const config = new SnakedObject(appConfig);
|
||||
|
||||
const pagesConfig = config.get("embedded_pages");
|
||||
let pageUrl = pagesConfig ? (new SnakedObject(pagesConfig).get("home_url")) : null;
|
||||
let pageUrl = pagesConfig ? new SnakedObject(pagesConfig).get("home_url") : null;
|
||||
|
||||
if (!pageUrl) {
|
||||
// This is a deprecated config option for the home page
|
||||
|
@ -34,7 +34,7 @@ export function getHomePageUrl(appConfig: IConfigOptions): string | null {
|
|||
if (pageUrl) {
|
||||
logger.warn(
|
||||
"You are using a deprecated config option: `welcomePageUrl`. Please use " +
|
||||
"`embedded_pages.home_url` instead, per https://github.com/vector-im/element-web/issues/21428",
|
||||
"`embedded_pages.home_url` instead, per https://github.com/vector-im/element-web/issues/21428",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,5 @@ export function getHomePageUrl(appConfig: IConfigOptions): string | null {
|
|||
export function shouldUseLoginForWelcome(appConfig: IConfigOptions): boolean {
|
||||
const config = new SnakedObject(appConfig);
|
||||
const pagesConfig = config.get("embedded_pages");
|
||||
return pagesConfig
|
||||
? ((new SnakedObject(pagesConfig).get("login_for_welcome")) === true)
|
||||
: false;
|
||||
return pagesConfig ? new SnakedObject(pagesConfig).get("login_for_welcome") === true : false;
|
||||
}
|
||||
|
|
|
@ -44,9 +44,9 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor {
|
|||
}
|
||||
|
||||
forEntity(entityId: string): string {
|
||||
if (entityId[0] === '!' || entityId[0] === '#') {
|
||||
if (entityId[0] === "!" || entityId[0] === "#") {
|
||||
return this.forRoom(entityId);
|
||||
} else if (entityId[0] === '@') {
|
||||
} else if (entityId[0] === "@") {
|
||||
return this.forUser(entityId);
|
||||
} else throw new Error("Unrecognized entity");
|
||||
}
|
||||
|
@ -57,8 +57,8 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor {
|
|||
}
|
||||
|
||||
encodeServerCandidates(candidates?: string[]) {
|
||||
if (!candidates || candidates.length === 0) return '';
|
||||
return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
|
||||
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):
|
||||
|
@ -82,7 +82,8 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor {
|
|||
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
|
||||
if (parts.length < 2) {
|
||||
// we're expecting an entity and an ID of some kind at least
|
||||
throw new Error("URL is missing parts");
|
||||
}
|
||||
|
||||
|
@ -93,13 +94,13 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor {
|
|||
|
||||
const entityType = parts[0];
|
||||
const entity = parts[1];
|
||||
if (entityType === 'user') {
|
||||
if (entityType === "user") {
|
||||
// Probably a user, no further parsing needed.
|
||||
return PermalinkParts.forUser(entity);
|
||||
} else if (entityType === 'room') {
|
||||
} else if (entityType === "room") {
|
||||
// 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);
|
||||
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");
|
||||
|
|
|
@ -39,8 +39,10 @@ export default class MatrixSchemePermalinkConstructor extends PermalinkConstruct
|
|||
}
|
||||
|
||||
forEvent(roomId: string, eventId: string, serverCandidates: string[]): string {
|
||||
return `matrix:${this.encodeEntity(roomId)}` +
|
||||
`/${this.encodeEntity(eventId)}${this.encodeServerCandidates(serverCandidates)}`;
|
||||
return (
|
||||
`matrix:${this.encodeEntity(roomId)}` +
|
||||
`/${this.encodeEntity(eventId)}${this.encodeServerCandidates(serverCandidates)}`
|
||||
);
|
||||
}
|
||||
|
||||
forRoom(roomIdOrAlias: string, serverCandidates: string[]): string {
|
||||
|
@ -61,8 +63,8 @@ export default class MatrixSchemePermalinkConstructor extends PermalinkConstruct
|
|||
}
|
||||
|
||||
encodeServerCandidates(candidates: string[]) {
|
||||
if (!candidates || candidates.length === 0) return '';
|
||||
return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
|
||||
if (!candidates || candidates.length === 0) return "";
|
||||
return `?via=${candidates.map((c) => encodeURIComponent(c)).join("&via=")}`;
|
||||
}
|
||||
|
||||
parsePermalink(fullUrl: string): PermalinkParts {
|
||||
|
@ -70,26 +72,28 @@ export default class MatrixSchemePermalinkConstructor extends PermalinkConstruct
|
|||
throw new Error("Does not appear to be a permalink");
|
||||
}
|
||||
|
||||
const parts = fullUrl.substring("matrix:".length).split('/');
|
||||
const parts = fullUrl.substring("matrix:".length).split("/");
|
||||
|
||||
const identifier = parts[0];
|
||||
const entityNoSigil = parts[1];
|
||||
if (identifier === 'u') {
|
||||
if (identifier === "u") {
|
||||
// Probably a user, no further parsing needed.
|
||||
return PermalinkParts.forUser(`@${entityNoSigil}`);
|
||||
} else if (identifier === 'r' || identifier === 'roomid') {
|
||||
const sigil = identifier === 'r' ? '#' : '!';
|
||||
} else if (identifier === "r" || identifier === "roomid") {
|
||||
const sigil = identifier === "r" ? "#" : "!";
|
||||
|
||||
if (parts.length === 2) { // room without event permalink
|
||||
if (parts.length === 2) {
|
||||
// room without event permalink
|
||||
const [roomId, query = ""] = entityNoSigil.split("?");
|
||||
const via = query.split(/&?via=/g).filter(p => !!p);
|
||||
const via = query.split(/&?via=/g).filter((p) => !!p);
|
||||
return PermalinkParts.forRoom(`${sigil}${roomId}`, via);
|
||||
}
|
||||
|
||||
if (parts[2] === 'e') { // event permalink
|
||||
const eventIdAndQuery = parts.length > 3 ? parts.slice(3).join('/') : "";
|
||||
if (parts[2] === "e") {
|
||||
// event permalink
|
||||
const eventIdAndQuery = parts.length > 3 ? parts.slice(3).join("/") : "";
|
||||
const [eventId, query = ""] = eventIdAndQuery.split("?");
|
||||
const via = query.split(/&?via=/g).filter(p => !!p);
|
||||
const via = query.split(/&?via=/g).filter((p) => !!p);
|
||||
return PermalinkParts.forEvent(`${sigil}${entityNoSigil}`, `$${eventId}`, via);
|
||||
}
|
||||
|
||||
|
|
|
@ -48,8 +48,8 @@ export default class MatrixToPermalinkConstructor extends PermalinkConstructor {
|
|||
}
|
||||
|
||||
encodeServerCandidates(candidates: string[]) {
|
||||
if (!candidates || candidates.length === 0) return '';
|
||||
return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
|
||||
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):
|
||||
|
@ -62,20 +62,21 @@ export default class MatrixToPermalinkConstructor extends PermalinkConstructor {
|
|||
const parts = fullUrl.substring(`${baseUrl}/#/`.length).split("/");
|
||||
|
||||
const entity = parts[0];
|
||||
if (entity[0] === '@') {
|
||||
if (entity[0] === "@") {
|
||||
// Probably a user, no further parsing needed.
|
||||
return PermalinkParts.forUser(entity);
|
||||
} else if (entity[0] === '#' || entity[0] === '!') {
|
||||
if (parts.length === 1) { // room without event permalink
|
||||
const [roomId, query=""] = entity.split("?");
|
||||
const via = query.split(/&?via=/g).filter(p => !!p);
|
||||
} else if (entity[0] === "#" || entity[0] === "!") {
|
||||
if (parts.length === 1) {
|
||||
// room without event permalink
|
||||
const [roomId, query = ""] = entity.split("?");
|
||||
const via = query.split(/&?via=/g).filter((p) => !!p);
|
||||
return PermalinkParts.forRoom(roomId, via);
|
||||
}
|
||||
|
||||
// rejoin the rest because v3 events can have slashes (annoyingly)
|
||||
const eventIdAndQuery = parts.length > 1 ? parts.slice(1).join('/') : "";
|
||||
const [eventId, query=""] = eventIdAndQuery.split("?");
|
||||
const via = query.split(/&?via=/g).filter(p => !!p);
|
||||
const eventIdAndQuery = parts.length > 1 ? parts.slice(1).join("/") : "";
|
||||
const [eventId, query = ""] = eventIdAndQuery.split("?");
|
||||
const via = query.split(/&?via=/g).filter((p) => !!p);
|
||||
|
||||
return PermalinkParts.forEvent(entity, eventId, via);
|
||||
} else {
|
||||
|
|
|
@ -191,13 +191,18 @@ export class RoomPermalinkCreator {
|
|||
const serverName = getServerName(userId);
|
||||
|
||||
const domain = getHostnameFromMatrixServerName(serverName) ?? serverName;
|
||||
return !isHostnameIpAddress(domain) &&
|
||||
return (
|
||||
!isHostnameIpAddress(domain) &&
|
||||
!isHostInRegex(domain, this.bannedHostsRegexps) &&
|
||||
isHostInRegex(domain, this.allowedHostsRegexps);
|
||||
isHostInRegex(domain, this.allowedHostsRegexps)
|
||||
);
|
||||
});
|
||||
const maxEntry = allowedEntries.reduce((max, entry) => {
|
||||
return (entry[1] > max[1]) ? entry : max;
|
||||
}, [null, 0]);
|
||||
const maxEntry = allowedEntries.reduce(
|
||||
(max, entry) => {
|
||||
return entry[1] > max[1] ? entry : max;
|
||||
},
|
||||
[null, 0],
|
||||
);
|
||||
const [userId, powerLevel] = maxEntry;
|
||||
// object wasn't empty, and max entry wasn't a demotion from the default
|
||||
if (userId !== null && powerLevel >= 50) {
|
||||
|
@ -219,11 +224,11 @@ export class RoomPermalinkCreator {
|
|||
const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$");
|
||||
|
||||
const denied = aclEvent.getContent().deny || [];
|
||||
denied.forEach(h => bannedHostsRegexps.push(getRegex(h)));
|
||||
denied.forEach((h) => bannedHostsRegexps.push(getRegex(h)));
|
||||
|
||||
const allowed = aclEvent.getContent().allow || [];
|
||||
allowedHostsRegexps = []; // we don't want to use the default rule here
|
||||
allowed.forEach(h => allowedHostsRegexps.push(getRegex(h)));
|
||||
allowed.forEach((h) => allowedHostsRegexps.push(getRegex(h)));
|
||||
}
|
||||
}
|
||||
this.bannedHostsRegexps = bannedHostsRegexps;
|
||||
|
@ -248,8 +253,9 @@ export class RoomPermalinkCreator {
|
|||
candidates.add(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],
|
||||
);
|
||||
|
||||
for (let i = 0; i < serversByPopulation.length && candidates.size < MAX_SERVER_CANDIDATES; i++) {
|
||||
const serverName = serversByPopulation[i];
|
||||
|
@ -283,7 +289,7 @@ export function makeRoomPermalink(roomId: string): string {
|
|||
|
||||
// If the roomId isn't actually a room ID, don't try to list the servers.
|
||||
// Aliases are already routable, and don't need extra information.
|
||||
if (roomId[0] !== '!') return getPermalinkConstructor().forRoom(roomId, []);
|
||||
if (roomId[0] !== "!") return getPermalinkConstructor().forRoom(roomId, []);
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(roomId);
|
||||
|
@ -313,15 +319,15 @@ export function tryTransformEntityToPermalink(entity: string): string {
|
|||
if (!entity) return null;
|
||||
|
||||
// Check to see if it is a bare entity for starters
|
||||
if (entity[0] === '#' || entity[0] === '!') return makeRoomPermalink(entity);
|
||||
if (entity[0] === '@') return makeUserPermalink(entity);
|
||||
if (entity[0] === "#" || entity[0] === "!") return makeRoomPermalink(entity);
|
||||
if (entity[0] === "@") return makeUserPermalink(entity);
|
||||
|
||||
if (entity.slice(0, 7) === "matrix:") {
|
||||
try {
|
||||
const permalinkParts = parsePermalink(entity);
|
||||
if (permalinkParts) {
|
||||
if (permalinkParts.roomIdOrAlias) {
|
||||
const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : '';
|
||||
const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : "";
|
||||
let pl = matrixtoBaseUrl + `/#/${permalinkParts.roomIdOrAlias}${eventIdPart}`;
|
||||
if (permalinkParts.viaServers.length > 0) {
|
||||
pl += new MatrixToPermalinkConstructor().encodeServerCandidates(permalinkParts.viaServers);
|
||||
|
@ -344,7 +350,8 @@ export function tryTransformEntityToPermalink(entity: string): string {
|
|||
* @returns {string} The transformed permalink or original URL if unable.
|
||||
*/
|
||||
export function tryTransformPermalinkToLocalHref(permalink: string): string {
|
||||
if (!permalink.startsWith("http:") &&
|
||||
if (
|
||||
!permalink.startsWith("http:") &&
|
||||
!permalink.startsWith("https:") &&
|
||||
!permalink.startsWith("matrix:") &&
|
||||
!permalink.startsWith("vector:") // Element Desktop
|
||||
|
@ -367,7 +374,7 @@ export function tryTransformPermalinkToLocalHref(permalink: string): string {
|
|||
const permalinkParts = parsePermalink(permalink);
|
||||
if (permalinkParts) {
|
||||
if (permalinkParts.roomIdOrAlias) {
|
||||
const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : '';
|
||||
const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : "";
|
||||
permalink = `#/room/${permalinkParts.roomIdOrAlias}${eventIdPart}`;
|
||||
if (permalinkParts.viaServers.length > 0) {
|
||||
permalink += new MatrixToPermalinkConstructor().encodeServerCandidates(permalinkParts.viaServers);
|
||||
|
@ -393,7 +400,7 @@ export function getPrimaryPermalinkEntity(permalink: string): string {
|
|||
if (m) {
|
||||
// A bit of a hack, but it gets the job done
|
||||
const handler = new ElementPermalinkConstructor("http://localhost");
|
||||
const entityInfo = m[1].split('#').slice(1).join('#');
|
||||
const entityInfo = m[1].split("#").slice(1).join("#");
|
||||
permalinkParts = handler.parsePermalink(`http://localhost/#${entityInfo}`);
|
||||
}
|
||||
}
|
||||
|
@ -452,7 +459,7 @@ function isHostInRegex(hostname: string, regexps: RegExp[]): boolean {
|
|||
if (!hostname) return true; // assumed
|
||||
if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0].toString());
|
||||
|
||||
return regexps.some(h => h.test(hostname));
|
||||
return regexps.some((h) => h.test(hostname));
|
||||
}
|
||||
|
||||
function isHostnameIpAddress(hostname: string): boolean {
|
||||
|
|
|
@ -23,7 +23,8 @@ import { tryTransformPermalinkToLocalHref } from "./Permalinks";
|
|||
*/
|
||||
export function navigateToPermalink(uri: string): void {
|
||||
const localUri = tryTransformPermalinkToLocalHref(uri);
|
||||
if (!localUri || localUri === uri) { // parse failure can lead to an unmodified URL
|
||||
if (!localUri || localUri === uri) {
|
||||
// parse failure can lead to an unmodified URL
|
||||
throw new Error("Failed to transform URI");
|
||||
}
|
||||
window.location.hash = localUri; // it'll just be a fragment
|
||||
|
|
|
@ -15,11 +15,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import ReactDOM from 'react-dom';
|
||||
import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor';
|
||||
import ReactDOM from "react-dom";
|
||||
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import Pill, { PillType } from "../components/views/elements/Pill";
|
||||
import { parsePermalink } from "./permalinks/Permalinks";
|
||||
|
@ -54,14 +54,11 @@ export function pillifyLinks(nodes: ArrayLike<Element>, mxEvent: MatrixEvent, pi
|
|||
// If the link is a (localised) matrix.to link, replace it with a pill
|
||||
// We don't want to pill event permalinks, so those are ignored.
|
||||
if (parts && !parts.eventId) {
|
||||
const pillContainer = document.createElement('span');
|
||||
const pillContainer = document.createElement("span");
|
||||
|
||||
const pill = <Pill
|
||||
url={href}
|
||||
inMessage={true}
|
||||
room={room}
|
||||
shouldShowPillAvatar={shouldShowPillAvatar}
|
||||
/>;
|
||||
const pill = (
|
||||
<Pill url={href} inMessage={true} room={room} shouldShowPillAvatar={shouldShowPillAvatar} />
|
||||
);
|
||||
|
||||
ReactDOM.render(pill, pillContainer);
|
||||
node.parentNode.replaceChild(pillContainer, node);
|
||||
|
@ -111,13 +108,15 @@ export function pillifyLinks(nodes: ArrayLike<Element>, mxEvent: MatrixEvent, pi
|
|||
// Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once
|
||||
node = roomNotifTextNode.nextSibling;
|
||||
|
||||
const pillContainer = document.createElement('span');
|
||||
const pill = <Pill
|
||||
type={PillType.AtRoomMention}
|
||||
inMessage={true}
|
||||
room={room}
|
||||
shouldShowPillAvatar={shouldShowPillAvatar}
|
||||
/>;
|
||||
const pillContainer = document.createElement("span");
|
||||
const pill = (
|
||||
<Pill
|
||||
type={PillType.AtRoomMention}
|
||||
inMessage={true}
|
||||
room={room}
|
||||
shouldShowPillAvatar={shouldShowPillAvatar}
|
||||
/>
|
||||
);
|
||||
|
||||
ReactDOM.render(pill, pillContainer);
|
||||
roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode);
|
||||
|
|
|
@ -30,7 +30,7 @@ export function readReceiptChangeIsFor(event: MatrixEvent, client: MatrixClient)
|
|||
for (const [receiptType, receipt] of Object.entries(event.getContent()[eventId])) {
|
||||
if (!isSupportedReceiptType(receiptType)) continue;
|
||||
|
||||
if (Object.keys((receipt || {})).includes(myUserId)) return true;
|
||||
if (Object.keys(receipt || {}).includes(myUserId)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,5 +23,5 @@ import { getFunctionalMembers } from "./getFunctionalMembers";
|
|||
*/
|
||||
export const getJoinedNonFunctionalMembers = (room: Room): RoomMember[] => {
|
||||
const functionalMembers = getFunctionalMembers(room);
|
||||
return room.getJoinedMembers().filter(m => !functionalMembers.includes(m.userId));
|
||||
return room.getJoinedMembers().filter((m) => !functionalMembers.includes(m.userId));
|
||||
};
|
||||
|
|
|
@ -15,5 +15,5 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
export function htmlToPlainText(html: string) {
|
||||
return new DOMParser().parseFromString(html, 'text/html').documentElement.textContent;
|
||||
return new DOMParser().parseFromString(html, "text/html").documentElement.textContent;
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ import { arrayDiff, Diff } from "./arrays";
|
|||
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 (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;
|
||||
|
|
|
@ -41,18 +41,20 @@ import { SdkContextClass } from "../contexts/SDKContext";
|
|||
|
||||
export const shouldShowSpaceSettings = (space: Room) => {
|
||||
const userId = space.client.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));
|
||||
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,
|
||||
via: calculateRoomVia(room),
|
||||
canonical: canonical,
|
||||
},
|
||||
state_key: room.roomId,
|
||||
});
|
||||
|
@ -85,19 +87,20 @@ export const showCreateNewRoom = async (space: Room, type?: RoomType): Promise<b
|
|||
};
|
||||
|
||||
export const shouldShowSpaceInvite = (space: Room) =>
|
||||
(
|
||||
(space?.getMyMembership() === "join" && space.canInvite(space.client.getUserId())) ||
|
||||
space.getJoinRule() === JoinRule.Public
|
||||
) && shouldShowComponent(UIComponent.InviteUsers);
|
||||
((space?.getMyMembership() === "join" && space.canInvite(space.client.getUserId())) ||
|
||||
space.getJoinRule() === JoinRule.Public) &&
|
||||
shouldShowComponent(UIComponent.InviteUsers);
|
||||
|
||||
export const showSpaceInvite = (space: Room, initialText = ""): void => {
|
||||
if (space.getJoinRule() === "public") {
|
||||
const modal = Modal.createDialog(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>,
|
||||
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",
|
||||
|
@ -109,27 +112,35 @@ export const showSpaceInvite = (space: Room, initialText = ""): void => {
|
|||
};
|
||||
|
||||
export const showAddExistingSubspace = (space: Room): void => {
|
||||
Modal.createDialog(AddExistingSubspaceDialog, {
|
||||
space,
|
||||
onCreateSubspaceClick: () => showCreateNewSubspace(space),
|
||||
onFinished: (added: boolean) => {
|
||||
if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) {
|
||||
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
|
||||
}
|
||||
Modal.createDialog(
|
||||
AddExistingSubspaceDialog,
|
||||
{
|
||||
space,
|
||||
onCreateSubspaceClick: () => showCreateNewSubspace(space),
|
||||
onFinished: (added: boolean) => {
|
||||
if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) {
|
||||
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
|
||||
}
|
||||
},
|
||||
},
|
||||
}, "mx_AddExistingToSpaceDialog_wrapper");
|
||||
"mx_AddExistingToSpaceDialog_wrapper",
|
||||
);
|
||||
};
|
||||
|
||||
export const showCreateNewSubspace = (space: Room): void => {
|
||||
Modal.createDialog(CreateSubspaceDialog, {
|
||||
space,
|
||||
onAddExistingSpaceClick: () => showAddExistingSubspace(space),
|
||||
onFinished: (added: boolean) => {
|
||||
if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) {
|
||||
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
|
||||
}
|
||||
Modal.createDialog(
|
||||
CreateSubspaceDialog,
|
||||
{
|
||||
space,
|
||||
onAddExistingSpaceClick: () => showAddExistingSubspace(space),
|
||||
onFinished: (added: boolean) => {
|
||||
if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) {
|
||||
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
|
||||
}
|
||||
},
|
||||
},
|
||||
}, "mx_CreateSubspaceDialog_wrapper");
|
||||
"mx_CreateSubspaceDialog_wrapper",
|
||||
);
|
||||
};
|
||||
|
||||
export const bulkSpaceBehaviour = async (
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue