;
+ >;
}
- // return suitable content for the main (text) part of the status bar.
- _getContent() {
+ render() {
if (this._shouldShowConnectionError()) {
return (
-
- {this._renderLegal()}
- {this._renderCredits()}
+ {this.renderLegal()}
+ {this.renderCredits()}
{_t("Advanced")}
{_t("Homeserver is")}
{MatrixClientPeg.get().getHomeserverUrl()}
{_t("Identity Server is")}
{MatrixClientPeg.get().getIdentityServerUrl()}
{_t("Access Token:") + ' '}
-
+
<{ _t("click to reveal") }>
-
+
{_t("Clear cache and reload")}
diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx
similarity index 90%
rename from src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js
rename to src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx
index 91f6728a7a..6997defea9 100644
--- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -25,10 +25,16 @@ import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../../index";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
+interface IState {
+ busy: boolean;
+ newPersonalRule: string;
+ newList: string;
+}
+
@replaceableComponent("views.settings.tabs.user.MjolnirUserSettingsTab")
-export default class MjolnirUserSettingsTab extends React.Component {
- constructor() {
- super();
+export default class MjolnirUserSettingsTab extends React.Component<{}, IState> {
+ constructor(props) {
+ super(props);
this.state = {
busy: false,
@@ -37,15 +43,15 @@ export default class MjolnirUserSettingsTab extends React.Component {
};
}
- _onPersonalRuleChanged = (e) => {
+ private onPersonalRuleChanged = (e) => {
this.setState({newPersonalRule: e.target.value});
};
- _onNewListChanged = (e) => {
+ private onNewListChanged = (e) => {
this.setState({newList: e.target.value});
};
- _onAddPersonalRule = async (e) => {
+ private onAddPersonalRule = async (e) => {
e.preventDefault();
e.stopPropagation();
@@ -72,7 +78,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
}
};
- _onSubscribeList = async (e) => {
+ private onSubscribeList = async (e) => {
e.preventDefault();
e.stopPropagation();
@@ -94,7 +100,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
}
};
- async _removePersonalRule(rule: ListRule) {
+ private async removePersonalRule(rule: ListRule) {
this.setState({busy: true});
try {
const list = Mjolnir.sharedInstance().getPersonalList();
@@ -112,7 +118,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
}
}
- async _unsubscribeFromList(list: BanList) {
+ private async unsubscribeFromList(list: BanList) {
this.setState({busy: true});
try {
await Mjolnir.sharedInstance().unsubscribeFromList(list.roomId);
@@ -130,7 +136,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
}
}
- _viewListRules(list: BanList) {
+ private viewListRules(list: BanList) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const room = MatrixClientPeg.get().getRoom(list.roomId);
@@ -161,7 +167,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
});
}
- _renderPersonalBanListRules() {
+ private renderPersonalBanListRules() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const list = Mjolnir.sharedInstance().getPersonalList();
@@ -174,7 +180,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
this._removePersonalRule(rule)}
+ onClick={() => this.removePersonalRule(rule)}
disabled={this.state.busy}
>
{_t("Remove")}
@@ -192,7 +198,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
);
}
- _renderSubscribedBanLists() {
+ private renderSubscribedBanLists() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const personalList = Mjolnir.sharedInstance().getPersonalList();
@@ -209,14 +215,14 @@ export default class MjolnirUserSettingsTab extends React.Component {
this._unsubscribeFromList(list)}
+ onClick={() => this.unsubscribeFromList(list)}
disabled={this.state.busy}
>
{_t("Unsubscribe")}
this._viewListRules(list)}
+ onClick={() => this.viewListRules(list)}
disabled={this.state.busy}
>
{_t("View rules")}
@@ -271,21 +277,21 @@ export default class MjolnirUserSettingsTab extends React.Component {
)}
- {this._renderPersonalBanListRules()}
+ {this.renderPersonalBanListRules()}
- {this._renderSubscribedBanLists()}
+ {this.renderSubscribedBanLists()}
);
diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts
index b651e40a3b..f9d957b60c 100644
--- a/src/customisations/Media.ts
+++ b/src/customisations/Media.ts
@@ -96,6 +96,9 @@ export class Media {
*/
public getThumbnailHttp(width: number, height: number, mode: ResizeMethod = "scale"): string | null | undefined {
if (!this.hasThumbnail) return null;
+ // scale using the device pixel ratio to keep images clear
+ width = Math.floor(width * window.devicePixelRatio);
+ height = Math.floor(height * window.devicePixelRatio);
return this.client.mxcUrlToHttp(this.thumbnailMxc, width, height, mode);
}
@@ -107,6 +110,9 @@ export class Media {
* @returns {string} The HTTP URL which points to the thumbnail.
*/
public getThumbnailOfSourceHttp(width: number, height: number, mode: ResizeMethod = "scale"): string {
+ // scale using the device pixel ratio to keep images clear
+ width = Math.floor(width * window.devicePixelRatio);
+ height = Math.floor(height * window.devicePixelRatio);
return this.client.mxcUrlToHttp(this.srcMxc, width, height, mode);
}
@@ -117,6 +123,7 @@ export class Media {
* @returns {string} An HTTP URL for the thumbnail.
*/
public getSquareThumbnailHttp(dim: number): string {
+ dim = Math.floor(dim * window.devicePixelRatio); // scale using the device pixel ratio to keep images clear
if (this.hasThumbnail) {
return this.getThumbnailHttp(dim, dim, 'crop');
}
diff --git a/src/editor/parts.ts b/src/editor/parts.ts
index ccd90da3e2..e3ca36e606 100644
--- a/src/editor/parts.ts
+++ b/src/editor/parts.ts
@@ -341,11 +341,7 @@ class RoomPillPart extends PillPart {
setAvatar(node: HTMLElement) {
let initialLetter = "";
- let avatarUrl = Avatar.avatarUrlForRoom(
- this.room,
- 16 * window.devicePixelRatio,
- 16 * window.devicePixelRatio,
- "crop");
+ let avatarUrl = Avatar.avatarUrlForRoom(this.room, 16, 16, "crop");
if (!avatarUrl) {
initialLetter = Avatar.getInitialLetter(this.room ? this.room.name : this.resourceId);
avatarUrl = Avatar.defaultAvatarUrlForString(this.room ? this.room.roomId : this.resourceId);
@@ -383,11 +379,7 @@ class UserPillPart extends PillPart {
}
const name = this.member.name || this.member.userId;
const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(this.member.userId);
- const avatarUrl = Avatar.avatarUrlForMember(
- this.member,
- 16 * window.devicePixelRatio,
- 16 * window.devicePixelRatio,
- "crop");
+ const avatarUrl = Avatar.avatarUrlForMember(this.member, 16, 16, "crop");
let initialLetter = "";
if (avatarUrl === defaultAvatarUrl) {
initialLetter = Avatar.getInitialLetter(name);
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 0fdc52c5e1..3a98d0d047 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -658,7 +658,6 @@
"No homeserver URL provided": "No homeserver URL provided",
"Unexpected error resolving homeserver configuration": "Unexpected error resolving homeserver configuration",
"Unexpected error resolving identity server configuration": "Unexpected error resolving identity server configuration",
- "The message you are trying to send is too large.": "The message you are trying to send is too large.",
"This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.",
"This homeserver has been blocked by its administrator.": "This homeserver has been blocked by its administrator.",
"This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.",
@@ -1454,6 +1453,7 @@
"Sending your message...": "Sending your message...",
"Encrypting your message...": "Encrypting your message...",
"Your message was sent": "Your message was sent",
+ "Failed to send": "Failed to send",
"Please select the destination room for this message": "Please select the destination room for this message",
"Scroll to most recent messages": "Scroll to most recent messages",
"Close preview": "Close preview",
@@ -1812,8 +1812,9 @@
"The encryption used by this room isn't supported.": "The encryption used by this room isn't supported.",
"Error decrypting audio": "Error decrypting audio",
"React": "React",
- "Reply": "Reply",
"Edit": "Edit",
+ "Retry": "Retry",
+ "Reply": "Reply",
"Message Actions": "Message Actions",
"Attachment": "Attachment",
"Error decrypting attachment": "Error decrypting attachment",
@@ -1924,10 +1925,10 @@
"%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s",
"%(count)s people you know have already joined|other": "%(count)s people you know have already joined",
"%(count)s people you know have already joined|one": "%(count)s person you know has already joined",
- "Rotate Right": "Rotate Right",
- "Rotate Left": "Rotate Left",
"Zoom out": "Zoom out",
"Zoom in": "Zoom in",
+ "Rotate Right": "Rotate Right",
+ "Rotate Left": "Rotate Left",
"Download": "Download",
"Information": "Information",
"View message": "View message",
@@ -2398,7 +2399,6 @@
"Confirm encryption setup": "Confirm encryption setup",
"Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.",
"Unable to set up keys": "Unable to set up keys",
- "Retry": "Retry",
"Restoring keys from backup": "Restoring keys from backup",
"Fetching keys from server...": "Fetching keys from server...",
"%(completed)s of %(total)s keys restored": "%(completed)s of %(total)s keys restored",
@@ -2427,10 +2427,7 @@
"Reject invitation": "Reject invitation",
"Are you sure you want to reject the invitation?": "Are you sure you want to reject the invitation?",
"Unable to reject invite": "Unable to reject invite",
- "Resend edit": "Resend edit",
"Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)",
- "Resend removal": "Resend removal",
- "Cancel Sending": "Cancel Sending",
"Forward Message": "Forward Message",
"Pin Message": "Pin Message",
"Unhide Preview": "Unhide Preview",
@@ -2618,10 +2615,11 @@
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please
contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please
contact your service administrator to continue using the service.",
"Your message wasn't sent because this homeserver has been blocked by it's administrator. Please
contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please
contact your service administrator to continue using the service.",
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please
contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please
contact your service administrator to continue using the service.",
- "%(count)s of your messages have not been sent.|other": "Some of your messages have not been sent.",
- "%(count)s of your messages have not been sent.|one": "Your message was not sent.",
- "%(count)s
Resend all or
cancel all now. You can also select individual messages to resend or cancel.|other": "
Resend all or
cancel all now. You can also select individual messages to resend or cancel.",
- "%(count)s
Resend all or
cancel all now. You can also select individual messages to resend or cancel.|one": "
Resend message or
cancel message now.",
+ "Some of your messages have not been sent": "Some of your messages have not been sent",
+ "Delete all": "Delete all",
+ "Retry all": "Retry all",
+ "Sending": "Sending",
+ "You can select all or individual messages to retry or delete": "You can select all or individual messages to retry or delete",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
@@ -2641,6 +2639,7 @@
"%(count)s rooms|one": "%(count)s room",
"This room is suggested as a good one to join": "This room is suggested as a good one to join",
"Suggested": "Suggested",
+ "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
"%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s rooms and %(numSpaces)s spaces",
"%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s room and %(numSpaces)s spaces",
"%(count)s rooms and 1 space|other": "%(count)s rooms and 1 space",
@@ -2651,7 +2650,6 @@
"Mark as suggested": "Mark as suggested",
"No results found": "No results found",
"You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.",
- "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
"Search names and description": "Search names and description",
"If you can't find the room you're looking for, ask for an invite or
create a new room.": "If you can't find the room you're looking for, ask for an invite or
create a new room.",
"Create room": "Create room",
diff --git a/src/indexing/BaseEventIndexManager.ts b/src/indexing/BaseEventIndexManager.ts
index 2474406618..6349f31524 100644
--- a/src/indexing/BaseEventIndexManager.ts
+++ b/src/indexing/BaseEventIndexManager.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -133,6 +133,10 @@ export default abstract class BaseEventIndexManager {
throw new Error("Unimplemented");
}
+ async isEventIndexEmpty(): Promise
{
+ throw new Error("Unimplemented");
+ }
+
/**
* Check if our event index is empty.
*/
diff --git a/src/indexing/EventIndexPeg.js b/src/indexing/EventIndexPeg.ts
similarity index 94%
rename from src/indexing/EventIndexPeg.js
rename to src/indexing/EventIndexPeg.ts
index 7004efc554..4356d882d5 100644
--- a/src/indexing/EventIndexPeg.js
+++ b/src/indexing/EventIndexPeg.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -27,12 +27,11 @@ import {SettingLevel} from "../settings/SettingLevel";
const INDEX_VERSION = 1;
-class EventIndexPeg {
- constructor() {
- this.index = null;
- this._supportIsInstalled = false;
- this.error = null;
- }
+export class EventIndexPeg {
+ public index: EventIndex = null;
+ public error: Error = null;
+
+ private _supportIsInstalled = false;
/**
* Initialize the EventIndexPeg and if event indexing is enabled initialize
@@ -181,7 +180,7 @@ class EventIndexPeg {
}
}
-if (!global.mxEventIndexPeg) {
- global.mxEventIndexPeg = new EventIndexPeg();
+if (!window.mxEventIndexPeg) {
+ window.mxEventIndexPeg = new EventIndexPeg();
}
-export default global.mxEventIndexPeg;
+export default window.mxEventIndexPeg;
diff --git a/src/mjolnir/BanList.js b/src/mjolnir/BanList.ts
similarity index 100%
rename from src/mjolnir/BanList.js
rename to src/mjolnir/BanList.ts
diff --git a/src/mjolnir/ListRule.js b/src/mjolnir/ListRule.ts
similarity index 100%
rename from src/mjolnir/ListRule.js
rename to src/mjolnir/ListRule.ts
diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.ts
similarity index 100%
rename from src/mjolnir/Mjolnir.js
rename to src/mjolnir/Mjolnir.ts
diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx
index e4b180f3ce..ec6227e45e 100644
--- a/src/stores/SpaceStore.tsx
+++ b/src/stores/SpaceStore.tsx
@@ -113,7 +113,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
public async setActiveSpace(space: Room | null, contextSwitch = true) {
- if (space === this.activeSpace) return;
+ if (space === this.activeSpace || (space && !space?.isSpaceRoom())) return;
this._activeSpace = space;
this.emit(UPDATE_SELECTED_SPACE, this.activeSpace);
@@ -195,7 +195,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
const childEvents = room?.currentState.getStateEvents(EventType.SpaceChild).filter(ev => ev.getContent()?.via);
return sortBy(childEvents, getOrder)
.map(ev => this.matrixClient.getRoom(ev.getStateKey()))
- .filter(room => room?.getMyMembership() === "join") || [];
+ .filter(room => room?.getMyMembership() === "join" || room?.getMyMembership() === "invite") || [];
}
public getChildRooms(spaceId: string): Room[] {
@@ -203,7 +203,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
public getChildSpaces(spaceId: string): Room[] {
- return this.getChildren(spaceId).filter(r => r.isSpaceRoom());
+ // don't show invited subspaces as they surface at the top level for better visibility
+ return this.getChildren(spaceId).filter(r => r.isSpaceRoom() && r.getMyMembership() === "join");
}
public getParents(roomId: string, canonicalOnly = false): Room[] {
@@ -409,32 +410,39 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
});
}, 100, {trailing: true, leading: true});
- private onRoom = (room: Room, membership?: string, oldMembership?: string) => {
- if ((membership || room.getMyMembership()) === "invite") {
- this._invitedSpaces.add(room);
- this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
- } else if (oldMembership === "invite") {
- this._invitedSpaces.delete(room);
- this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
- } else if (room?.isSpaceRoom()) {
- this.onSpaceUpdate();
- this.emit(room.roomId);
- } else {
+ private onRoom = (room: Room, newMembership?: string, oldMembership?: string) => {
+ const membership = newMembership || room.getMyMembership();
+
+ if (!room.isSpaceRoom()) {
// this.onRoomUpdate(room);
this.onRoomsUpdate();
- }
- if (room.getMyMembership() === "join") {
- if (!room.isSpaceRoom()) {
+ if (membership === "join") {
+ // the user just joined a room, remove it from the suggested list if it was there
const numSuggestedRooms = this._suggestedRooms.length;
this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId);
if (numSuggestedRooms !== this._suggestedRooms.length) {
this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
}
- } else if (room.roomId === RoomViewStore.getRoomId()) {
- // if the user was looking at the space and then joined: select that space
- this.setActiveSpace(room);
}
+ return;
+ }
+
+ // Space
+ if (membership === "invite") {
+ this._invitedSpaces.add(room);
+ this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
+ } else if (oldMembership === "invite" && membership !== "join") {
+ this._invitedSpaces.delete(room);
+ this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
+ } else {
+ this.onSpaceUpdate();
+ this.emit(room.roomId);
+ }
+
+ if (membership === "join" && room.roomId === RoomViewStore.getRoomId()) {
+ // if the user was looking at the space and then joined: select that space
+ this.setActiveSpace(room);
}
};
@@ -498,6 +506,17 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
};
+ protected async reset() {
+ this.rootSpaces = [];
+ this.orphanedRooms = new Set();
+ this.parentMap = new EnhancedMap();
+ this.notificationStateMap = new Map();
+ this.spaceFilteredRooms = new Map();
+ this._activeSpace = null;
+ this._suggestedRooms = [];
+ this._invitedSpaces = new Set();
+ }
+
protected async onNotReady() {
if (!SettingsStore.getValue("feature_spaces")) return;
if (this.matrixClient) {
@@ -507,7 +526,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData);
this.matrixClient.removeListener("accountData", this.onAccountData);
}
- await this.reset({});
+ await this.reset();
}
protected async onReady() {
@@ -540,15 +559,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// as this is not helpful and can create loops of rooms/space switching
if (!room || payload.context_switch) break;
- // persist last viewed room from a space
-
if (room.isSpaceRoom()) {
- this.setActiveSpace(room);
+ // Don't context switch when navigating to the space room
+ // as it will cause you to end up in the wrong room
+ this.setActiveSpace(room, false);
} else if (!this.getSpaceFilteredRoomIds(this.activeSpace).has(room.roomId)) {
- // TODO maybe reverse these first 2 clauses once space panel active is fixed
- let parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(room.roomId));
+ let parent = this.getCanonicalParent(room.roomId);
if (!parent) {
- parent = this.getCanonicalParent(room.roomId);
+ parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(room.roomId));
}
if (!parent) {
const parents = Array.from(this.parentMap.get(room.roomId) || []);
@@ -582,7 +600,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
return state;
}
- // traverse space tree with DFS calling fn on each space including the given root one
+ // traverse space tree with DFS calling fn on each space including the given root one,
+ // if includeRooms is true then fn will be called on each leaf room, if it is present in multiple sub-spaces
+ // then fn will be called with it multiple times.
public traverseSpace(
spaceId: string,
fn: (roomId: string) => void,
diff --git a/src/stores/TypingStore.js b/src/stores/TypingStore.ts
similarity index 84%
rename from src/stores/TypingStore.js
rename to src/stores/TypingStore.ts
index e86d698eac..d5177a33a0 100644
--- a/src/stores/TypingStore.js
+++ b/src/stores/TypingStore.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -25,15 +25,23 @@ const TYPING_SERVER_TIMEOUT = 30000;
* Tracks typing state for users.
*/
export default class TypingStore {
+ private typingStates: {
+ [roomId: string]: {
+ isTyping: boolean,
+ userTimer: Timer,
+ serverTimer: Timer,
+ },
+ };
+
constructor() {
this.reset();
}
static sharedInstance(): TypingStore {
- if (global.mxTypingStore === undefined) {
- global.mxTypingStore = new TypingStore();
+ if (window.mxTypingStore === undefined) {
+ window.mxTypingStore = new TypingStore();
}
- return global.mxTypingStore;
+ return window.mxTypingStore;
}
/**
@@ -41,7 +49,7 @@ export default class TypingStore {
* MatrixClientPeg client changes.
*/
reset() {
- this._typingStates = {
+ this.typingStates = {
// "roomId": {
// isTyping: bool, // Whether the user is typing or not
// userTimer: Timer, // Local timeout for "user has stopped typing"
@@ -59,14 +67,14 @@ export default class TypingStore {
if (!SettingsStore.getValue('sendTypingNotifications')) return;
if (SettingsStore.getValue('lowBandwidth')) return;
- let currentTyping = this._typingStates[roomId];
+ let currentTyping = this.typingStates[roomId];
if ((!isTyping && !currentTyping) || (currentTyping && currentTyping.isTyping === isTyping)) {
// No change in state, so don't do anything. We'll let the timer run its course.
return;
}
if (!currentTyping) {
- currentTyping = this._typingStates[roomId] = {
+ currentTyping = this.typingStates[roomId] = {
isTyping: isTyping,
serverTimer: new Timer(TYPING_SERVER_TIMEOUT),
userTimer: new Timer(TYPING_USER_TIMEOUT),
@@ -78,7 +86,7 @@ export default class TypingStore {
if (isTyping) {
if (!currentTyping.serverTimer.isRunning()) {
currentTyping.serverTimer.restart().finished().then(() => {
- const currentTyping = this._typingStates[roomId];
+ const currentTyping = this.typingStates[roomId];
if (currentTyping) currentTyping.isTyping = false;
// The server will (should) time us out on typing, so we don't
diff --git a/src/stores/WidgetEchoStore.js b/src/stores/WidgetEchoStore.ts
similarity index 71%
rename from src/stores/WidgetEchoStore.js
rename to src/stores/WidgetEchoStore.ts
index 3aef1beb3e..09120d6108 100644
--- a/src/stores/WidgetEchoStore.js
+++ b/src/stores/WidgetEchoStore.ts
@@ -1,6 +1,5 @@
/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2018-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,6 +15,8 @@ limitations under the License.
*/
import EventEmitter from 'events';
+import { IWidget } from 'matrix-widget-api';
+import MatrixEvent from "matrix-js-sdk/src/models/event";
import {WidgetType} from "../widgets/WidgetType";
/**
@@ -23,14 +24,20 @@ import {WidgetType} from "../widgets/WidgetType";
* proxying through state from the js-sdk.
*/
class WidgetEchoStore extends EventEmitter {
+ private roomWidgetEcho: {
+ [roomId: string]: {
+ [widgetId: string]: IWidget,
+ },
+ };
+
constructor() {
super();
- this._roomWidgetEcho = {
+ this.roomWidgetEcho = {
// Map as below. Object is the content of the widget state event,
// so for widgets that have been deleted locally, the object is empty.
// roomId: {
- // widgetId: [object]
+ // widgetId: IWidget
// }
};
}
@@ -42,14 +49,14 @@ class WidgetEchoStore extends EventEmitter {
* and we don't really need the actual widget events anyway since we just want to
* show a spinner / prevent widgets being added twice.
*
- * @param {Room} roomId The ID of the room to get widgets for
+ * @param {string} roomId The ID of the room to get widgets for
* @param {MatrixEvent[]} currentRoomWidgets Current widgets for the room
* @returns {MatrixEvent[]} List of widgets in the room, minus any pending removal
*/
- getEchoedRoomWidgets(roomId, currentRoomWidgets) {
+ getEchoedRoomWidgets(roomId: string, currentRoomWidgets: MatrixEvent[]): MatrixEvent[] {
const echoedWidgets = [];
- const roomEchoState = Object.assign({}, this._roomWidgetEcho[roomId]);
+ const roomEchoState = Object.assign({}, this.roomWidgetEcho[roomId]);
for (const w of currentRoomWidgets) {
const widgetId = w.getStateKey();
@@ -65,8 +72,8 @@ class WidgetEchoStore extends EventEmitter {
return echoedWidgets;
}
- roomHasPendingWidgetsOfType(roomId, currentRoomWidgets, type: WidgetType) {
- const roomEchoState = Object.assign({}, this._roomWidgetEcho[roomId]);
+ roomHasPendingWidgetsOfType(roomId: string, currentRoomWidgets: MatrixEvent[], type?: WidgetType): boolean {
+ const roomEchoState = Object.assign({}, this.roomWidgetEcho[roomId]);
// any widget IDs that are already in the room are not pending, so
// echoes for them don't count as pending.
@@ -85,20 +92,20 @@ class WidgetEchoStore extends EventEmitter {
}
}
- roomHasPendingWidgets(roomId, currentRoomWidgets) {
+ roomHasPendingWidgets(roomId: string, currentRoomWidgets: MatrixEvent[]): boolean {
return this.roomHasPendingWidgetsOfType(roomId, currentRoomWidgets);
}
- setRoomWidgetEcho(roomId, widgetId, state) {
- if (this._roomWidgetEcho[roomId] === undefined) this._roomWidgetEcho[roomId] = {};
+ setRoomWidgetEcho(roomId: string, widgetId: string, state: IWidget) {
+ if (this.roomWidgetEcho[roomId] === undefined) this.roomWidgetEcho[roomId] = {};
- this._roomWidgetEcho[roomId][widgetId] = state;
+ this.roomWidgetEcho[roomId][widgetId] = state;
this.emit('update', roomId, widgetId);
}
- removeRoomWidgetEcho(roomId, widgetId) {
- delete this._roomWidgetEcho[roomId][widgetId];
- if (Object.keys(this._roomWidgetEcho[roomId]).length === 0) delete this._roomWidgetEcho[roomId];
+ removeRoomWidgetEcho(roomId: string, widgetId: string) {
+ delete this.roomWidgetEcho[roomId][widgetId];
+ if (Object.keys(this.roomWidgetEcho[roomId]).length === 0) delete this.roomWidgetEcho[roomId];
this.emit('update', roomId, widgetId);
}
}
diff --git a/src/stores/notifications/StaticNotificationState.ts b/src/stores/notifications/StaticNotificationState.ts
index 0392ed3716..b18aa78e0f 100644
--- a/src/stores/notifications/StaticNotificationState.ts
+++ b/src/stores/notifications/StaticNotificationState.ts
@@ -18,6 +18,8 @@ import { NotificationColor } from "./NotificationColor";
import { NotificationState } from "./NotificationState";
export class StaticNotificationState extends NotificationState {
+ public static readonly RED_EXCLAMATION = StaticNotificationState.forSymbol("!", NotificationColor.Red);
+
constructor(symbol: string, count: number, color: NotificationColor) {
super();
this._symbol = symbol;
diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts
index 388bb061e3..f212b1f9d9 100644
--- a/src/stores/room-list/filters/VisibilityProvider.ts
+++ b/src/stores/room-list/filters/VisibilityProvider.ts
@@ -37,7 +37,11 @@ export class VisibilityProvider {
await VoipUserMapper.sharedInstance().onNewInvitedRoom(room);
}
- public isRoomVisible(room: Room): boolean {
+ public isRoomVisible(room?: Room): boolean {
+ if (!room) {
+ return false;
+ }
+
if (
CallHandler.sharedInstance().getSupportsVirtualRooms() &&
VoipUserMapper.sharedInstance().isVirtualRoom(room)
diff --git a/src/stores/room-list/previews/MessageEventPreview.ts b/src/stores/room-list/previews/MessageEventPreview.ts
index deed7dcf2c..b900afc13f 100644
--- a/src/stores/room-list/previews/MessageEventPreview.ts
+++ b/src/stores/room-list/previews/MessageEventPreview.ts
@@ -20,7 +20,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from "../../../languageHandler";
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
import ReplyThread from "../../../components/views/elements/ReplyThread";
-import { sanitizedHtmlNodeInnerText } from "../../../HtmlUtils";
+import { getHtmlText } from "../../../HtmlUtils";
export class MessageEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
@@ -55,7 +55,7 @@ export class MessageEventPreview implements IPreview {
}
if (hasHtml) {
- body = sanitizedHtmlNodeInnerText(body);
+ body = getHtmlText(body);
}
if (msgtype === 'm.emote') {
diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.tsx
similarity index 91%
rename from src/utils/AutoDiscoveryUtils.js
rename to src/utils/AutoDiscoveryUtils.tsx
index 614aa4cea8..e3a7fd2d0b 100644
--- a/src/utils/AutoDiscoveryUtils.js
+++ b/src/utils/AutoDiscoveryUtils.tsx
@@ -1,6 +1,5 @@
/*
-Copyright 2019 New Vector Ltd
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
+import React, { ReactNode } from 'react';
import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery";
import {_t, _td, newTranslatableError} from "../languageHandler";
import {makeType} from "./TypeUtils";
import SdkConfig from '../SdkConfig';
-const LIVELINESS_DISCOVERY_ERRORS = [
+const LIVELINESS_DISCOVERY_ERRORS: string[] = [
AutoDiscovery.ERROR_INVALID_HOMESERVER,
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
];
@@ -40,17 +39,23 @@ export class ValidatedServerConfig {
warning: string;
}
+export interface IAuthComponentState {
+ serverIsAlive: boolean;
+ serverErrorIsFatal: boolean;
+ serverDeadError?: ReactNode;
+}
+
export default class AutoDiscoveryUtils {
/**
* Checks if a given error or error message is considered an error
* relating to the liveliness of the server. Must be an error returned
* from this AutoDiscoveryUtils class.
- * @param {string|Error} error The error to check
+ * @param {string | Error} error The error to check
* @returns {boolean} True if the error is a liveliness error.
*/
- static isLivelinessError(error: string|Error): boolean {
+ static isLivelinessError(error: string | Error): boolean {
if (!error) return false;
- return !!LIVELINESS_DISCOVERY_ERRORS.find(e => e === error || e === error.message);
+ return !!LIVELINESS_DISCOVERY_ERRORS.find(e => typeof error === "string" ? e === error : e === error.message);
}
/**
@@ -61,7 +66,7 @@ export default class AutoDiscoveryUtils {
* implementation for known values.
* @returns {*} The state for the component, given the error.
*/
- static authComponentStateForError(err: string | Error | null, pageName = "login"): Object {
+ static authComponentStateForError(err: string | Error | null, pageName = "login"): IAuthComponentState {
if (!err) {
return {
serverIsAlive: true,
@@ -70,7 +75,7 @@ export default class AutoDiscoveryUtils {
};
}
let title = _t("Cannot reach homeserver");
- let body = _t("Ensure you have a stable internet connection, or get in touch with the server admin");
+ let body: ReactNode = _t("Ensure you have a stable internet connection, or get in touch with the server admin");
if (!AutoDiscoveryUtils.isLivelinessError(err)) {
const brand = SdkConfig.get().brand;
title = _t("Your %(brand)s is misconfigured", { brand });
@@ -92,7 +97,7 @@ export default class AutoDiscoveryUtils {
}
let isFatalError = true;
- const errorMessage = err.message ? err.message : err;
+ const errorMessage = typeof err === "string" ? err : err.message;
if (errorMessage === AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER) {
isFatalError = false;
title = _t("Cannot reach identity server");
@@ -141,7 +146,10 @@ export default class AutoDiscoveryUtils {
* @returns {Promise} Resolves to the validated configuration.
*/
static async validateServerConfigWithStaticUrls(
- homeserverUrl: string, identityUrl: string, syntaxOnly = false): ValidatedServerConfig {
+ homeserverUrl: string,
+ identityUrl?: string,
+ syntaxOnly = false,
+ ): Promise {
if (!homeserverUrl) {
throw newTranslatableError(_td("No homeserver URL provided"));
}
@@ -171,7 +179,7 @@ export default class AutoDiscoveryUtils {
* @param {string} serverName The homeserver domain name (eg: "matrix.org") to validate.
* @returns {Promise} Resolves to the validated configuration.
*/
- static async validateServerName(serverName: string): ValidatedServerConfig {
+ static async validateServerName(serverName: string): Promise {
const result = await AutoDiscovery.findClientConfig(serverName);
return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result);
}
diff --git a/src/utils/ErrorUtils.js b/src/utils/ErrorUtils.js
index 2c6acd5503..b5bd5b0af0 100644
--- a/src/utils/ErrorUtils.js
+++ b/src/utils/ErrorUtils.js
@@ -49,12 +49,6 @@ export function messageForResourceLimitError(limitType, adminContact, strings, e
}
}
-export function messageForSendError(errorData) {
- if (errorData.errcode === "M_TOO_LARGE") {
- return _t("The message you are trying to send is too large.");
- }
-}
-
export function messageForSyncError(err) {
if (err.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
const limitError = messageForResourceLimitError(
diff --git a/src/utils/MatrixGlob.js b/src/utils/MatrixGlob.ts
similarity index 100%
rename from src/utils/MatrixGlob.js
rename to src/utils/MatrixGlob.ts
diff --git a/src/utils/StorageManager.js b/src/utils/StorageManager.ts
similarity index 96%
rename from src/utils/StorageManager.js
rename to src/utils/StorageManager.ts
index 23c27a2d1c..883c032771 100644
--- a/src/utils/StorageManager.js
+++ b/src/utils/StorageManager.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2019 New Vector Ltd
+Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -32,15 +32,15 @@ try {
const SYNC_STORE_NAME = "riot-web-sync";
const CRYPTO_STORE_NAME = "matrix-js-sdk:crypto";
-function log(msg) {
+function log(msg: string) {
console.log(`StorageManager: ${msg}`);
}
-function error(msg) {
- console.error(`StorageManager: ${msg}`);
+function error(msg: string, ...args: string[]) {
+ console.error(`StorageManager: ${msg}`, ...args);
}
-function track(action) {
+function track(action: string) {
Analytics.trackEvent("StorageManager", action);
}
@@ -73,7 +73,7 @@ export async function checkConsistency() {
dataInLocalStorage = localStorage.length > 0;
log(`Local storage contains data? ${dataInLocalStorage}`);
- cryptoInited = localStorage.getItem("mx_crypto_initialised");
+ cryptoInited = !!localStorage.getItem("mx_crypto_initialised");
log(`Crypto initialised? ${cryptoInited}`);
} else {
healthy = false;
diff --git a/src/utils/Timer.js b/src/utils/Timer.ts
similarity index 60%
rename from src/utils/Timer.js
rename to src/utils/Timer.ts
index ca06237fbf..9760631d09 100644
--- a/src/utils/Timer.js
+++ b/src/utils/Timer.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2018 New Vector Ltd
+Copyright 2018, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -26,44 +26,48 @@ Once a timer is finished or aborted, it can't be started again
a new one through `clone()` or `cloneIfRun()`.
*/
export default class Timer {
- constructor(timeout) {
- this._timeout = timeout;
- this._onTimeout = this._onTimeout.bind(this);
- this._setNotStarted();
+ private timerHandle: NodeJS.Timeout;
+ private startTs: number;
+ private promise: Promise;
+ private resolve: () => void;
+ private reject: (Error) => void;
+
+ constructor(private timeout: number) {
+ this.setNotStarted();
}
- _setNotStarted() {
- this._timerHandle = null;
- this._startTs = null;
- this._promise = new Promise((resolve, reject) => {
- this._resolve = resolve;
- this._reject = reject;
+ private setNotStarted() {
+ this.timerHandle = null;
+ this.startTs = null;
+ this.promise = new Promise((resolve, reject) => {
+ this.resolve = resolve;
+ this.reject = reject;
}).finally(() => {
- this._timerHandle = null;
+ this.timerHandle = null;
});
}
- _onTimeout() {
+ private onTimeout = () => {
const now = Date.now();
- const elapsed = now - this._startTs;
- if (elapsed >= this._timeout) {
- this._resolve();
- this._setNotStarted();
+ const elapsed = now - this.startTs;
+ if (elapsed >= this.timeout) {
+ this.resolve();
+ this.setNotStarted();
} else {
- const delta = this._timeout - elapsed;
- this._timerHandle = setTimeout(this._onTimeout, delta);
+ const delta = this.timeout - elapsed;
+ this.timerHandle = setTimeout(this.onTimeout, delta);
}
}
- changeTimeout(timeout) {
- if (timeout === this._timeout) {
+ changeTimeout(timeout: number) {
+ if (timeout === this.timeout) {
return;
}
- const isSmallerTimeout = timeout < this._timeout;
- this._timeout = timeout;
+ const isSmallerTimeout = timeout < this.timeout;
+ this.timeout = timeout;
if (this.isRunning() && isSmallerTimeout) {
- clearTimeout(this._timerHandle);
- this._onTimeout();
+ clearTimeout(this.timerHandle);
+ this.onTimeout();
}
}
@@ -73,8 +77,8 @@ export default class Timer {
*/
start() {
if (!this.isRunning()) {
- this._startTs = Date.now();
- this._timerHandle = setTimeout(this._onTimeout, this._timeout);
+ this.startTs = Date.now();
+ this.timerHandle = setTimeout(this.onTimeout, this.timeout);
}
return this;
}
@@ -89,7 +93,7 @@ export default class Timer {
// can be called in fast succession,
// instead just take note and compare
// when the already running timeout expires
- this._startTs = Date.now();
+ this.startTs = Date.now();
return this;
} else {
return this.start();
@@ -103,9 +107,9 @@ export default class Timer {
*/
abort() {
if (this.isRunning()) {
- clearTimeout(this._timerHandle);
- this._reject(new Error("Timer was aborted."));
- this._setNotStarted();
+ clearTimeout(this.timerHandle);
+ this.reject(new Error("Timer was aborted."));
+ this.setNotStarted();
}
return this;
}
@@ -116,10 +120,10 @@ export default class Timer {
*@return {Promise}
*/
finished() {
- return this._promise;
+ return this.promise;
}
isRunning() {
- return this._timerHandle !== null;
+ return this.timerHandle !== null;
}
}
diff --git a/src/utils/TypeUtils.js b/src/utils/TypeUtils.ts
similarity index 100%
rename from src/utils/TypeUtils.js
rename to src/utils/TypeUtils.ts
diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts
index 8ab66dfb29..cea377bfe9 100644
--- a/src/utils/arrays.ts
+++ b/src/utils/arrays.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,23 +15,47 @@ limitations under the License.
*/
/**
- * Quickly resample an array to have less data points. This isn't a perfect representation,
- * though this does work best if given a large array to downsample to a much smaller array.
- * @param {number[]} input The input array to downsample.
+ * Quickly resample an array to have less/more data points. If an input which is larger
+ * than the desired size is provided, it will be downsampled. Similarly, if the input
+ * is smaller than the desired size then it will be upsampled.
+ * @param {number[]} input The input array to resample.
* @param {number} points The number of samples to end up with.
- * @returns {number[]} The downsampled array.
+ * @returns {number[]} The resampled array.
*/
export function arrayFastResample(input: number[], points: number): number[] {
- // Heavily inpired by matrix-media-repo (used with permission)
+ if (input.length === points) return input; // short-circuit a complicated call
+
+ // Heavily inspired by matrix-media-repo (used with permission)
// https://github.com/turt2live/matrix-media-repo/blob/abe72c87d2e29/util/util_audio/fastsample.go#L10
- const everyNth = Math.round(input.length / points);
- const samples: number[] = [];
- for (let i = 0; i < input.length; i += everyNth) {
- samples.push(input[i]);
+ let samples: number[] = [];
+ if (input.length > points) {
+ // Danger: this loop can cause out of memory conditions if the input is too small.
+ const everyNth = Math.round(input.length / points);
+ for (let i = 0; i < input.length; i += everyNth) {
+ samples.push(input[i]);
+ }
+ } else {
+ // Smaller inputs mean we have to spread the values over the desired length. We
+ // end up overshooting the target length in doing this, so we'll resample down
+ // before returning. This recursion is risky, but mathematically should not go
+ // further than 1 level deep.
+ const spreadFactor = Math.ceil(points / input.length);
+ for (const val of input) {
+ samples.push(...arraySeed(val, spreadFactor));
+ }
+ samples = arrayFastResample(samples, points);
}
+
+ // Sanity fill, just in case
while (samples.length < points) {
samples.push(input[input.length - 1]);
}
+
+ // Sanity trim, just in case
+ if (samples.length > points) {
+ samples = samples.slice(0, points);
+ }
+
return samples;
}
@@ -178,6 +202,13 @@ export class GroupedArray {
constructor(private val: Map) {
}
+ /**
+ * The value of this group, after all applicable alterations.
+ */
+ public get value(): Map {
+ return this.val;
+ }
+
/**
* Orders the grouping into an array using the provided key order.
* @param keyOrder The key order.
diff --git a/src/utils/enums.ts b/src/utils/enums.ts
index f7f4787896..d3ca318c28 100644
--- a/src/utils/enums.ts
+++ b/src/utils/enums.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -19,11 +19,23 @@ limitations under the License.
* @param e The enum.
* @returns The enum values.
*/
-export function getEnumValues(e: any): T[] {
+export function getEnumValues(e: any): (string | number)[] {
+ // String-based enums will simply be objects ({Key: "value"}), but number-based
+ // enums will instead map themselves twice: in one direction for {Key: 12} and
+ // the reverse for easy lookup, presumably ({12: Key}). In the reverse mapping,
+ // the key is a string, not a number.
+ //
+ // For this reason, we try to determine what kind of enum we're dealing with.
+
const keys = Object.keys(e);
- return keys
- .filter(k => ['string', 'number'].includes(typeof(e[k])))
- .map(k => e[k]);
+ const values: (string | number)[] = [];
+ for (const key of keys) {
+ const value = e[key];
+ if (Number.isFinite(value) || e[value.toString()] !== Number(key)) {
+ values.push(value);
+ }
+ }
+ return values;
}
/**
diff --git a/src/utils/objects.ts b/src/utils/objects.ts
index e7f4f0f907..2c9361beba 100644
--- a/src/utils/objects.ts
+++ b/src/utils/objects.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -141,3 +141,21 @@ export function objectKeyChanges(a: O, b: O): (keyof O)[] {
export function objectClone(obj: O): O {
return JSON.parse(JSON.stringify(obj));
}
+
+/**
+ * Converts a series of entries to an object.
+ * @param entries The entries to convert.
+ * @returns The converted object.
+ */
+// NOTE: Deprecated once we have Object.fromEntries() support.
+// @ts-ignore - return type is complaining about non-string keys, but we know better
+export function objectFromEntries(entries: Iterable<[K, V]>): {[k: K]: V} {
+ const obj: {
+ // @ts-ignore - same as return type
+ [k: K]: V} = {};
+ for (const e of entries) {
+ // @ts-ignore - same as return type
+ obj[e[0]] = e[1];
+ }
+ return obj;
+}
diff --git a/src/utils/permalinks/ElementPermalinkConstructor.js b/src/utils/permalinks/ElementPermalinkConstructor.ts
similarity index 82%
rename from src/utils/permalinks/ElementPermalinkConstructor.js
rename to src/utils/permalinks/ElementPermalinkConstructor.ts
index da7f5797ea..cd7f2b9d2c 100644
--- a/src/utils/permalinks/ElementPermalinkConstructor.js
+++ b/src/utils/permalinks/ElementPermalinkConstructor.ts
@@ -20,31 +20,31 @@ import PermalinkConstructor, {PermalinkParts} from "./PermalinkConstructor";
* Generates permalinks that self-reference the running webapp
*/
export default class ElementPermalinkConstructor extends PermalinkConstructor {
- _elementUrl: string;
+ private elementUrl: string;
constructor(elementUrl: string) {
super();
- this._elementUrl = elementUrl;
+ this.elementUrl = elementUrl;
- if (!this._elementUrl.startsWith("http:") && !this._elementUrl.startsWith("https:")) {
+ if (!this.elementUrl.startsWith("http:") && !this.elementUrl.startsWith("https:")) {
throw new Error("Element prefix URL does not appear to be an HTTP(S) URL");
}
}
forEvent(roomId: string, eventId: string, serverCandidates: string[]): string {
- return `${this._elementUrl}/#/room/${roomId}/${eventId}${this.encodeServerCandidates(serverCandidates)}`;
+ return `${this.elementUrl}/#/room/${roomId}/${eventId}${this.encodeServerCandidates(serverCandidates)}`;
}
- forRoom(roomIdOrAlias: string, serverCandidates: string[]): string {
- return `${this._elementUrl}/#/room/${roomIdOrAlias}${this.encodeServerCandidates(serverCandidates)}`;
+ forRoom(roomIdOrAlias: string, serverCandidates?: string[]): string {
+ return `${this.elementUrl}/#/room/${roomIdOrAlias}${this.encodeServerCandidates(serverCandidates)}`;
}
forUser(userId: string): string {
- return `${this._elementUrl}/#/user/${userId}`;
+ return `${this.elementUrl}/#/user/${userId}`;
}
forGroup(groupId: string): string {
- return `${this._elementUrl}/#/group/${groupId}`;
+ return `${this.elementUrl}/#/group/${groupId}`;
}
forEntity(entityId: string): string {
@@ -58,11 +58,11 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor {
}
isPermalinkHost(testHost: string): boolean {
- const parsedUrl = new URL(this._elementUrl);
+ const parsedUrl = new URL(this.elementUrl);
return testHost === (parsedUrl.host || parsedUrl.hostname); // one of the hosts should match
}
- encodeServerCandidates(candidates: string[]) {
+ encodeServerCandidates(candidates?: string[]) {
if (!candidates || candidates.length === 0) return '';
return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
}
@@ -71,11 +71,11 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor {
// https://github.com/turt2live/matrix-js-bot-sdk/blob/7c4665c9a25c2c8e0fe4e509f2616505b5b66a1c/src/Permalinks.ts#L33-L61
// Adapted for Element's URL format
parsePermalink(fullUrl: string): PermalinkParts {
- if (!fullUrl || !fullUrl.startsWith(this._elementUrl)) {
+ if (!fullUrl || !fullUrl.startsWith(this.elementUrl)) {
throw new Error("Does not appear to be a permalink");
}
- const parts = fullUrl.substring(`${this._elementUrl}/#/`.length);
+ const parts = fullUrl.substring(`${this.elementUrl}/#/`.length);
return ElementPermalinkConstructor.parseAppRoute(parts);
}
diff --git a/src/utils/permalinks/PermalinkConstructor.js b/src/utils/permalinks/PermalinkConstructor.ts
similarity index 100%
rename from src/utils/permalinks/PermalinkConstructor.js
rename to src/utils/permalinks/PermalinkConstructor.ts
diff --git a/src/utils/permalinks/Permalinks.js b/src/utils/permalinks/Permalinks.ts
similarity index 77%
rename from src/utils/permalinks/Permalinks.js
rename to src/utils/permalinks/Permalinks.ts
index bcf4d87136..2ef955c358 100644
--- a/src/utils/permalinks/Permalinks.js
+++ b/src/utils/permalinks/Permalinks.ts
@@ -17,6 +17,9 @@ limitations under the License.
import isIp from "is-ip";
import * as utils from "matrix-js-sdk/src/utils";
import {Room} from "matrix-js-sdk/src/models/room";
+import {EventType} from "matrix-js-sdk/src/@types/event";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import {MatrixClientPeg} from "../../MatrixClientPeg";
import SpecPermalinkConstructor, {baseUrl as matrixtoBaseUrl} from "./SpecPermalinkConstructor";
@@ -74,29 +77,35 @@ const MAX_SERVER_CANDIDATES = 3;
// the list and magically have the link work.
export class RoomPermalinkCreator {
+ private room: Room;
+ private roomId: string;
+ private highestPlUserId: string;
+ private populationMap: { [serverName: string]: number };
+ private bannedHostsRegexps: RegExp[];
+ private allowedHostsRegexps: RegExp[];
+ private _serverCandidates: string[];
+ private started: boolean;
+
// We support being given a roomId as a fallback in the event the `room` object
// doesn't exist or is not healthy for us to rely on. For example, loading a
// permalink to a room which the MatrixClient doesn't know about.
- constructor(room, roomId = null) {
- this._room = room;
- this._roomId = room ? room.roomId : roomId;
- this._highestPlUserId = null;
- this._populationMap = null;
- this._bannedHostsRegexps = null;
- this._allowedHostsRegexps = null;
+ constructor(room: Room, roomId: string = null) {
+ this.room = room;
+ this.roomId = room ? room.roomId : roomId;
+ this.highestPlUserId = null;
+ this.populationMap = null;
+ this.bannedHostsRegexps = null;
+ this.allowedHostsRegexps = null;
this._serverCandidates = null;
- this._started = false;
+ this.started = false;
- if (!this._roomId) {
+ if (!this.roomId) {
throw new Error("Failed to resolve a roomId for the permalink creator to use");
}
-
- this.onMembership = this.onMembership.bind(this);
- this.onRoomState = this.onRoomState.bind(this);
}
load() {
- if (!this._room || !this._room.currentState) {
+ if (!this.room || !this.room.currentState) {
// Under rare and unknown circumstances it is possible to have a room with no
// currentState, at least potentially at the early stages of joining a room.
// To avoid breaking everything, we'll just warn rather than throw as well as
@@ -104,23 +113,23 @@ export class RoomPermalinkCreator {
console.warn("Tried to load a permalink creator with no room state");
return;
}
- this._updateAllowedServers();
- this._updateHighestPlUser();
- this._updatePopulationMap();
- this._updateServerCandidates();
+ this.updateAllowedServers();
+ this.updateHighestPlUser();
+ this.updatePopulationMap();
+ this.updateServerCandidates();
}
start() {
this.load();
- this._room.on("RoomMember.membership", this.onMembership);
- this._room.on("RoomState.events", this.onRoomState);
- this._started = true;
+ this.room.on("RoomMember.membership", this.onMembership);
+ this.room.on("RoomState.events", this.onRoomState);
+ this.started = true;
}
stop() {
- this._room.removeListener("RoomMember.membership", this.onMembership);
- this._room.removeListener("RoomState.events", this.onRoomState);
- this._started = false;
+ this.room.removeListener("RoomMember.membership", this.onMembership);
+ this.room.removeListener("RoomState.events", this.onRoomState);
+ this.started = false;
}
get serverCandidates() {
@@ -128,44 +137,44 @@ export class RoomPermalinkCreator {
}
isStarted() {
- return this._started;
+ return this.started;
}
- forEvent(eventId) {
- return getPermalinkConstructor().forEvent(this._roomId, eventId, this._serverCandidates);
+ forEvent(eventId: string): string {
+ return getPermalinkConstructor().forEvent(this.roomId, eventId, this._serverCandidates);
}
- forShareableRoom() {
- if (this._room) {
+ forShareableRoom(): string {
+ if (this.room) {
// Prefer to use canonical alias for permalink if possible
- const alias = this._room.getCanonicalAlias();
+ const alias = this.room.getCanonicalAlias();
if (alias) {
return getPermalinkConstructor().forRoom(alias, this._serverCandidates);
}
}
- return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates);
+ return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);
}
- forRoom() {
- return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates);
+ forRoom(): string {
+ return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);
}
- onRoomState(event) {
+ private onRoomState = (event: MatrixEvent) => {
switch (event.getType()) {
- case "m.room.server_acl":
- this._updateAllowedServers();
- this._updateHighestPlUser();
- this._updatePopulationMap();
- this._updateServerCandidates();
+ case EventType.RoomServerAcl:
+ this.updateAllowedServers();
+ this.updateHighestPlUser();
+ this.updatePopulationMap();
+ this.updateServerCandidates();
return;
- case "m.room.power_levels":
- this._updateHighestPlUser();
- this._updateServerCandidates();
+ case EventType.RoomPowerLevels:
+ this.updateHighestPlUser();
+ this.updateServerCandidates();
return;
}
}
- onMembership(evt, member, oldMembership) {
+ private onMembership = (evt: MatrixEvent, member: RoomMember, oldMembership: string) => {
const userId = member.userId;
const membership = member.membership;
const serverName = getServerName(userId);
@@ -173,17 +182,17 @@ export class RoomPermalinkCreator {
const hasLeft = oldMembership === "join" && membership !== "join";
if (hasLeft) {
- this._populationMap[serverName]--;
+ this.populationMap[serverName]--;
} else if (hasJoined) {
- this._populationMap[serverName]++;
+ this.populationMap[serverName]++;
}
- this._updateHighestPlUser();
- this._updateServerCandidates();
+ this.updateHighestPlUser();
+ this.updateServerCandidates();
}
- _updateHighestPlUser() {
- const plEvent = this._room.currentState.getStateEvents("m.room.power_levels", "");
+ private updateHighestPlUser() {
+ const plEvent = this.room.currentState.getStateEvents("m.room.power_levels", "");
if (plEvent) {
const content = plEvent.getContent();
if (content) {
@@ -191,14 +200,14 @@ export class RoomPermalinkCreator {
if (users) {
const entries = Object.entries(users);
const allowedEntries = entries.filter(([userId]) => {
- const member = this._room.getMember(userId);
+ const member = this.room.getMember(userId);
if (!member || member.membership !== "join") {
return false;
}
const serverName = getServerName(userId);
return !isHostnameIpAddress(serverName) &&
- !isHostInRegex(serverName, this._bannedHostsRegexps) &&
- isHostInRegex(serverName, this._allowedHostsRegexps);
+ !isHostInRegex(serverName, this.bannedHostsRegexps) &&
+ isHostInRegex(serverName, this.allowedHostsRegexps);
});
const maxEntry = allowedEntries.reduce((max, entry) => {
return (entry[1] > max[1]) ? entry : max;
@@ -206,20 +215,20 @@ export class RoomPermalinkCreator {
const [userId, powerLevel] = maxEntry;
// object wasn't empty, and max entry wasn't a demotion from the default
if (userId !== null && powerLevel >= 50) {
- this._highestPlUserId = userId;
+ this.highestPlUserId = userId;
return;
}
}
}
}
- this._highestPlUserId = null;
+ this.highestPlUserId = null;
}
- _updateAllowedServers() {
+ private updateAllowedServers() {
const bannedHostsRegexps = [];
let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone
- if (this._room.currentState) {
- const aclEvent = this._room.currentState.getStateEvents("m.room.server_acl", "");
+ if (this.room.currentState) {
+ const aclEvent = this.room.currentState.getStateEvents("m.room.server_acl", "");
if (aclEvent && aclEvent.getContent()) {
const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$");
@@ -231,35 +240,35 @@ export class RoomPermalinkCreator {
allowed.forEach(h => allowedHostsRegexps.push(getRegex(h)));
}
}
- this._bannedHostsRegexps = bannedHostsRegexps;
- this._allowedHostsRegexps = allowedHostsRegexps;
+ this.bannedHostsRegexps = bannedHostsRegexps;
+ this.allowedHostsRegexps = allowedHostsRegexps;
}
- _updatePopulationMap() {
+ private updatePopulationMap() {
const populationMap: { [server: string]: number } = {};
- for (const member of this._room.getJoinedMembers()) {
+ for (const member of this.room.getJoinedMembers()) {
const serverName = getServerName(member.userId);
if (!populationMap[serverName]) {
populationMap[serverName] = 0;
}
populationMap[serverName]++;
}
- this._populationMap = populationMap;
+ this.populationMap = populationMap;
}
- _updateServerCandidates() {
+ private updateServerCandidates() {
let candidates = [];
- if (this._highestPlUserId) {
- candidates.push(getServerName(this._highestPlUserId));
+ if (this.highestPlUserId) {
+ candidates.push(getServerName(this.highestPlUserId));
}
- const serversByPopulation = Object.keys(this._populationMap)
- .sort((a, b) => this._populationMap[b] - this._populationMap[a])
+ const serversByPopulation = Object.keys(this.populationMap)
+ .sort((a, b) => this.populationMap[b] - this.populationMap[a])
.filter(a => {
return !candidates.includes(a) &&
!isHostnameIpAddress(a) &&
- !isHostInRegex(a, this._bannedHostsRegexps) &&
- isHostInRegex(a, this._allowedHostsRegexps);
+ !isHostInRegex(a, this.bannedHostsRegexps) &&
+ isHostInRegex(a, this.allowedHostsRegexps);
});
const remainingServers = serversByPopulation.slice(0, MAX_SERVER_CANDIDATES - candidates.length);
@@ -273,11 +282,11 @@ export function makeGenericPermalink(entityId: string): string {
return getPermalinkConstructor().forEntity(entityId);
}
-export function makeUserPermalink(userId) {
+export function makeUserPermalink(userId: string): string {
return getPermalinkConstructor().forUser(userId);
}
-export function makeRoomPermalink(roomId) {
+export function makeRoomPermalink(roomId: string): string {
if (!roomId) {
throw new Error("can't permalink a falsey roomId");
}
@@ -296,7 +305,7 @@ export function makeRoomPermalink(roomId) {
return permalinkCreator.forRoom();
}
-export function makeGroupPermalink(groupId) {
+export function makeGroupPermalink(groupId: string): string {
return getPermalinkConstructor().forGroup(groupId);
}
@@ -428,24 +437,24 @@ export function parseAppLocalLink(localLink: string): PermalinkParts {
return null;
}
-function getServerName(userId) {
+function getServerName(userId: string): string {
return userId.split(":").splice(1).join(":");
}
-function getHostnameFromMatrixDomain(domain) {
+function getHostnameFromMatrixDomain(domain: string): string {
if (!domain) return null;
return new URL(`https://${domain}`).hostname;
}
-function isHostInRegex(hostname, regexps) {
+function isHostInRegex(hostname: string, regexps: RegExp[]) {
hostname = getHostnameFromMatrixDomain(hostname);
if (!hostname) return true; // assumed
- if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0]);
+ if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0].toString());
return regexps.filter(h => h.test(hostname)).length > 0;
}
-function isHostnameIpAddress(hostname) {
+function isHostnameIpAddress(hostname: string): boolean {
hostname = getHostnameFromMatrixDomain(hostname);
if (!hostname) return false;
diff --git a/src/utils/permalinks/SpecPermalinkConstructor.js b/src/utils/permalinks/SpecPermalinkConstructor.ts
similarity index 100%
rename from src/utils/permalinks/SpecPermalinkConstructor.js
rename to src/utils/permalinks/SpecPermalinkConstructor.ts
diff --git a/test/ScalarAuthClient-test.js b/test/ScalarAuthClient-test.js
index 83f357811a..3435f70932 100644
--- a/test/ScalarAuthClient-test.js
+++ b/test/ScalarAuthClient-test.js
@@ -29,7 +29,7 @@ describe('ScalarAuthClient', function() {
it('should request a new token if the old one fails', async function() {
const sac = new ScalarAuthClient();
- sac._getAccountName = jest.fn((arg) => {
+ sac.getAccountName = jest.fn((arg) => {
switch (arg) {
case "brokentoken":
return Promise.reject({
diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js
index fcdd71629e..d3211f564c 100644
--- a/test/components/views/rooms/RoomList-test.js
+++ b/test/components/views/rooms/RoomList-test.js
@@ -29,7 +29,10 @@ function waitForRoomListStoreUpdate() {
describe('RoomList', () => {
function createRoom(opts) {
- const room = new Room(generateRoomId(), null, client.getUserId());
+ const room = new Room(generateRoomId(), MatrixClientPeg.get(), client.getUserId(), {
+ // The room list now uses getPendingEvents(), so we need a detached ordering.
+ pendingEventOrdering: "detached",
+ });
if (opts) {
Object.assign(room, opts);
}
diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts
new file mode 100644
index 0000000000..aef788647d
--- /dev/null
+++ b/test/stores/SpaceStore-test.ts
@@ -0,0 +1,714 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { EventEmitter } from "events";
+import { EventType } from "matrix-js-sdk/src/@types/event";
+
+import "../skinned-sdk"; // Must be first for skinning to work
+import SpaceStore, {
+ UPDATE_INVITED_SPACES,
+ UPDATE_SELECTED_SPACE,
+ UPDATE_TOP_LEVEL_SPACES
+} from "../../src/stores/SpaceStore";
+import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils";
+import { mkEvent, mkStubRoom, stubClient } from "../test-utils";
+import { EnhancedMap } from "../../src/utils/maps";
+import SettingsStore from "../../src/settings/SettingsStore";
+import DMRoomMap from "../../src/utils/DMRoomMap";
+import { MatrixClientPeg } from "../../src/MatrixClientPeg";
+import defaultDispatcher from "../../src/dispatcher/dispatcher";
+
+type MatrixEvent = any; // importing from js-sdk upsets things
+
+jest.useFakeTimers();
+
+const mockStateEventImplementation = (events: MatrixEvent[]) => {
+ const stateMap = new EnhancedMap>();
+ events.forEach(event => {
+ stateMap.getOrCreate(event.getType(), new Map()).set(event.getStateKey(), event);
+ });
+
+ return (eventType: string, stateKey?: string) => {
+ if (stateKey || stateKey === "") {
+ return stateMap.get(eventType)?.get(stateKey) || null;
+ }
+ return Array.from(stateMap.get(eventType)?.values() || []);
+ };
+};
+
+const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise(r => e.once(k, r));
+
+const testUserId = "@test:user";
+
+let rooms = [];
+
+const mkRoom = (roomId: string) => {
+ const room = mkStubRoom(roomId);
+ room.currentState.getStateEvents.mockImplementation(mockStateEventImplementation([]));
+ rooms.push(room);
+ return room;
+};
+
+const mkSpace = (spaceId: string, children: string[] = []) => {
+ const space = mkRoom(spaceId);
+ space.isSpaceRoom.mockReturnValue(true);
+ space.currentState.getStateEvents.mockImplementation(mockStateEventImplementation(children.map(roomId =>
+ mkEvent({
+ event: true,
+ type: EventType.SpaceChild,
+ room: spaceId,
+ user: testUserId,
+ skey: roomId,
+ content: { via: [] },
+ ts: Date.now(),
+ }),
+ )));
+ return space;
+};
+
+const getValue = jest.fn();
+SettingsStore.getValue = getValue;
+
+const getUserIdForRoomId = jest.fn();
+// @ts-ignore
+DMRoomMap.sharedInstance = { getUserIdForRoomId };
+
+const fav1 = "!fav1:server";
+const fav2 = "!fav2:server";
+const fav3 = "!fav3:server";
+const dm1 = "!dm1:server";
+const dm1Partner = "@dm1Partner:server";
+const dm2 = "!dm2:server";
+const dm2Partner = "@dm2Partner:server";
+const dm3 = "!dm3:server";
+const dm3Partner = "@dm3Partner:server";
+const orphan1 = "!orphan1:server";
+const orphan2 = "!orphan2:server";
+const invite1 = "!invite1:server";
+const invite2 = "!invite2:server";
+const room1 = "!room1:server";
+const room2 = "!room2:server";
+const space1 = "!space1:server";
+const space2 = "!space2:server";
+const space3 = "!space3:server";
+
+describe("SpaceStore", () => {
+ stubClient();
+ const store = SpaceStore.instance;
+ const client = MatrixClientPeg.get();
+
+ const viewRoom = roomId => defaultDispatcher.dispatch({ action: "view_room", room_id: roomId }, true);
+
+ const run = async () => {
+ client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId));
+ await setupAsyncStoreWithClient(store, client);
+ jest.runAllTimers();
+ };
+
+ beforeEach(() => {
+ jest.runAllTimers();
+ client.getVisibleRooms.mockReturnValue(rooms = []);
+ getValue.mockImplementation(settingName => {
+ if (settingName === "feature_spaces") {
+ return true;
+ }
+ });
+ });
+ afterEach(async () => {
+ await resetAsyncStoreWithClient(store);
+ });
+
+ describe("static hierarchy resolution tests", () => {
+ it("handles no spaces", async () => {
+ await run();
+
+ expect(store.spacePanelSpaces).toStrictEqual([]);
+ expect(store.invitedSpaces).toStrictEqual([]);
+ });
+
+ it("handles 3 joined top level spaces", async () => {
+ mkSpace("!space1:server");
+ mkSpace("!space2:server");
+ mkSpace("!space3:server");
+ await run();
+
+ expect(store.spacePanelSpaces.sort()).toStrictEqual(client.getVisibleRooms().sort());
+ expect(store.invitedSpaces).toStrictEqual([]);
+ });
+
+ it("handles a basic hierarchy", async () => {
+ mkSpace("!space1:server");
+ mkSpace("!space2:server");
+ mkSpace("!company:server", [
+ mkSpace("!company_dept1:server", [
+ mkSpace("!company_dept1_group1:server").roomId,
+ ]).roomId,
+ mkSpace("!company_dept2:server").roomId,
+ ]);
+ await run();
+
+ expect(store.spacePanelSpaces.map(r => r.roomId).sort()).toStrictEqual([
+ "!space1:server",
+ "!space2:server",
+ "!company:server",
+ ].sort());
+ expect(store.invitedSpaces).toStrictEqual([]);
+
+ expect(store.getChildRooms("!space1:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!space1:server")).toStrictEqual([]);
+ expect(store.getChildRooms("!space2:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!space2:server")).toStrictEqual([]);
+ expect(store.getChildRooms("!company:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!company:server")).toStrictEqual([
+ client.getRoom("!company_dept1:server"),
+ client.getRoom("!company_dept2:server"),
+ ]);
+ expect(store.getChildRooms("!company_dept1:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!company_dept1:server")).toStrictEqual([
+ client.getRoom("!company_dept1_group1:server"),
+ ]);
+ expect(store.getChildRooms("!company_dept1_group1:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!company_dept1_group1:server")).toStrictEqual([]);
+ expect(store.getChildRooms("!company_dept2:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!company_dept2:server")).toStrictEqual([]);
+ });
+
+ it("handles a sub-space existing in multiple places in the space tree", async () => {
+ const subspace = mkSpace("!subspace:server");
+ mkSpace("!space1:server");
+ mkSpace("!space2:server");
+ mkSpace("!company:server", [
+ mkSpace("!company_dept1:server", [
+ mkSpace("!company_dept1_group1:server", [subspace.roomId]).roomId,
+ ]).roomId,
+ mkSpace("!company_dept2:server", [subspace.roomId]).roomId,
+ subspace.roomId,
+ ]);
+ await run();
+
+ expect(store.spacePanelSpaces.map(r => r.roomId).sort()).toStrictEqual([
+ "!space1:server",
+ "!space2:server",
+ "!company:server",
+ ].sort());
+ expect(store.invitedSpaces).toStrictEqual([]);
+
+ expect(store.getChildRooms("!space1:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!space1:server")).toStrictEqual([]);
+ expect(store.getChildRooms("!space2:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!space2:server")).toStrictEqual([]);
+ expect(store.getChildRooms("!company:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!company:server")).toStrictEqual([
+ client.getRoom("!company_dept1:server"),
+ client.getRoom("!company_dept2:server"),
+ subspace,
+ ]);
+ expect(store.getChildRooms("!company_dept1:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!company_dept1:server")).toStrictEqual([
+ client.getRoom("!company_dept1_group1:server"),
+ ]);
+ expect(store.getChildRooms("!company_dept1_group1:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!company_dept1_group1:server")).toStrictEqual([subspace]);
+ expect(store.getChildRooms("!company_dept2:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!company_dept2:server")).toStrictEqual([subspace]);
+ });
+
+ it("handles full cycles", async () => {
+ mkSpace("!a:server", [
+ mkSpace("!b:server", [
+ mkSpace("!c:server", [
+ "!a:server",
+ ]).roomId,
+ ]).roomId,
+ ]);
+ await run();
+
+ expect(store.spacePanelSpaces.map(r => r.roomId)).toStrictEqual(["!a:server"]);
+ expect(store.invitedSpaces).toStrictEqual([]);
+
+ expect(store.getChildRooms("!a:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!a:server")).toStrictEqual([client.getRoom("!b:server")]);
+ expect(store.getChildRooms("!b:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!b:server")).toStrictEqual([client.getRoom("!c:server")]);
+ expect(store.getChildRooms("!c:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!c:server")).toStrictEqual([client.getRoom("!a:server")]);
+ });
+
+ it("handles partial cycles", async () => {
+ mkSpace("!b:server", [
+ mkSpace("!a:server", [
+ mkSpace("!c:server", [
+ "!a:server",
+ ]).roomId,
+ ]).roomId,
+ ]);
+ await run();
+
+ expect(store.spacePanelSpaces.map(r => r.roomId)).toStrictEqual(["!b:server"]);
+ expect(store.invitedSpaces).toStrictEqual([]);
+
+ expect(store.getChildRooms("!b:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!b:server")).toStrictEqual([client.getRoom("!a:server")]);
+ expect(store.getChildRooms("!a:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!a:server")).toStrictEqual([client.getRoom("!c:server")]);
+ expect(store.getChildRooms("!c:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!c:server")).toStrictEqual([client.getRoom("!a:server")]);
+ });
+
+ it("handles partial cycles with additional spaces coming off them", async () => {
+ // TODO this test should be failing right now
+ mkSpace("!a:server", [
+ mkSpace("!b:server", [
+ mkSpace("!c:server", [
+ "!a:server",
+ mkSpace("!d:server").roomId,
+ ]).roomId,
+ ]).roomId,
+ ]);
+ await run();
+
+ expect(store.spacePanelSpaces.map(r => r.roomId)).toStrictEqual(["!a:server"]);
+ expect(store.invitedSpaces).toStrictEqual([]);
+
+ expect(store.getChildRooms("!a:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!a:server")).toStrictEqual([client.getRoom("!b:server")]);
+ expect(store.getChildRooms("!b:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!b:server")).toStrictEqual([client.getRoom("!c:server")]);
+ expect(store.getChildRooms("!c:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!c:server")).toStrictEqual([
+ client.getRoom("!a:server"),
+ client.getRoom("!d:server"),
+ ]);
+ expect(store.getChildRooms("!d:server")).toStrictEqual([]);
+ expect(store.getChildSpaces("!d:server")).toStrictEqual([]);
+ });
+
+ it("invite to a subspace is only shown at the top level", async () => {
+ mkSpace(invite1).getMyMembership.mockReturnValue("invite");
+ mkSpace(space1, [invite1]);
+ await run();
+
+ expect(store.spacePanelSpaces).toStrictEqual([client.getRoom(space1)]);
+ expect(store.getChildSpaces(space1)).toStrictEqual([]);
+ expect(store.getChildRooms(space1)).toStrictEqual([]);
+ expect(store.invitedSpaces).toStrictEqual([client.getRoom(invite1)]);
+ });
+
+ describe("test fixture 1", () => {
+ beforeEach(async () => {
+ [fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, room1].forEach(mkRoom);
+ mkSpace(space1, [fav1, room1]);
+ mkSpace(space2, [fav1, fav2, fav3, room1]);
+ mkSpace(space3, [invite2]);
+
+ [fav1, fav2, fav3].forEach(roomId => {
+ client.getRoom(roomId).tags = {
+ "m.favourite": {
+ order: 0.5,
+ },
+ };
+ });
+
+ [invite1, invite2].forEach(roomId => {
+ client.getRoom(roomId).getMyMembership.mockReturnValue("invite");
+ });
+
+ getUserIdForRoomId.mockImplementation(roomId => {
+ return {
+ [dm1]: dm1Partner,
+ [dm2]: dm2Partner,
+ [dm3]: dm3Partner,
+ }[roomId];
+ });
+ await run();
+ });
+
+ it("home space contains orphaned rooms", () => {
+ expect(store.getSpaceFilteredRoomIds(null).has(orphan1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(null).has(orphan2)).toBeTruthy();
+ });
+
+ it("home space contains favourites", () => {
+ expect(store.getSpaceFilteredRoomIds(null).has(fav1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(null).has(fav2)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(null).has(fav3)).toBeTruthy();
+ });
+
+ it("home space contains dm rooms", () => {
+ expect(store.getSpaceFilteredRoomIds(null).has(dm1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(null).has(dm2)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(null).has(dm3)).toBeTruthy();
+ });
+
+ it("home space contains invites", () => {
+ expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeTruthy();
+ });
+
+ it("home space contains invites even if they are also shown in a space", () => {
+ expect(store.getSpaceFilteredRoomIds(null).has(invite2)).toBeTruthy();
+ });
+
+ it("home space does not contain rooms/low priority from rooms within spaces", () => {
+ expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeFalsy();
+ });
+
+ it("space contains child rooms", () => {
+ const space = client.getRoom(space1);
+ expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(space).has(room1)).toBeTruthy();
+ });
+
+ it("space contains child favourites", () => {
+ const space = client.getRoom(space2);
+ expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(space).has(fav2)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(space).has(fav3)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(space).has(room1)).toBeTruthy();
+ });
+
+ it("space contains child invites", () => {
+ const space = client.getRoom(space3);
+ expect(store.getSpaceFilteredRoomIds(space).has(invite2)).toBeTruthy();
+ });
+ });
+ });
+
+ describe("hierarchy resolution update tests", () => {
+ let emitter: EventEmitter;
+ beforeEach(async () => {
+ emitter = new EventEmitter();
+ client.on.mockImplementation(emitter.on.bind(emitter));
+ client.removeListener.mockImplementation(emitter.removeListener.bind(emitter));
+ });
+ afterEach(() => {
+ client.on.mockReset();
+ client.removeListener.mockReset();
+ });
+
+ it("updates state when spaces are joined", async () => {
+ await run();
+ expect(store.spacePanelSpaces).toStrictEqual([]);
+ const space = mkSpace(space1);
+ const prom = emitPromise(store, UPDATE_TOP_LEVEL_SPACES);
+ emitter.emit("Room", space);
+ await prom;
+ expect(store.spacePanelSpaces).toStrictEqual([space]);
+ expect(store.invitedSpaces).toStrictEqual([]);
+ });
+
+ it("updates state when spaces are left", async () => {
+ const space = mkSpace(space1);
+ await run();
+
+ expect(store.spacePanelSpaces).toStrictEqual([space]);
+ space.getMyMembership.mockReturnValue("leave");
+ const prom = emitPromise(store, UPDATE_TOP_LEVEL_SPACES);
+ emitter.emit("Room.myMembership", space, "leave", "join");
+ await prom;
+ expect(store.spacePanelSpaces).toStrictEqual([]);
+ });
+
+ it("updates state when space invite comes in", async () => {
+ await run();
+ expect(store.spacePanelSpaces).toStrictEqual([]);
+ expect(store.invitedSpaces).toStrictEqual([]);
+ const space = mkSpace(space1);
+ space.getMyMembership.mockReturnValue("invite");
+ const prom = emitPromise(store, UPDATE_INVITED_SPACES);
+ emitter.emit("Room", space);
+ await prom;
+ expect(store.spacePanelSpaces).toStrictEqual([]);
+ expect(store.invitedSpaces).toStrictEqual([space]);
+ });
+
+ it("updates state when space invite is accepted", async () => {
+ const space = mkSpace(space1);
+ space.getMyMembership.mockReturnValue("invite");
+ await run();
+
+ expect(store.spacePanelSpaces).toStrictEqual([]);
+ expect(store.invitedSpaces).toStrictEqual([space]);
+ space.getMyMembership.mockReturnValue("join");
+ const prom = emitPromise(store, UPDATE_TOP_LEVEL_SPACES);
+ emitter.emit("Room.myMembership", space, "join", "invite");
+ await prom;
+ expect(store.spacePanelSpaces).toStrictEqual([space]);
+ expect(store.invitedSpaces).toStrictEqual([]);
+ });
+
+ it("updates state when space invite is rejected", async () => {
+ const space = mkSpace(space1);
+ space.getMyMembership.mockReturnValue("invite");
+ await run();
+
+ expect(store.spacePanelSpaces).toStrictEqual([]);
+ expect(store.invitedSpaces).toStrictEqual([space]);
+ space.getMyMembership.mockReturnValue("leave");
+ const prom = emitPromise(store, UPDATE_INVITED_SPACES);
+ emitter.emit("Room.myMembership", space, "leave", "invite");
+ await prom;
+ expect(store.spacePanelSpaces).toStrictEqual([]);
+ expect(store.invitedSpaces).toStrictEqual([]);
+ });
+
+ it("room invite gets added to relevant space filters", async () => {
+ const space = mkSpace(space1, [invite1]);
+ await run();
+
+ expect(store.spacePanelSpaces).toStrictEqual([space]);
+ expect(store.invitedSpaces).toStrictEqual([]);
+ expect(store.getChildSpaces(space1)).toStrictEqual([]);
+ expect(store.getChildRooms(space1)).toStrictEqual([]);
+ expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(invite1)).toBeFalsy();
+ expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeFalsy();
+
+ const invite = mkRoom(invite1);
+ invite.getMyMembership.mockReturnValue("invite");
+ const prom = emitPromise(store, space1);
+ emitter.emit("Room", space);
+ await prom;
+
+ expect(store.spacePanelSpaces).toStrictEqual([space]);
+ expect(store.invitedSpaces).toStrictEqual([]);
+ expect(store.getChildSpaces(space1)).toStrictEqual([]);
+ expect(store.getChildRooms(space1)).toStrictEqual([invite]);
+ expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(invite1)).toBeTruthy();
+ expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeTruthy();
+ });
+ });
+
+ describe("active space switching tests", () => {
+ const fn = jest.spyOn(store, "emit");
+
+ beforeEach(async () => {
+ mkRoom(room1); // not a space
+ mkSpace(space1, [
+ mkSpace(space2).roomId,
+ ]);
+ mkSpace(space3).getMyMembership.mockReturnValue("invite");
+ await run();
+ await store.setActiveSpace(null);
+ expect(store.activeSpace).toBe(null);
+ });
+ afterEach(() => {
+ fn.mockClear();
+ });
+
+ it("switch to home space", async () => {
+ await store.setActiveSpace(client.getRoom(space1));
+ fn.mockClear();
+
+ await store.setActiveSpace(null);
+ expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, null);
+ expect(store.activeSpace).toBe(null);
+ });
+
+ it("switch to invited space", async () => {
+ const space = client.getRoom(space3);
+ await store.setActiveSpace(space);
+ expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space);
+ expect(store.activeSpace).toBe(space);
+ });
+
+ it("switch to top level space", async () => {
+ const space = client.getRoom(space1);
+ await store.setActiveSpace(space);
+ expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space);
+ expect(store.activeSpace).toBe(space);
+ });
+
+ it("switch to subspace", async () => {
+ const space = client.getRoom(space2);
+ await store.setActiveSpace(space);
+ expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space);
+ expect(store.activeSpace).toBe(space);
+ });
+
+ it("switch to unknown space is a nop", async () => {
+ expect(store.activeSpace).toBe(null);
+ const space = client.getRoom(room1); // not a space
+ await store.setActiveSpace(space);
+ expect(fn).not.toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space);
+ expect(store.activeSpace).toBe(null);
+ });
+ });
+
+ describe("context switching tests", () => {
+ const fn = jest.spyOn(defaultDispatcher, "dispatch");
+
+ beforeEach(async () => {
+ [room1, room2, orphan1].forEach(mkRoom);
+ mkSpace(space1, [room1, room2]);
+ mkSpace(space2, [room2]);
+ await run();
+ });
+ afterEach(() => {
+ fn.mockClear();
+ localStorage.clear();
+ });
+
+ const getCurrentRoom = () => fn.mock.calls.reverse().find(([p]) => p.action === "view_room")?.[0].room_id;
+
+ it("last viewed room in target space is the current viewed and in both spaces", async () => {
+ await store.setActiveSpace(client.getRoom(space1));
+ viewRoom(room2);
+ await store.setActiveSpace(client.getRoom(space2));
+ viewRoom(room2);
+ await store.setActiveSpace(client.getRoom(space1));
+ expect(getCurrentRoom()).toBe(room2);
+ });
+
+ it("last viewed room in target space is in the current space", async () => {
+ await store.setActiveSpace(client.getRoom(space1));
+ viewRoom(room2);
+ await store.setActiveSpace(client.getRoom(space2));
+ expect(getCurrentRoom()).toBe(space2);
+ await store.setActiveSpace(client.getRoom(space1));
+ expect(getCurrentRoom()).toBe(room2);
+ });
+
+ it("last viewed room in target space is not in the current space", async () => {
+ await store.setActiveSpace(client.getRoom(space1));
+ viewRoom(room1);
+ await store.setActiveSpace(client.getRoom(space2));
+ viewRoom(room2);
+ await store.setActiveSpace(client.getRoom(space1));
+ expect(getCurrentRoom()).toBe(room1);
+ });
+
+ it("last viewed room is target space is not known", async () => {
+ await store.setActiveSpace(client.getRoom(space1));
+ viewRoom(room1);
+ localStorage.setItem(`mx_space_context_${space2}`, orphan2);
+ await store.setActiveSpace(client.getRoom(space2));
+ expect(getCurrentRoom()).toBe(space2);
+ });
+
+ it("no last viewed room in target space", async () => {
+ await store.setActiveSpace(client.getRoom(space1));
+ viewRoom(room1);
+ await store.setActiveSpace(client.getRoom(space2));
+ expect(getCurrentRoom()).toBe(space2);
+ });
+
+ it("no last viewed room in home space", async () => {
+ await store.setActiveSpace(client.getRoom(space1));
+ viewRoom(room1);
+ await store.setActiveSpace(null);
+ expect(fn.mock.calls[fn.mock.calls.length - 1][0]).toStrictEqual({ action: "view_home_page" });
+ });
+ });
+
+ describe("space auto switching tests", () => {
+ beforeEach(async () => {
+ [room1, room2, orphan1].forEach(mkRoom);
+ mkSpace(space1, [room1, room2]);
+ mkSpace(space2, [room1, room2]);
+
+ client.getRoom(room2).currentState.getStateEvents.mockImplementation(mockStateEventImplementation([
+ mkEvent({
+ event: true,
+ type: EventType.SpaceParent,
+ room: room2,
+ user: testUserId,
+ skey: space2,
+ content: { via: [], canonical: true },
+ ts: Date.now(),
+ }),
+ ]));
+ await run();
+ });
+
+ it("no switch required, room is in current space", async () => {
+ viewRoom(room1);
+ await store.setActiveSpace(client.getRoom(space1), false);
+ viewRoom(room2);
+ expect(store.activeSpace).toBe(client.getRoom(space1));
+ });
+
+ it("switch to canonical parent space for room", async () => {
+ viewRoom(room1);
+ await store.setActiveSpace(null, false);
+ viewRoom(room2);
+ expect(store.activeSpace).toBe(client.getRoom(space2));
+ });
+
+ it("switch to first containing space for room", async () => {
+ viewRoom(room2);
+ await store.setActiveSpace(null, false);
+ viewRoom(room1);
+ expect(store.activeSpace).toBe(client.getRoom(space1));
+ });
+
+ it("switch to home for orphaned room", async () => {
+ viewRoom(room1);
+ await store.setActiveSpace(client.getRoom(space1), false);
+ viewRoom(orphan1);
+ expect(store.activeSpace).toBeNull();
+ });
+ });
+
+ describe("traverseSpace", () => {
+ beforeEach(() => {
+ mkSpace("!a:server", [
+ mkSpace("!b:server", [
+ mkSpace("!c:server", [
+ "!a:server",
+ mkRoom("!c-child:server").roomId,
+ mkRoom("!shared-child:server").roomId,
+ ]).roomId,
+ mkRoom("!b-child:server").roomId,
+ ]).roomId,
+ mkRoom("!a-child:server").roomId,
+ "!shared-child:server",
+ ]);
+ });
+
+ it("avoids cycles", () => {
+ const fn = jest.fn();
+ store.traverseSpace("!b:server", fn);
+
+ expect(fn).toBeCalledTimes(3);
+ expect(fn).toBeCalledWith("!a:server");
+ expect(fn).toBeCalledWith("!b:server");
+ expect(fn).toBeCalledWith("!c:server");
+ });
+
+ it("including rooms", () => {
+ const fn = jest.fn();
+ store.traverseSpace("!b:server", fn, true);
+
+ expect(fn).toBeCalledTimes(8); // twice for shared-child
+ expect(fn).toBeCalledWith("!a:server");
+ expect(fn).toBeCalledWith("!a-child:server");
+ expect(fn).toBeCalledWith("!b:server");
+ expect(fn).toBeCalledWith("!b-child:server");
+ expect(fn).toBeCalledWith("!c:server");
+ expect(fn).toBeCalledWith("!c-child:server");
+ expect(fn).toBeCalledWith("!shared-child:server");
+ });
+
+ it("excluding rooms", () => {
+ const fn = jest.fn();
+ store.traverseSpace("!b:server", fn, false);
+
+ expect(fn).toBeCalledTimes(3);
+ expect(fn).toBeCalledWith("!a:server");
+ expect(fn).toBeCalledWith("!b:server");
+ expect(fn).toBeCalledWith("!c:server");
+ });
+ });
+});
diff --git a/test/test-utils.js b/test/test-utils.js
index d259fcb95f..baa77114ed 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -79,6 +79,17 @@ export function createTestClient() {
generateClientSecret: () => "t35tcl1Ent5ECr3T",
isGuest: () => false,
isCryptoEnabled: () => false,
+ getSpaceSummary: jest.fn().mockReturnValue({
+ rooms: [],
+ events: [],
+ }),
+
+ // Used by various internal bits we aren't concerned with (yet)
+ _sessionStore: {
+ store: {
+ getItem: jest.fn(),
+ },
+ },
};
}
@@ -88,8 +99,8 @@ export function createTestClient() {
* @param {string} opts.type The event.type
* @param {string} opts.room The event.room_id
* @param {string} opts.user The event.user_id
- * @param {string} opts.skey Optional. The state key (auto inserts empty string)
- * @param {Number} opts.ts Optional. Timestamp for the event
+ * @param {string=} opts.skey Optional. The state key (auto inserts empty string)
+ * @param {number=} opts.ts Optional. Timestamp for the event
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object} a JSON object representing this event.
@@ -224,7 +235,7 @@ export function mkStubRoom(roomId = null) {
hasMembershipState: () => null,
getVersion: () => '1',
shouldUpgradeToVersion: () => null,
- getMyMembership: () => "join",
+ getMyMembership: jest.fn().mockReturnValue("join"),
maySendMessage: jest.fn().mockReturnValue(true),
currentState: {
getStateEvents: jest.fn(),
@@ -233,17 +244,17 @@ export function mkStubRoom(roomId = null) {
maySendEvent: jest.fn().mockReturnValue(true),
members: [],
},
- tags: {
- "m.favourite": {
- order: 0.5,
- },
- },
+ tags: {},
setBlacklistUnverifiedDevices: jest.fn(),
on: jest.fn(),
removeListener: jest.fn(),
getDMInviter: jest.fn(),
getAvatarUrl: () => 'mxc://avatar.url/room.png',
getMxcAvatarUrl: () => 'mxc://avatar.url/room.png',
+ isSpaceRoom: jest.fn(() => false),
+ getUnreadNotificationCount: jest.fn(() => 0),
+ getEventReadUpTo: jest.fn(() => null),
+ timeline: [],
};
}
diff --git a/test/utils/ShieldUtils-test.js b/test/utils/ShieldUtils-test.js
index 8e3b19c1c4..bea3d26565 100644
--- a/test/utils/ShieldUtils-test.js
+++ b/test/utils/ShieldUtils-test.js
@@ -128,7 +128,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() {
describe("shieldStatusForMembership other-trust behaviour", function() {
beforeAll(() => {
- DMRoomMap._sharedInstance = {
+ DMRoomMap.sharedInstance = {
getUserIdForRoomId: (roomId) => roomId === "DM" ? "@any:h" : null,
};
});
diff --git a/test/Singleflight-test.ts b/test/utils/Singleflight-test.ts
similarity index 98%
rename from test/Singleflight-test.ts
rename to test/utils/Singleflight-test.ts
index 4f0c6e0da3..80258701bb 100644
--- a/test/Singleflight-test.ts
+++ b/test/utils/Singleflight-test.ts
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {Singleflight} from "../src/utils/Singleflight";
+import {Singleflight} from "../../src/utils/Singleflight";
describe('Singleflight', () => {
afterEach(() => {
diff --git a/test/utils/arrays-test.ts b/test/utils/arrays-test.ts
new file mode 100644
index 0000000000..ececd274b2
--- /dev/null
+++ b/test/utils/arrays-test.ts
@@ -0,0 +1,294 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {
+ arrayDiff,
+ arrayFastClone,
+ arrayFastResample,
+ arrayHasDiff,
+ arrayHasOrderChange,
+ arrayMerge,
+ arraySeed,
+ arrayUnion,
+ ArrayUtil,
+ GroupedArray,
+} from "../../src/utils/arrays";
+import {objectFromEntries} from "../../src/utils/objects";
+
+function expectSample(i: number, input: number[], expected: number[]) {
+ console.log(`Resample case index: ${i}`); // for debugging test failures
+ const result = arrayFastResample(input, expected.length);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(expected.length);
+ expect(result).toEqual(expected);
+}
+
+describe('arrays', () => {
+ describe('arrayFastResample', () => {
+ it('should downsample', () => {
+ [
+ {input: [1, 2, 3, 4, 5], output: [1, 4]}, // Odd -> Even
+ {input: [1, 2, 3, 4, 5], output: [1, 3, 5]}, // Odd -> Odd
+ {input: [1, 2, 3, 4], output: [1, 2, 3]}, // Even -> Odd
+ {input: [1, 2, 3, 4], output: [1, 3]}, // Even -> Even
+ ].forEach((c, i) => expectSample(i, c.input, c.output));
+ });
+
+ it('should upsample', () => {
+ [
+ {input: [1, 2, 3], output: [1, 1, 2, 2, 3, 3]}, // Odd -> Even
+ {input: [1, 2, 3], output: [1, 1, 2, 2, 3]}, // Odd -> Odd
+ {input: [1, 2], output: [1, 1, 1, 2, 2]}, // Even -> Odd
+ {input: [1, 2], output: [1, 1, 1, 2, 2, 2]}, // Even -> Even
+ ].forEach((c, i) => expectSample(i, c.input, c.output));
+ });
+
+ it('should maintain sample', () => {
+ [
+ {input: [1, 2, 3], output: [1, 2, 3]}, // Odd
+ {input: [1, 2], output: [1, 2]}, // Even
+ ].forEach((c, i) => expectSample(i, c.input, c.output));
+ });
+ });
+
+ describe('arraySeed', () => {
+ it('should create an array of given length', () => {
+ const val = 1;
+ const output = [val, val, val];
+ const result = arraySeed(val, output.length);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(output.length);
+ expect(result).toEqual(output);
+ });
+ it('should maintain pointers', () => {
+ const val = {}; // this works because `{} !== {}`, which is what toEqual checks
+ const output = [val, val, val];
+ const result = arraySeed(val, output.length);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(output.length);
+ expect(result).toEqual(output);
+ });
+ });
+
+ describe('arrayFastClone', () => {
+ it('should break pointer reference on source array', () => {
+ const val = {}; // we'll test to make sure the values maintain pointers too
+ const input = [val, val, val];
+ const result = arrayFastClone(input);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(input.length);
+ expect(result).toEqual(input); // we want the array contents to match...
+ expect(result).not.toBe(input); // ... but be a different reference
+ });
+ });
+
+ describe('arrayHasOrderChange', () => {
+ it('should flag true on B ordering difference', () => {
+ const a = [1, 2, 3];
+ const b = [3, 2, 1];
+ const result = arrayHasOrderChange(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should flag false on no ordering difference', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 3];
+ const result = arrayHasOrderChange(a, b);
+ expect(result).toBe(false);
+ });
+
+ it('should flag true on A length > B length', () => {
+ const a = [1, 2, 3, 4];
+ const b = [1, 2, 3];
+ const result = arrayHasOrderChange(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should flag true on A length < B length', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 3, 4];
+ const result = arrayHasOrderChange(a, b);
+ expect(result).toBe(true);
+ });
+ });
+
+ describe('arrayHasDiff', () => {
+ it('should flag true on A length > B length', () => {
+ const a = [1, 2, 3, 4];
+ const b = [1, 2, 3];
+ const result = arrayHasDiff(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should flag true on A length < B length', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 3, 4];
+ const result = arrayHasDiff(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should flag true on element differences', () => {
+ const a = [1, 2, 3];
+ const b = [4, 5, 6];
+ const result = arrayHasDiff(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should flag false if same but order different', () => {
+ const a = [1, 2, 3];
+ const b = [3, 1, 2];
+ const result = arrayHasDiff(a, b);
+ expect(result).toBe(false);
+ });
+
+ it('should flag false if same', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 3];
+ const result = arrayHasDiff(a, b);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('arrayDiff', () => {
+ it('should see added from A->B', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 3, 4];
+ const result = arrayDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.added).toHaveLength(1);
+ expect(result.removed).toHaveLength(0);
+ expect(result.added).toEqual([4]);
+ });
+
+ it('should see removed from A->B', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2];
+ const result = arrayDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(1);
+ expect(result.removed).toEqual([3]);
+ });
+
+ it('should see added and removed in the same set', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 4]; // note diff
+ const result = arrayDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.added).toHaveLength(1);
+ expect(result.removed).toHaveLength(1);
+ expect(result.added).toEqual([4]);
+ expect(result.removed).toEqual([3]);
+ });
+ });
+
+ describe('arrayUnion', () => {
+ it('should return a union', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 4]; // note diff
+ const result = arrayUnion(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(2);
+ expect(result).toEqual([1, 2]);
+ });
+
+ it('should return an empty array on no matches', () => {
+ const a = [1, 2, 3];
+ const b = [4, 5, 6];
+ const result = arrayUnion(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(0);
+ });
+ });
+
+ describe('arrayMerge', () => {
+ it('should merge 3 arrays with deduplication', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 4, 5]; // note missing 3
+ const c = [6, 7, 8, 9];
+ const result = arrayMerge(a, b, c);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(9);
+ expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ });
+
+ it('should deduplicate a single array', () => {
+ // dev note: this is technically an edge case, but it is described behaviour if the
+ // function is only provided one function (it'll merge the array against itself)
+ const a = [1, 1, 2, 2, 3, 3];
+ const result = arrayMerge(a);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(3);
+ expect(result).toEqual([1, 2, 3]);
+ });
+ });
+
+ describe('ArrayUtil', () => {
+ it('should maintain the pointer to the given array', () => {
+ const input = [1, 2, 3];
+ const result = new ArrayUtil(input);
+ expect(result.value).toBe(input);
+ });
+
+ it('should group appropriately', () => {
+ const input = [['a', 1], ['b', 2], ['c', 3], ['a', 4], ['a', 5], ['b', 6]];
+ const output = {
+ 'a': [['a', 1], ['a', 4], ['a', 5]],
+ 'b': [['b', 2], ['b', 6]],
+ 'c': [['c', 3]],
+ };
+ const result = new ArrayUtil(input).groupBy(p => p[0]);
+ expect(result).toBeDefined();
+ expect(result.value).toBeDefined();
+
+ const asObject = objectFromEntries(result.value.entries());
+ expect(asObject).toMatchObject(output);
+ });
+ });
+
+ describe('GroupedArray', () => {
+ it('should maintain the pointer to the given map', () => {
+ const input = new Map([
+ ['a', [1, 2, 3]],
+ ['b', [7, 8, 9]],
+ ['c', [4, 5, 6]],
+ ]);
+ const result = new GroupedArray(input);
+ expect(result.value).toBe(input);
+ });
+
+ it('should ordering by the provided key order', () => {
+ const input = new Map([
+ ['a', [1, 2, 3]],
+ ['b', [7, 8, 9]], // note counting diff
+ ['c', [4, 5, 6]],
+ ]);
+ const output = [4, 5, 6, 1, 2, 3, 7, 8, 9];
+ const keyOrder = ['c', 'a', 'b']; // note weird order to cause the `output` to be strange
+ const result = new GroupedArray(input).orderBy(keyOrder);
+ expect(result).toBeDefined();
+ expect(result.value).toBeDefined();
+ expect(result.value).toEqual(output);
+ });
+ });
+});
+
diff --git a/test/utils/enums-test.ts b/test/utils/enums-test.ts
new file mode 100644
index 0000000000..423b135f77
--- /dev/null
+++ b/test/utils/enums-test.ts
@@ -0,0 +1,67 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {getEnumValues, isEnumValue} from "../../src/utils/enums";
+
+enum TestStringEnum {
+ First = "__first__",
+ Second = "__second__",
+}
+
+enum TestNumberEnum {
+ FirstKey = 10,
+ SecondKey = 20,
+}
+
+describe('enums', () => {
+ describe('getEnumValues', () => {
+ it('should work on string enums', () => {
+ const result = getEnumValues(TestStringEnum);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(2);
+ expect(result).toEqual(['__first__', '__second__']);
+ });
+
+ it('should work on number enums', () => {
+ const result = getEnumValues(TestNumberEnum);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(2);
+ expect(result).toEqual([10, 20]);
+ });
+ });
+
+ describe('isEnumValue', () => {
+ it('should return true on values in a string enum', () => {
+ const result = isEnumValue(TestStringEnum, '__first__');
+ expect(result).toBe(true);
+ });
+
+ it('should return false on values not in a string enum', () => {
+ const result = isEnumValue(TestStringEnum, 'not a value');
+ expect(result).toBe(false);
+ });
+
+ it('should return true on values in a number enum', () => {
+ const result = isEnumValue(TestNumberEnum, 10);
+ expect(result).toBe(true);
+ });
+
+ it('should return false on values not in a number enum', () => {
+ const result = isEnumValue(TestStringEnum, 99);
+ expect(result).toBe(false);
+ });
+ });
+});
diff --git a/test/utils/iterables-test.ts b/test/utils/iterables-test.ts
new file mode 100644
index 0000000000..9b30b6241c
--- /dev/null
+++ b/test/utils/iterables-test.ts
@@ -0,0 +1,77 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {iterableDiff, iterableUnion} from "../../src/utils/iterables";
+
+describe('iterables', () => {
+ describe('iterableUnion', () => {
+ it('should return a union', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 4]; // note diff
+ const result = iterableUnion(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(2);
+ expect(result).toEqual([1, 2]);
+ });
+
+ it('should return an empty array on no matches', () => {
+ const a = [1, 2, 3];
+ const b = [4, 5, 6];
+ const result = iterableUnion(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(0);
+ });
+ });
+
+ describe('iterableDiff', () => {
+ it('should see added from A->B', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 3, 4];
+ const result = iterableDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.added).toHaveLength(1);
+ expect(result.removed).toHaveLength(0);
+ expect(result.added).toEqual([4]);
+ });
+
+ it('should see removed from A->B', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2];
+ const result = iterableDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(1);
+ expect(result.removed).toEqual([3]);
+ });
+
+ it('should see added and removed in the same set', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 4]; // note diff
+ const result = iterableDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.added).toHaveLength(1);
+ expect(result.removed).toHaveLength(1);
+ expect(result.added).toEqual([4]);
+ expect(result.removed).toEqual([3]);
+ });
+ });
+});
diff --git a/test/utils/maps-test.ts b/test/utils/maps-test.ts
new file mode 100644
index 0000000000..8764a8f2cf
--- /dev/null
+++ b/test/utils/maps-test.ts
@@ -0,0 +1,245 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {EnhancedMap, mapDiff, mapKeyChanges} from "../../src/utils/maps";
+
+describe('maps', () => {
+ describe('mapDiff', () => {
+ it('should indicate no differences when the pointers are the same', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const result = mapDiff(a, a);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(0);
+ expect(result.changed).toHaveLength(0);
+ });
+
+ it('should indicate no differences when there are none', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const b = new Map([[1, 1], [2, 2], [3, 3]]);
+ const result = mapDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(0);
+ expect(result.changed).toHaveLength(0);
+ });
+
+ it('should indicate added properties', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const b = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]);
+ const result = mapDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toHaveLength(1);
+ expect(result.removed).toHaveLength(0);
+ expect(result.changed).toHaveLength(0);
+ expect(result.added).toEqual([4]);
+ });
+
+ it('should indicate removed properties', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const b = new Map([[1, 1], [2, 2]]);
+ const result = mapDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(1);
+ expect(result.changed).toHaveLength(0);
+ expect(result.removed).toEqual([3]);
+ });
+
+ it('should indicate changed properties', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const b = new Map([[1, 1], [2, 2], [3, 4]]); // note change
+ const result = mapDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(0);
+ expect(result.changed).toHaveLength(1);
+ expect(result.changed).toEqual([3]);
+ });
+
+ it('should indicate changed, added, and removed properties', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const b = new Map([[1, 1], [2, 8], [4, 4]]); // note change
+ const result = mapDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toHaveLength(1);
+ expect(result.removed).toHaveLength(1);
+ expect(result.changed).toHaveLength(1);
+ expect(result.added).toEqual([4]);
+ expect(result.removed).toEqual([3]);
+ expect(result.changed).toEqual([2]);
+ });
+
+ it('should indicate changes for difference in pointers', () => {
+ const a = new Map([[1, {}]]); // {} always creates a new object
+ const b = new Map([[1, {}]]);
+ const result = mapDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(0);
+ expect(result.changed).toHaveLength(1);
+ expect(result.changed).toEqual([1]);
+ });
+ });
+
+ describe('mapKeyChanges', () => {
+ it('should indicate no changes for unchanged pointers', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const result = mapKeyChanges(a, a);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(0);
+ });
+
+ it('should indicate no changes for unchanged maps with different pointers', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const b = new Map([[1, 1], [2, 2], [3, 3]]);
+ const result = mapKeyChanges(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(0);
+ });
+
+ it('should indicate changes for added properties', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const b = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]);
+ const result = mapKeyChanges(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(1);
+ expect(result).toEqual([4]);
+ });
+
+ it('should indicate changes for removed properties', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]);
+ const b = new Map([[1, 1], [2, 2], [3, 3]]);
+ const result = mapKeyChanges(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(1);
+ expect(result).toEqual([4]);
+ });
+
+ it('should indicate changes for changed properties', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]);
+ const b = new Map([[1, 1], [2, 2], [3, 3], [4, 55]]);
+ const result = mapKeyChanges(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(1);
+ expect(result).toEqual([4]);
+ });
+
+ it('should indicate changes for properties with different pointers', () => {
+ const a = new Map([[1, {}]]); // {} always creates a new object
+ const b = new Map([[1, {}]]);
+ const result = mapKeyChanges(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(1);
+ expect(result).toEqual([1]);
+ });
+
+ it('should indicate changes for changed, added, and removed properties', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const b = new Map([[1, 1], [2, 8], [4, 4]]); // note change
+ const result = mapKeyChanges(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(3);
+ expect(result).toEqual([3, 4, 2]); // order irrelevant, but the test cares
+ });
+ });
+
+ describe('EnhancedMap', () => {
+ // Most of these tests will make sure it implements the Map class
+
+ it('should be empty by default', () => {
+ const result = new EnhancedMap();
+ expect(result.size).toBe(0);
+ });
+
+ it('should use the provided entries', () => {
+ const obj = {a: 1, b: 2};
+ const result = new EnhancedMap(Object.entries(obj));
+ expect(result.size).toBe(2);
+ expect(result.get('a')).toBe(1);
+ expect(result.get('b')).toBe(2);
+ });
+
+ it('should create keys if they do not exist', () => {
+ const key = 'a';
+ const val = {}; // we'll check pointers
+
+ const result = new EnhancedMap();
+ expect(result.size).toBe(0);
+
+ let get = result.getOrCreate(key, val);
+ expect(get).toBeDefined();
+ expect(get).toBe(val);
+ expect(result.size).toBe(1);
+
+ get = result.getOrCreate(key, 44); // specifically change `val`
+ expect(get).toBeDefined();
+ expect(get).toBe(val);
+ expect(result.size).toBe(1);
+
+ get = result.get(key); // use the base class function
+ expect(get).toBeDefined();
+ expect(get).toBe(val);
+ expect(result.size).toBe(1);
+ });
+
+ it('should proxy remove to delete and return it', () => {
+ const val = {};
+ const result = new EnhancedMap();
+ result.set('a', val);
+
+ expect(result.size).toBe(1);
+
+ const removed = result.remove('a');
+ expect(result.size).toBe(0);
+ expect(removed).toBeDefined();
+ expect(removed).toBe(val);
+ });
+
+ it('should support removing unknown keys', () => {
+ const val = {};
+ const result = new EnhancedMap();
+ result.set('a', val);
+
+ expect(result.size).toBe(1);
+
+ const removed = result.remove('not-a');
+ expect(result.size).toBe(1);
+ expect(removed).not.toBeDefined();
+ });
+ });
+});
diff --git a/test/utils/numbers-test.ts b/test/utils/numbers-test.ts
new file mode 100644
index 0000000000..36e7d4f7e7
--- /dev/null
+++ b/test/utils/numbers-test.ts
@@ -0,0 +1,163 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {clamp, defaultNumber, percentageOf, percentageWithin, sum} from "../../src/utils/numbers";
+
+describe('numbers', () => {
+ describe('defaultNumber', () => {
+ it('should use the default when the input is not a number', () => {
+ const def = 42;
+
+ let result = defaultNumber(null, def);
+ expect(result).toBe(def);
+
+ result = defaultNumber(undefined, def);
+ expect(result).toBe(def);
+
+ result = defaultNumber(Number.NaN, def);
+ expect(result).toBe(def);
+ });
+
+ it('should use the number when it is a number', () => {
+ const input = 24;
+ const def = 42;
+ const result = defaultNumber(input, def);
+ expect(result).toBe(input);
+ });
+ });
+
+ describe('clamp', () => {
+ it('should clamp high numbers', () => {
+ const input = 101;
+ const min = 0;
+ const max = 100;
+ const result = clamp(input, min, max);
+ expect(result).toBe(max);
+ });
+
+ it('should clamp low numbers', () => {
+ const input = -1;
+ const min = 0;
+ const max = 100;
+ const result = clamp(input, min, max);
+ expect(result).toBe(min);
+ });
+
+ it('should not clamp numbers in range', () => {
+ const input = 50;
+ const min = 0;
+ const max = 100;
+ const result = clamp(input, min, max);
+ expect(result).toBe(input);
+ });
+
+ it('should clamp floats', () => {
+ const min = -0.10;
+ const max = +0.10;
+
+ let result = clamp(-1.2, min, max);
+ expect(result).toBe(min);
+
+ result = clamp(1.2, min, max);
+ expect(result).toBe(max);
+
+ result = clamp(0.02, min, max);
+ expect(result).toBe(0.02);
+ });
+ });
+
+ describe('sum', () => {
+ it('should sum', () => { // duh
+ const result = sum(1, 2, 1, 4);
+ expect(result).toBe(8);
+ });
+ });
+
+ describe('percentageWithin', () => {
+ it('should work within 0-100', () => {
+ const result = percentageWithin(0.4, 0, 100);
+ expect(result).toBe(40);
+ });
+
+ it('should work within 0-100 when pct > 1', () => {
+ const result = percentageWithin(1.4, 0, 100);
+ expect(result).toBe(140);
+ });
+
+ it('should work within 0-100 when pct < 0', () => {
+ const result = percentageWithin(-1.4, 0, 100);
+ expect(result).toBe(-140);
+ });
+
+ it('should work with ranges other than 0-100', () => {
+ const result = percentageWithin(0.4, 10, 20);
+ expect(result).toBe(14);
+ });
+
+ it('should work with ranges other than 0-100 when pct > 1', () => {
+ const result = percentageWithin(1.4, 10, 20);
+ expect(result).toBe(24);
+ });
+
+ it('should work with ranges other than 0-100 when pct < 0', () => {
+ const result = percentageWithin(-1.4, 10, 20);
+ expect(result).toBe(-4);
+ });
+
+ it('should work with floats', () => {
+ const result = percentageWithin(0.4, 10.2, 20.4);
+ expect(result).toBe(14.28);
+ });
+ });
+
+ // These are the inverse of percentageWithin
+ describe('percentageOf', () => {
+ it('should work within 0-100', () => {
+ const result = percentageOf(40, 0, 100);
+ expect(result).toBe(0.4);
+ });
+
+ it('should work within 0-100 when val > 100', () => {
+ const result = percentageOf(140, 0, 100);
+ expect(result).toBe(1.40);
+ });
+
+ it('should work within 0-100 when val < 0', () => {
+ const result = percentageOf(-140, 0, 100);
+ expect(result).toBe(-1.40);
+ });
+
+ it('should work with ranges other than 0-100', () => {
+ const result = percentageOf(14, 10, 20);
+ expect(result).toBe(0.4);
+ });
+
+ it('should work with ranges other than 0-100 when val > 100', () => {
+ const result = percentageOf(24, 10, 20);
+ expect(result).toBe(1.4);
+ });
+
+ it('should work with ranges other than 0-100 when val < 0', () => {
+ const result = percentageOf(-4, 10, 20);
+ expect(result).toBe(-1.4);
+ });
+
+ it('should work with floats', () => {
+ const result = percentageOf(14.28, 10.2, 20.4);
+ expect(result).toBe(0.4);
+ });
+ });
+});
diff --git a/test/utils/objects-test.ts b/test/utils/objects-test.ts
new file mode 100644
index 0000000000..b7a80e6761
--- /dev/null
+++ b/test/utils/objects-test.ts
@@ -0,0 +1,262 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {
+ objectClone,
+ objectDiff,
+ objectExcluding,
+ objectFromEntries,
+ objectHasDiff,
+ objectKeyChanges,
+ objectShallowClone,
+ objectWithOnly,
+} from "../../src/utils/objects";
+
+describe('objects', () => {
+ describe('objectExcluding', () => {
+ it('should exclude the given properties', () => {
+ const input = {hello: "world", test: true};
+ const output = {hello: "world"};
+ const props = ["test", "doesnotexist"]; // we also make sure it doesn't explode on missing props
+ const result = objectExcluding(input, props); // any is to test the missing prop
+ expect(result).toBeDefined();
+ expect(result).toMatchObject(output);
+ });
+ });
+
+ describe('objectWithOnly', () => {
+ it('should exclusively use the given properties', () => {
+ const input = {hello: "world", test: true};
+ const output = {hello: "world"};
+ const props = ["hello", "doesnotexist"]; // we also make sure it doesn't explode on missing props
+ const result = objectWithOnly(input, props); // any is to test the missing prop
+ expect(result).toBeDefined();
+ expect(result).toMatchObject(output);
+ });
+ });
+
+ describe('objectShallowClone', () => {
+ it('should create a new object', () => {
+ const input = {test: 1};
+ const result = objectShallowClone(input);
+ expect(result).toBeDefined();
+ expect(result).not.toBe(input);
+ expect(result).toMatchObject(input);
+ });
+
+ it('should only clone the top level properties', () => {
+ const input = {a: 1, b: {c: 2}};
+ const result = objectShallowClone(input);
+ expect(result).toBeDefined();
+ expect(result).toMatchObject(input);
+ expect(result.b).toBe(input.b);
+ });
+
+ it('should support custom clone functions', () => {
+ const input = {a: 1, b: 2};
+ const output = {a: 4, b: 8};
+ const result = objectShallowClone(input, (k, v) => {
+ // XXX: inverted expectation for ease of assertion
+ expect(Object.keys(input)).toContain(k);
+
+ return v * 4;
+ });
+ expect(result).toBeDefined();
+ expect(result).toMatchObject(output);
+ });
+ });
+
+ describe('objectHasDiff', () => {
+ it('should return false for the same pointer', () => {
+ const a = {};
+ const result = objectHasDiff(a, a);
+ expect(result).toBe(false);
+ });
+
+ it('should return true if keys for A > keys for B', () => {
+ const a = {a: 1, b: 2};
+ const b = {a: 1};
+ const result = objectHasDiff(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should return true if keys for A < keys for B', () => {
+ const a = {a: 1};
+ const b = {a: 1, b: 2};
+ const result = objectHasDiff(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should return false if the objects are the same but different pointers', () => {
+ const a = {a: 1, b: 2};
+ const b = {a: 1, b: 2};
+ const result = objectHasDiff(a, b);
+ expect(result).toBe(false);
+ });
+
+ it('should consider pointers when testing values', () => {
+ const a = {a: {}, b: 2}; // `{}` is shorthand for `new Object()`
+ const b = {a: {}, b: 2};
+ const result = objectHasDiff(a, b);
+ expect(result).toBe(true); // even though the keys are the same, the value pointers vary
+ });
+ });
+
+ describe('objectDiff', () => {
+ it('should return empty sets for the same object', () => {
+ const a = {a: 1, b: 2};
+ const b = {a: 1, b: 2};
+ const result = objectDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toHaveLength(0);
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(0);
+ });
+
+ it('should return empty sets for the same object pointer', () => {
+ const a = {a: 1, b: 2};
+ const result = objectDiff(a, a);
+ expect(result).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toHaveLength(0);
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(0);
+ });
+
+ it('should indicate when property changes are made', () => {
+ const a = {a: 1, b: 2};
+ const b = {a: 11, b: 2};
+ const result = objectDiff(a, b);
+ expect(result.changed).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toHaveLength(1);
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(0);
+ expect(result.changed).toEqual(['a']);
+ });
+
+ it('should indicate when properties are added', () => {
+ const a = {a: 1, b: 2};
+ const b = {a: 1, b: 2, c: 3};
+ const result = objectDiff(a, b);
+ expect(result.changed).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toHaveLength(0);
+ expect(result.added).toHaveLength(1);
+ expect(result.removed).toHaveLength(0);
+ expect(result.added).toEqual(['c']);
+ });
+
+ it('should indicate when properties are removed', () => {
+ const a = {a: 1, b: 2};
+ const b = {a: 1};
+ const result = objectDiff(a, b);
+ expect(result.changed).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toHaveLength(0);
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(1);
+ expect(result.removed).toEqual(['b']);
+ });
+
+ it('should indicate when multiple aspects change', () => {
+ const a = {a: 1, b: 2, c: 3};
+ const b: (typeof a | {d: number}) = {a: 1, b: 22, d: 4};
+ const result = objectDiff(a, b);
+ expect(result.changed).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toHaveLength(1);
+ expect(result.added).toHaveLength(1);
+ expect(result.removed).toHaveLength(1);
+ expect(result.changed).toEqual(['b']);
+ expect(result.removed).toEqual(['c']);
+ expect(result.added).toEqual(['d']);
+ });
+ });
+
+ describe('objectKeyChanges', () => {
+ it('should return an empty set if no properties changed', () => {
+ const a = {a: 1, b: 2};
+ const b = {a: 1, b: 2};
+ const result = objectKeyChanges(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(0);
+ });
+
+ it('should return an empty set if no properties changed for the same pointer', () => {
+ const a = {a: 1, b: 2};
+ const result = objectKeyChanges(a, a);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(0);
+ });
+
+ it('should return properties which were changed, added, or removed', () => {
+ const a = {a: 1, b: 2, c: 3};
+ const b: (typeof a | {d: number}) = {a: 1, b: 22, d: 4};
+ const result = objectKeyChanges(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(3);
+ expect(result).toEqual(['c', 'd', 'b']); // order isn't important, but the test cares
+ });
+ });
+
+ describe('objectClone', () => {
+ it('should deep clone an object', () => {
+ const a = {
+ hello: "world",
+ test: {
+ another: "property",
+ test: 42,
+ third: {
+ prop: true,
+ },
+ },
+ };
+ const result = objectClone(a);
+ expect(result).toBeDefined();
+ expect(result).not.toBe(a);
+ expect(result).toMatchObject(a);
+ expect(result.test).not.toBe(a.test);
+ expect(result.test.third).not.toBe(a.test.third);
+ });
+ });
+
+ describe('objectFromEntries', () => {
+ it('should create an object from an array of entries', () => {
+ const output = {a: 1, b: 2, c: 3};
+ const result = objectFromEntries(Object.entries(output));
+ expect(result).toBeDefined();
+ expect(result).toMatchObject(output);
+ });
+
+ it('should maintain pointers in values', () => {
+ const output = {a: {}, b: 2, c: 3};
+ const result = objectFromEntries(Object.entries(output));
+ expect(result).toBeDefined();
+ expect(result).toMatchObject(output);
+ expect(result['a']).toBe(output.a);
+ });
+ });
+});
diff --git a/test/utils/sets-test.ts b/test/utils/sets-test.ts
new file mode 100644
index 0000000000..98dc218309
--- /dev/null
+++ b/test/utils/sets-test.ts
@@ -0,0 +1,56 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {setHasDiff} from "../../src/utils/sets";
+
+describe('sets', () => {
+ describe('setHasDiff', () => {
+ it('should flag true on A length > B length', () => {
+ const a = new Set([1, 2, 3, 4]);
+ const b = new Set([1, 2, 3]);
+ const result = setHasDiff(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should flag true on A length < B length', () => {
+ const a = new Set([1, 2, 3]);
+ const b = new Set([1, 2, 3, 4]);
+ const result = setHasDiff(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should flag true on element differences', () => {
+ const a = new Set([1, 2, 3]);
+ const b = new Set([4, 5, 6]);
+ const result = setHasDiff(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should flag false if same but order different', () => {
+ const a = new Set([1, 2, 3]);
+ const b = new Set([3, 1, 2]);
+ const result = setHasDiff(a, b);
+ expect(result).toBe(false);
+ });
+
+ it('should flag false if same', () => {
+ const a = new Set([1, 2, 3]);
+ const b = new Set([1, 2, 3]);
+ const result = setHasDiff(a, b);
+ expect(result).toBe(false);
+ });
+ });
+});
diff --git a/test/utils/test-utils.ts b/test/utils/test-utils.ts
new file mode 100644
index 0000000000..af92987a3d
--- /dev/null
+++ b/test/utils/test-utils.ts
@@ -0,0 +1,33 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { AsyncStoreWithClient } from "../../src/stores/AsyncStoreWithClient";
+
+// These methods make some use of some private methods on the AsyncStoreWithClient to simplify getting into a consistent
+// ready state without needing to wire up a dispatcher and pretend to be a js-sdk client.
+
+export const setupAsyncStoreWithClient = async (store: AsyncStoreWithClient, client: MatrixClient) => {
+ // @ts-ignore
+ store.readyStore.useUnitTestClient(client);
+ // @ts-ignore
+ await store.onReady();
+};
+
+export const resetAsyncStoreWithClient = async (store: AsyncStoreWithClient) => {
+ // @ts-ignore
+ await store.onNotReady();
+};
diff --git a/yarn.lock b/yarn.lock
index 66329cfa89..acdca26e55 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -26,6 +26,13 @@
dependencies:
"@babel/highlight" "^7.10.4"
+"@babel/code-frame@^7.12.13":
+ version "7.12.13"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
+ integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
+ dependencies:
+ "@babel/highlight" "^7.12.13"
+
"@babel/compat-data@^7.12.5", "@babel/compat-data@^7.12.7":
version "7.12.7"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.7.tgz#9329b4782a7d6bbd7eef57e11addf91ee3ef1e41"
@@ -61,6 +68,15 @@
jsesc "^2.5.1"
source-map "^0.5.0"
+"@babel/generator@^7.13.16":
+ version "7.13.16"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.16.tgz#0befc287031a201d84cdfc173b46b320ae472d14"
+ integrity sha512-grBBR75UnKOcUWMp8WoDxNsWCFl//XCK6HWTrBQKTr5SV9f5g0pNOjdyzi/DTBv12S9GnYPInIXQBTky7OXEMg==
+ dependencies:
+ "@babel/types" "^7.13.16"
+ jsesc "^2.5.1"
+ source-map "^0.5.0"
+
"@babel/helper-annotate-as-pure@^7.10.4", "@babel/helper-annotate-as-pure@^7.12.10":
version "7.12.10"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz#54ab9b000e60a93644ce17b3f37d313aaf1d115d"
@@ -130,6 +146,15 @@
"@babel/template" "^7.12.7"
"@babel/types" "^7.12.11"
+"@babel/helper-function-name@^7.12.13":
+ version "7.12.13"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a"
+ integrity sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==
+ dependencies:
+ "@babel/helper-get-function-arity" "^7.12.13"
+ "@babel/template" "^7.12.13"
+ "@babel/types" "^7.12.13"
+
"@babel/helper-get-function-arity@^7.12.10":
version "7.12.10"
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf"
@@ -137,6 +162,13 @@
dependencies:
"@babel/types" "^7.12.10"
+"@babel/helper-get-function-arity@^7.12.13":
+ version "7.12.13"
+ resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz#bc63451d403a3b3082b97e1d8b3fe5bd4091e583"
+ integrity sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==
+ dependencies:
+ "@babel/types" "^7.12.13"
+
"@babel/helper-hoist-variables@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e"
@@ -225,6 +257,13 @@
dependencies:
"@babel/types" "^7.12.11"
+"@babel/helper-split-export-declaration@^7.12.13":
+ version "7.12.13"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz#e9430be00baf3e88b0e13e6f9d4eaf2136372b05"
+ integrity sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==
+ dependencies:
+ "@babel/types" "^7.12.13"
+
"@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.12.11":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
@@ -263,11 +302,25 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
+"@babel/highlight@^7.12.13":
+ version "7.13.10"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1"
+ integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.12.11"
+ chalk "^2.0.0"
+ js-tokens "^4.0.0"
+
"@babel/parser@^7.1.0", "@babel/parser@^7.12.10", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.7.0":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79"
integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==
+"@babel/parser@^7.12.13", "@babel/parser@^7.13.16":
+ version "7.13.16"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.16.tgz#0f18179b0448e6939b1f3f5c4c355a3a9bcdfd37"
+ integrity sha512-6bAg36mCwuqLO0hbR+z7PHuqWiCeP7Dzg73OpQwsAB1Eb8HnGEz5xYBzCfbu+YjoaJsJs+qheDxVAuqbt3ILEw==
+
"@babel/plugin-proposal-async-generator-functions@^7.12.1":
version "7.12.12"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.12.tgz#04b8f24fd4532008ab4e79f788468fd5a8476566"
@@ -980,6 +1033,15 @@
"@babel/parser" "^7.12.7"
"@babel/types" "^7.12.7"
+"@babel/template@^7.12.13":
+ version "7.12.13"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327"
+ integrity sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==
+ dependencies:
+ "@babel/code-frame" "^7.12.13"
+ "@babel/parser" "^7.12.13"
+ "@babel/types" "^7.12.13"
+
"@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.10", "@babel/traverse@^7.12.12", "@babel/traverse@^7.12.5", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.4":
version "7.12.12"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376"
@@ -995,6 +1057,20 @@
globals "^11.1.0"
lodash "^4.17.19"
+"@babel/traverse@^7.13.17":
+ version "7.13.17"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.17.tgz#c85415e0c7d50ac053d758baec98b28b2ecfeea3"
+ integrity sha512-BMnZn0R+X6ayqm3C3To7o1j7Q020gWdqdyP50KEoVqaCO2c/Im7sYZSmVgvefp8TTMQ+9CtwuBp0Z1CZ8V3Pvg==
+ dependencies:
+ "@babel/code-frame" "^7.12.13"
+ "@babel/generator" "^7.13.16"
+ "@babel/helper-function-name" "^7.12.13"
+ "@babel/helper-split-export-declaration" "^7.12.13"
+ "@babel/parser" "^7.13.16"
+ "@babel/types" "^7.13.17"
+ debug "^4.1.0"
+ globals "^11.1.0"
+
"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
version "7.12.12"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299"
@@ -1004,6 +1080,14 @@
lodash "^4.17.19"
to-fast-properties "^2.0.0"
+"@babel/types@^7.12.13", "@babel/types@^7.13.16", "@babel/types@^7.13.17":
+ version "7.13.17"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.17.tgz#48010a115c9fba7588b4437dd68c9469012b38b4"
+ integrity sha512-RawydLgxbOPDlTLJNtoIypwdmAy//uQIzlKt2+iBiJaRlVuI6QLUxVAyWGNfOzp8Yu4L4lLIacoCyTNtpb4wiA==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.12.11"
+ to-fast-properties "^2.0.0"
+
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@@ -5588,8 +5672,8 @@ mathml-tag-names@^2.1.3:
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
- version "9.11.0"
- resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e277de6e3d9bbb98fbfbbedd47d86ee85f6f47e5"
+ version "10.0.0"
+ resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c8f69c0b7937b9064938c134d708c4d064b71315"
dependencies:
"@babel/runtime" "^7.12.5"
another-json "^0.2.0"
@@ -5614,6 +5698,14 @@ matrix-react-test-utils@^0.2.2:
resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
+"matrix-web-i18n@github:matrix-org/matrix-web-i18n":
+ version "1.1.2"
+ resolved "https://codeload.github.com/matrix-org/matrix-web-i18n/tar.gz/63f9119bc0bc304e83d4e8e22364caa7850e7671"
+ dependencies:
+ "@babel/parser" "^7.13.16"
+ "@babel/traverse" "^7.13.17"
+ walk "^2.3.14"
+
matrix-widget-api@^0.1.0-beta.13:
version "0.1.0-beta.13"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.13.tgz#ebddc83eaef39bbb87b621a02a35902e1a29b9ef"