Apply prettier formatting

This commit is contained in:
Michael Weimann 2022-12-12 12:24:14 +01:00
parent 1cac306093
commit 526645c791
No known key found for this signature in database
GPG key ID: 53F535A266BB9584
1576 changed files with 65385 additions and 62478 deletions

View file

@ -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) {

View file

@ -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 = "";
}

View file

@ -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);

View file

@ -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 });

View file

@ -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;

View file

@ -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>;
}
}

View file

@ -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

View file

@ -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))
);
};

View file

@ -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,
},
"*",
);
}
}

View file

@ -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;
}

View file

@ -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.
}

View file

@ -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, ", ");

View file

@ -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);

View file

@ -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"];
}

View file

@ -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);

View file

@ -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();

View file

@ -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;

View file

@ -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.

View file

@ -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;

View file

@ -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;
}
/**

View file

@ -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" />;
}

View file

@ -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,
});
}

View file

@ -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));
}
}

View file

@ -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();

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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>;
}

View file

@ -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
) {

View file

@ -76,4 +76,3 @@ export default class ResizeNotifier extends EventEmitter {
this.updateMiddlePanel();
}
}

View file

@ -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++;

View file

@ -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);

View file

@ -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.

View file

@ -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];

View file

@ -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)),
};
});
}

View file

@ -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();
};
});
}

View file

@ -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;

View file

@ -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);

View file

@ -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"];
}

View file

@ -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.

View file

@ -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) {

View file

@ -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.

View file

@ -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,
};
};

View file

@ -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);

View file

@ -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}`;
};

View file

@ -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)

View file

@ -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";

View file

@ -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());

View file

@ -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()) {

View file

@ -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;

View file

@ -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);
});
};

View file

@ -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;
}

View file

@ -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;

View file

@ -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 {

View file

@ -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),
};
};

View file

@ -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,

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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];
}

View file

@ -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]);

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -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, '&amp;'),
encodeURI(this.getAvatarURL(mxEv)).replace(/&/g, "&amp;"),
`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();

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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
}

View file

@ -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;
}

View file

@ -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);
});
});
};

View file

@ -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");

View file

@ -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);

View file

@ -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));
}

View file

@ -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",
);
};

View file

@ -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);
}

View file

@ -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;

View file

@ -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.`,
);
}
};

View file

@ -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);
}

View file

@ -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";

View file

@ -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"];
};

View file

@ -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;

View file

@ -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=(.*)/);

View file

@ -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;
};

View file

@ -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 };
}

View file

@ -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 }),
});
}

View file

@ -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);

View file

@ -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)

View file

@ -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 {

View file

@ -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 };
}

View file

@ -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;
}

View file

@ -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");

View file

@ -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);
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -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);

View file

@ -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;
}
}
}

View file

@ -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));
};

View file

@ -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;
}

View file

@ -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;

View file

@ -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