Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into spaces-jump-to-room

This commit is contained in:
Jaiwanth 2021-06-29 14:31:08 +05:30
commit 746b11b24d
471 changed files with 19932 additions and 8649 deletions

View file

@ -107,8 +107,9 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
const pl = generalChat.currentState.getStateEvents("m.room.power_levels", "");
if (!pl) return this.isAdminOf(communityId);
const plContent = pl.getContent();
const invitePl = isNullOrUndefined(pl.invite) ? 50 : Number(pl.invite);
const invitePl = isNullOrUndefined(plContent.invite) ? 50 : Number(plContent.invite);
return invitePl <= myMember.powerLevel;
}
@ -126,7 +127,7 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
if (membership === EffectiveMembership.Invite) {
try {
const path = utils.encodeUri("/rooms/$roomId/group_info", {$roomId: room.roomId});
const profile = await this.matrixClient._http.authedRequest(
const profile = await this.matrixClient.http.authedRequest(
undefined, "GET", path,
undefined, undefined,
{prefix: "/_matrix/client/unstable/im.vector.custom"});
@ -159,10 +160,16 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
const data = this.matrixClient.getAccountData("im.vector.group_info." + roomId);
if (data && data.getContent()) {
return {displayName: data.getContent().name, avatarMxc: data.getContent().avatar_url};
return {
displayName: data.getContent().name,
avatarMxc: data.getContent().avatar_url,
};
}
}
return {displayName: room.name, avatarMxc: room.avatar_url};
return {
displayName: room.name,
avatarMxc: room.getMxcAvatarUrl(),
};
}
protected async onReady(): Promise<any> {

View file

@ -65,6 +65,10 @@ class FlairStore extends EventEmitter {
delete this._userGroups[userId];
}
cachedPublicisedGroups(userId) {
return this._userGroups[userId];
}
getPublicisedGroupsCached(matrixClient, userId) {
if (this._userGroups[userId]) {
return Promise.resolve(this._userGroups[userId]);

View file

@ -1,7 +1,5 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2017-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,11 +13,18 @@ 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 { Store } from 'flux/utils';
import dis from '../dispatcher/dispatcher';
import {Store} from 'flux/utils';
import { ActionPayload } from "../dispatcher/payloads";
interface IState {
deferredAction: any;
}
const INITIAL_STATE = {
deferred_action: null,
deferredAction: null,
};
/**
@ -27,39 +32,38 @@ const INITIAL_STATE = {
* store that listens for actions and updates its state accordingly, informing any
* listeners (views) of state changes.
*/
class LifecycleStore extends Store {
class LifecycleStore extends Store<ActionPayload> {
private state: IState = INITIAL_STATE;
constructor() {
super(dis);
// Initialise state
this._state = INITIAL_STATE;
}
_setState(newState) {
this._state = Object.assign(this._state, newState);
private setState(newState: Partial<IState>) {
this.state = Object.assign(this.state, newState);
this.__emitChange();
}
__onDispatch(payload) {
protected __onDispatch(payload: ActionPayload) {
switch (payload.action) {
case 'do_after_sync_prepared':
this._setState({
deferred_action: payload.deferred_action,
this.setState({
deferredAction: payload.deferred_action,
});
break;
case 'cancel_after_sync_prepared':
this._setState({
deferred_action: null,
this.setState({
deferredAction: null,
});
break;
case 'sync_state': {
case 'syncstate': {
if (payload.state !== 'PREPARED') {
break;
}
if (!this._state.deferred_action) break;
const deferredAction = Object.assign({}, this._state.deferred_action);
this._setState({
deferred_action: null,
if (!this.state.deferredAction) break;
const deferredAction = Object.assign({}, this.state.deferredAction);
this.setState({
deferredAction: null,
});
dis.dispatch(deferredAction);
break;
@ -71,8 +75,8 @@ class LifecycleStore extends Store {
}
}
reset() {
this._state = Object.assign({}, INITIAL_STATE);
private reset() {
this.state = Object.assign({}, INITIAL_STATE);
}
}

View file

@ -161,6 +161,7 @@ export default class RightPanelStore extends Store<ActionPayload> {
case Action.SetRightPanelPhase: {
let targetPhase = payload.phase;
let refireParams = payload.refireParams;
const allowClose = payload.allowClose ?? true;
// redirect to EncryptionPanel if there is an ongoing verification request
if (targetPhase === RightPanelPhases.RoomMemberInfo && payload.refireParams) {
const {member} = payload.refireParams;
@ -192,7 +193,7 @@ export default class RightPanelStore extends Store<ActionPayload> {
});
}
} else {
if (targetPhase === this.state.lastRoomPhase && !refireParams) {
if (targetPhase === this.state.lastRoomPhase && !refireParams && allowClose) {
this.setState({
showRoomPanel: !this.state.showRoomPanel,
previousPhase: null,

View file

@ -24,6 +24,7 @@ export enum RightPanelPhases {
EncryptionPanel = 'EncryptionPanel',
RoomSummary = 'RoomSummary',
Widget = 'Widget',
PinnedMessages = "PinnedMessages",
Room3pidMemberInfo = 'Room3pidMemberInfo',
// Group stuff
@ -43,6 +44,7 @@ export enum RightPanelPhases {
export const RIGHT_PANEL_PHASES_NO_ARGS = [
RightPanelPhases.RoomSummary,
RightPanelPhases.NotificationPanel,
RightPanelPhases.PinnedMessages,
RightPanelPhases.FilePanel,
RightPanelPhases.RoomMemberList,
RightPanelPhases.GroupMemberList,

View file

@ -17,17 +17,18 @@ limitations under the License.
*/
import React from "react";
import {Store} from 'flux/utils';
import {MatrixError} from "matrix-js-sdk/src/http-api";
import { Store } from 'flux/utils';
import { MatrixError } from "matrix-js-sdk/src/http-api";
import dis from '../dispatcher/dispatcher';
import {MatrixClientPeg} from '../MatrixClientPeg';
import { MatrixClientPeg } from '../MatrixClientPeg';
import * as sdk from '../index';
import Modal from '../Modal';
import { _t } from '../languageHandler';
import { getCachedRoomIDForAlias, storeRoomAliasInCache } from '../RoomAliasCache';
import {ActionPayload} from "../dispatcher/payloads";
import {retry} from "../utils/promise";
import { ActionPayload } from "../dispatcher/payloads";
import { Action } from "../dispatcher/actions";
import { retry } from "../utils/promise";
import CountlyAnalytics from "../CountlyAnalytics";
const NUM_JOIN_RETRY = 5;
@ -53,8 +54,6 @@ const INITIAL_STATE = {
// Any error that has occurred during loading
roomLoadError: null,
forwardingEvent: null,
quotingEvent: null,
replyingToEvent: null,
@ -136,24 +135,19 @@ class RoomViewStore extends Store<ActionPayload> {
break;
// join_room:
// - opts: options for joinRoom
case 'join_room':
case Action.JoinRoom:
this.joinRoom(payload);
break;
case 'join_room_error':
case Action.JoinRoomError:
this.joinRoomError(payload);
break;
case 'join_room_ready':
case Action.JoinRoomReady:
this.setState({ shouldPeek: false });
break;
case 'on_client_not_viable':
case 'on_logged_out':
this.reset();
break;
case 'forward_event':
this.setState({
forwardingEvent: payload.event,
});
break;
case 'reply_to_event':
// If currently viewed room does not match the room in which we wish to reply then change rooms
// this can happen when performing a search across all rooms
@ -173,6 +167,7 @@ class RoomViewStore extends Store<ActionPayload> {
const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog");
Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, {
roomId: payload.room_id || this.state.roomId,
initialTabId: payload.initial_tab_id,
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
break;
}
@ -186,7 +181,6 @@ class RoomViewStore extends Store<ActionPayload> {
roomAlias: payload.room_alias,
initialEventId: payload.event_id,
isInitialEventHighlighted: payload.highlighted,
forwardingEvent: null,
roomLoading: false,
roomLoadError: null,
// should peek by default
@ -206,18 +200,14 @@ class RoomViewStore extends Store<ActionPayload> {
newState.replyingToEvent = payload.replyingToEvent;
}
if (this.state.forwardingEvent) {
dis.dispatch({
action: 'send_event',
room_id: newState.roomId,
event: this.state.forwardingEvent,
});
}
this.setState(newState);
if (payload.auto_join) {
this.joinRoom(payload);
dis.dispatch({
...payload,
action: Action.JoinRoom,
roomId: payload.room_id,
});
}
} else if (payload.room_alias) {
// Try the room alias to room ID navigation cache first to avoid
@ -286,7 +276,7 @@ class RoomViewStore extends Store<ActionPayload> {
const address = this.state.roomAlias || this.state.roomId;
const viaServers = this.state.viaServers || [];
try {
await retry<void, MatrixError>(() => cli.joinRoom(address, {
await retry<any, MatrixError>(() => cli.joinRoom(address, {
viaServers,
...payload.opts,
}), NUM_JOIN_RETRY, (err) => {
@ -298,41 +288,16 @@ class RoomViewStore extends Store<ActionPayload> {
// We do *not* clear the 'joining' flag because the Room object and/or our 'joined' member event may not
// have come down the sync stream yet, and that's the point at which we'd consider the user joined to the
// room.
dis.dispatch({ action: 'join_room_ready' });
dis.dispatch({
action: Action.JoinRoomReady,
roomId: this.state.roomId,
});
} catch (err) {
dis.dispatch({
action: 'join_room_error',
action: Action.JoinRoomError,
roomId: this.state.roomId,
err: err,
});
let msg = err.message ? err.message : JSON.stringify(err);
console.log("Failed to join room:", msg);
if (err.name === "ConnectionError") {
msg = _t("There was an error joining the room");
} else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') {
msg = <div>
{_t("Sorry, your homeserver is too old to participate in this room.")}<br />
{_t("Please contact your homeserver administrator.")}
</div>;
} else if (err.httpStatus === 404) {
const invitingUserId = this.getInvitingUserId(this.state.roomId);
// only provide a better error message for invites
if (invitingUserId) {
// if the inviting user is on the same HS, there can only be one cause: they left.
if (invitingUserId.endsWith(`:${MatrixClientPeg.get().getDomain()}`)) {
msg = _t("The person who invited you already left the room.");
} else {
msg = _t("The person who invited you already left the room, or their server is offline.");
}
}
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, {
title: _t("Failed to join room"),
description: msg,
});
}
}
@ -351,6 +316,35 @@ class RoomViewStore extends Store<ActionPayload> {
joining: false,
joinError: payload.err,
});
const err = payload.err;
let msg = err.message ? err.message : JSON.stringify(err);
console.log("Failed to join room:", msg);
if (err.name === "ConnectionError") {
msg = _t("There was an error joining the room");
} else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') {
msg = <div>
{_t("Sorry, your homeserver is too old to participate in this room.")}<br />
{_t("Please contact your homeserver administrator.")}
</div>;
} else if (err.httpStatus === 404) {
const invitingUserId = this.getInvitingUserId(this.state.roomId);
// only provide a better error message for invites
if (invitingUserId) {
// if the inviting user is on the same HS, there can only be one cause: they left.
if (invitingUserId.endsWith(`:${MatrixClientPeg.get().getDomain()}`)) {
msg = _t("The person who invited you already left the room.");
} else {
msg = _t("The person who invited you already left the room, or their server is offline.");
}
}
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, {
title: _t("Failed to join room"),
description: msg,
});
}
public reset() {
@ -419,11 +413,6 @@ class RoomViewStore extends Store<ActionPayload> {
return this.state.joinError;
}
// The mxEvent if one is about to be forwarded
public getForwardingEvent() {
return this.state.forwardingEvent;
}
// The mxEvent if one is currently being replied to/quoted
public getQuotingEvent() {
return this.state.replyingToEvent;

View file

@ -15,29 +15,42 @@ limitations under the License.
*/
import EventEmitter from 'events';
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { IKeyBackupVersion } from "matrix-js-sdk/src/crypto/keybackup";
import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from '../MatrixClientPeg';
import { accessSecretStorage, AccessCancelledError } from '../SecurityManager';
import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
export const PHASE_LOADING = 0;
export const PHASE_INTRO = 1;
export const PHASE_BUSY = 2;
export const PHASE_DONE = 3; //final done stage, but still showing UX
export const PHASE_CONFIRM_SKIP = 4;
export const PHASE_FINISHED = 5; //UX can be closed
export enum Phase {
Loading = 0,
Intro = 1,
Busy = 2,
Done = 3, // final done stage, but still showing UX
ConfirmSkip = 4,
Finished = 5, // UX can be closed
}
export class SetupEncryptionStore extends EventEmitter {
static sharedInstance() {
if (!global.mx_SetupEncryptionStore) global.mx_SetupEncryptionStore = new SetupEncryptionStore();
return global.mx_SetupEncryptionStore;
private started: boolean;
public phase: Phase;
public verificationRequest: VerificationRequest;
public backupInfo: IKeyBackupVersion;
public keyId: string;
public keyInfo: ISecretStorageKeyInfo;
public hasDevicesToVerifyAgainst: boolean;
public static sharedInstance() {
if (!window.mxSetupEncryptionStore) window.mxSetupEncryptionStore = new SetupEncryptionStore();
return window.mxSetupEncryptionStore;
}
start() {
if (this._started) {
public start(): void {
if (this.started) {
return;
}
this._started = true;
this.phase = PHASE_LOADING;
this.started = true;
this.phase = Phase.Loading;
this.verificationRequest = null;
this.backupInfo = null;
@ -48,34 +61,34 @@ export class SetupEncryptionStore extends EventEmitter {
const cli = MatrixClientPeg.get();
cli.on("crypto.verification.request", this.onVerificationRequest);
cli.on('userTrustStatusChanged', this._onUserTrustStatusChanged);
cli.on('userTrustStatusChanged', this.onUserTrustStatusChanged);
const requestsInProgress = cli.getVerificationRequestsToDeviceInProgress(cli.getUserId());
if (requestsInProgress.length) {
// If there are multiple, we take the most recent. Equally if the user sends another request from
// another device after this screen has been shown, we'll switch to the new one, so this
// generally doesn't support multiple requests.
this._setActiveVerificationRequest(requestsInProgress[requestsInProgress.length - 1]);
this.setActiveVerificationRequest(requestsInProgress[requestsInProgress.length - 1]);
}
this.fetchKeyInfo();
}
stop() {
if (!this._started) {
public stop(): void {
if (!this.started) {
return;
}
this._started = false;
this.started = false;
if (this.verificationRequest) {
this.verificationRequest.off("change", this.onVerificationRequestChange);
}
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest);
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged);
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this.onUserTrustStatusChanged);
}
}
async fetchKeyInfo() {
public async fetchKeyInfo(): Promise<void> {
const cli = MatrixClientPeg.get();
const keys = await cli.isSecretStored('m.cross_signing.master', false);
if (keys === null || Object.keys(keys).length === 0) {
@ -97,15 +110,15 @@ export class SetupEncryptionStore extends EventEmitter {
if (!this.hasDevicesToVerifyAgainst && !this.keyInfo) {
// skip before we can even render anything.
this.phase = PHASE_FINISHED;
this.phase = Phase.Finished;
} else {
this.phase = PHASE_INTRO;
this.phase = Phase.Intro;
}
this.emit("update");
}
async usePassPhrase() {
this.phase = PHASE_BUSY;
public async usePassPhrase(): Promise<void> {
this.phase = Phase.Busy;
this.emit("update");
const cli = MatrixClientPeg.get();
try {
@ -120,7 +133,7 @@ export class SetupEncryptionStore extends EventEmitter {
// passphase cached for that work. This dialog itself will only wait
// on the first trust check, and the key backup restore will happen
// in the background.
await new Promise((resolve, reject) => {
await new Promise((resolve: (value?: unknown) => void, reject: (reason?: any) => void) => {
accessSecretStorage(async () => {
await cli.checkOwnCrossSigningTrust();
resolve();
@ -134,7 +147,7 @@ export class SetupEncryptionStore extends EventEmitter {
});
if (cli.getCrossSigningId()) {
this.phase = PHASE_DONE;
this.phase = Phase.Done;
this.emit("update");
}
} catch (e) {
@ -142,25 +155,25 @@ export class SetupEncryptionStore extends EventEmitter {
console.log(e);
}
// this will throw if the user hits cancel, so ignore
this.phase = PHASE_INTRO;
this.phase = Phase.Intro;
this.emit("update");
}
}
_onUserTrustStatusChanged = (userId) => {
private onUserTrustStatusChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return;
const publicKeysTrusted = MatrixClientPeg.get().getCrossSigningId();
if (publicKeysTrusted) {
this.phase = PHASE_DONE;
this.phase = Phase.Done;
this.emit("update");
}
}
onVerificationRequest = (request) => {
this._setActiveVerificationRequest(request);
public onVerificationRequest = (request: VerificationRequest): void => {
this.setActiveVerificationRequest(request);
}
onVerificationRequestChange = () => {
public onVerificationRequestChange = (): void => {
if (this.verificationRequest.cancelled) {
this.verificationRequest.off("change", this.onVerificationRequestChange);
this.verificationRequest = null;
@ -172,34 +185,34 @@ export class SetupEncryptionStore extends EventEmitter {
// cross signing to be ready to use, so wait for the user trust status to
// change (or change to DONE if it's already ready).
const publicKeysTrusted = MatrixClientPeg.get().getCrossSigningId();
this.phase = publicKeysTrusted ? PHASE_DONE : PHASE_BUSY;
this.phase = publicKeysTrusted ? Phase.Done : Phase.Busy;
this.emit("update");
}
}
skip() {
this.phase = PHASE_CONFIRM_SKIP;
public skip(): void {
this.phase = Phase.ConfirmSkip;
this.emit("update");
}
skipConfirm() {
this.phase = PHASE_FINISHED;
public skipConfirm(): void {
this.phase = Phase.Finished;
this.emit("update");
}
returnAfterSkip() {
this.phase = PHASE_INTRO;
public returnAfterSkip(): void {
this.phase = Phase.Intro;
this.emit("update");
}
done() {
this.phase = PHASE_FINISHED;
public done(): void {
this.phase = Phase.Finished;
this.emit("update");
// async - ask other clients for keys, if necessary
MatrixClientPeg.get()._crypto.cancelAndResendAllOutgoingKeyRequests();
MatrixClientPeg.get().crypto.cancelAndResendAllOutgoingKeyRequests();
}
async _setActiveVerificationRequest(request) {
private async setActiveVerificationRequest(request: VerificationRequest): Promise<void> {
if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
if (this.verificationRequest) {

View file

@ -14,40 +14,53 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ListIteratee, Many, sortBy, throttle} from "lodash";
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import { ListIteratee, Many, sortBy, throttle } from "lodash";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import {AsyncStoreWithClient} from "./AsyncStoreWithClient";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import {ActionPayload} from "../dispatcher/payloads";
import { ActionPayload } from "../dispatcher/payloads";
import RoomListStore from "./room-list/RoomListStore";
import SettingsStore from "../settings/SettingsStore";
import DMRoomMap from "../utils/DMRoomMap";
import {FetchRoomFn} from "./notifications/ListNotificationState";
import {SpaceNotificationState} from "./notifications/SpaceNotificationState";
import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore";
import {DefaultTagID} from "./room-list/models";
import {EnhancedMap, mapDiff} from "../utils/maps";
import {setHasDiff} from "../utils/sets";
import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory";
import { FetchRoomFn } from "./notifications/ListNotificationState";
import { SpaceNotificationState } from "./notifications/SpaceNotificationState";
import { RoomNotificationStateStore } from "./notifications/RoomNotificationStateStore";
import { DefaultTagID } from "./room-list/models";
import { EnhancedMap, mapDiff } from "../utils/maps";
import { setHasDiff } from "../utils/sets";
import { ISpaceSummaryEvent, ISpaceSummaryRoom } from "../components/structures/SpaceRoomDirectory";
import RoomViewStore from "./RoomViewStore";
import { Action } from "../dispatcher/actions";
import { arrayHasDiff } from "../utils/arrays";
import { objectDiff } from "../utils/objects";
import { arrayHasOrderChange } from "../utils/arrays";
import { reorderLexicographically } from "../utils/stringOrderField";
type SpaceKey = string | symbol;
interface IState {}
const ACTIVE_SPACE_LS_KEY = "mx_active_space";
export const HOME_SPACE = Symbol("home-space");
export const SUGGESTED_ROOMS = Symbol("suggested-rooms");
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
export const UPDATE_INVITED_SPACES = Symbol("invited-spaces");
export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
// Space Room ID will be emitted when a Space's children change
// Space Room ID/HOME_SPACE will be emitted when a Space's children change
export interface ISuggestedRoom extends ISpaceSummaryRoom {
viaServers: string[];
}
const MAX_SUGGESTED_ROOMS = 20;
const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "ALL_ROOMS"}`;
const homeSpaceKey = SettingsStore.getValue("feature_spaces.all_rooms") ? "ALL_ROOMS" : "HOME_SPACE";
const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || homeSpaceKey}`;
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
return arr.reduce((result, room: Room) => {
@ -56,18 +69,18 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces,
}, [[], []]);
};
// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id`
export const getOrder = (order: string, creationTs: number, roomId: string): Array<Many<ListIteratee<any>>> => {
let validatedOrder: string = null;
if (typeof order === "string" && Array.from(order).every((c: string) => {
const validOrder = (order: string): string | undefined => {
if (typeof order === "string" && order.length <= 50 && Array.from(order).every((c: string) => {
const charCode = c.charCodeAt(0);
return charCode >= 0x20 && charCode <= 0x7F;
return charCode >= 0x20 && charCode <= 0x7E;
})) {
validatedOrder = order;
return order;
}
};
return [validatedOrder, creationTs, roomId];
// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id`
export const getChildOrder = (order: string, creationTs: number, roomId: string): Array<Many<ListIteratee<any>>> => {
return [validOrder(order), creationTs, roomId];
}
const getRoomFn: FetchRoomFn = (room: Room) => {
@ -81,16 +94,19 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
// The spaces representing the roots of the various tree-like hierarchies
private rootSpaces: Room[] = [];
// The list of rooms not present in any currently joined spaces
private orphanedRooms = new Set<string>();
// Map from room ID to set of spaces which list it as a child
private parentMap = new EnhancedMap<string, Set<string>>();
// Map from spaceId to SpaceNotificationState instance representing that space
private notificationStateMap = new Map<string, SpaceNotificationState>();
// Map from SpaceKey to SpaceNotificationState instance representing that space
private notificationStateMap = new Map<SpaceKey, SpaceNotificationState>();
// Map from space key to Set of room IDs that should be shown as part of that space's filter
private spaceFilteredRooms = new Map<string, Set<string>>();
// The space currently selected in the Space Panel - if null then All Rooms is selected
private spaceFilteredRooms = new Map<SpaceKey, Set<string>>();
// The space currently selected in the Space Panel - if null then Home is selected
private _activeSpace?: Room = null;
private _suggestedRooms: ISpaceSummaryRoom[] = [];
private _suggestedRooms: ISuggestedRoom[] = [];
private _invitedSpaces = new Set<Room>();
private spaceOrderLocalEchoMap = new Map<string, string>();
public get invitedSpaces(): Room[] {
return Array.from(this._invitedSpaces);
@ -104,7 +120,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
return this._activeSpace || null;
}
public get suggestedRooms(): ISpaceSummaryRoom[] {
public get suggestedRooms(): ISuggestedRoom[] {
return this._suggestedRooms;
}
@ -147,7 +163,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
// if the space being selected is an invite then always view that invite
// else if the last viewed room in this space is joined then view that
// else view space home or home depending on what is being clicked on
if (space?.getMyMembership !== "invite" &&
if (space?.getMyMembership() !== "invite" &&
this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join"
) {
defaultDispatcher.dispatch({
@ -176,31 +192,41 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}
if (space) {
const data = await this.fetchSuggestedRooms(space);
const suggestedRooms = await this.fetchSuggestedRooms(space);
if (this._activeSpace === space) {
this._suggestedRooms = data.rooms.filter(roomInfo => {
return roomInfo.room_type !== RoomType.Space
&& this.matrixClient.getRoom(roomInfo.room_id)?.getMyMembership() !== "join";
});
this._suggestedRooms = suggestedRooms;
this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
}
}
}
public fetchSuggestedRooms = async (space: Room, limit = MAX_SUGGESTED_ROOMS) => {
public fetchSuggestedRooms = async (space: Room, limit = MAX_SUGGESTED_ROOMS): Promise<ISuggestedRoom[]> => {
try {
const data: {
rooms: ISpaceSummaryRoom[];
events: ISpaceSummaryEvent[];
} = await this.matrixClient.getSpaceSummary(space.roomId, 0, true, false, limit);
return data;
const viaMap = new EnhancedMap<string, Set<string>>();
data.events.forEach(ev => {
if (ev.type === EventType.SpaceChild && ev.content.via?.length) {
ev.content.via.forEach(via => {
viaMap.getOrCreate(ev.state_key, new Set()).add(via);
});
}
});
return data.rooms.filter(roomInfo => {
return roomInfo.room_type !== RoomType.Space
&& this.matrixClient.getRoom(roomInfo.room_id)?.getMyMembership() !== "join";
}).map(roomInfo => ({
...roomInfo,
viaServers: Array.from(viaMap.get(roomInfo.room_id) || []),
}));
} catch (e) {
console.error(e);
}
return {
rooms: [],
events: [],
};
return [];
};
public addRoomToSpace(space: Room, roomId: string, via: string[], suggested = false, autoJoin = false) {
@ -218,7 +244,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
const roomId = ev.getStateKey();
const childRoom = this.matrixClient?.getRoom(roomId);
const createTs = childRoom?.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs();
return getOrder(ev.getContent().order, createTs, roomId);
return getChildOrder(ev.getContent().order, createTs, roomId);
}).map(ev => {
return this.matrixClient.getRoom(ev.getStateKey());
}).filter(room => {
@ -240,7 +266,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
return room?.currentState.getStateEvents(EventType.SpaceParent)
.filter(ev => {
const content = ev.getContent();
if (!content?.via) return false;
if (!content?.via?.length) return false;
// TODO apply permissions check to verify that the parent mapping is valid
if (canonicalOnly && !content?.canonical) return false;
return true;
@ -255,10 +281,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}
public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => {
if (!space) {
if (!space && SettingsStore.getValue("feature_spaces.all_rooms")) {
return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId));
}
return this.spaceFilteredRooms.get(space.roomId) || new Set();
return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
};
private rebuild = throttle(() => {
@ -289,7 +315,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
});
});
const [rootSpaces] = partitionSpacesAndRooms(Array.from(unseenChildren));
const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren));
// somewhat algorithm to handle full-cycles
const detachedNodes = new Set<Room>(spaces);
@ -330,7 +356,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
// rootSpaces.push(space);
// });
this.rootSpaces = rootSpaces;
this.orphanedRooms = new Set(orphanedRooms.map(r => r.roomId));
this.rootSpaces = this.sortRootSpaces(rootSpaces);
this.parentMap = backrefs;
// if the currently selected space no longer exists, remove its selection
@ -342,14 +369,34 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
// build initial state of invited spaces as we would have missed the emitted events about the room at launch
this._invitedSpaces = new Set(invitedSpaces);
this._invitedSpaces = new Set(this.sortRootSpaces(invitedSpaces));
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
}, 100, {trailing: true, leading: true});
onSpaceUpdate = () => {
private onSpaceUpdate = () => {
this.rebuild();
}
private showInHomeSpace = (room: Room) => {
if (SettingsStore.getValue("feature_spaces.all_rooms")) return true;
if (room.isSpaceRoom()) return false;
return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space
|| DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites
};
// Update a given room due to its tag changing (e.g DM-ness or Fav-ness)
// This can only change whether it shows up in the HOME_SPACE or not
private onRoomUpdate = (room: Room) => {
if (this.showInHomeSpace(room)) {
this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId);
this.emit(HOME_SPACE);
} else if (!this.orphanedRooms.has(room.roomId)) {
this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId);
this.emit(HOME_SPACE);
}
};
private onSpaceMembersChange = (ev: MatrixEvent) => {
// skip this update if we do not have a DM with this user
if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return;
@ -363,6 +410,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
const oldFilteredRooms = this.spaceFilteredRooms;
this.spaceFilteredRooms = new Map();
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
// put all room invites in the Home Space
const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite");
this.spaceFilteredRooms.set(HOME_SPACE, new Set<string>(invites.map(room => room.roomId)));
visibleRooms.forEach(room => {
if (this.showInHomeSpace(room)) {
this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId);
}
});
}
this.rootSpaces.forEach(s => {
// traverse each space tree in DFS to build up the supersets as you go up,
// reusing results from like subtrees.
@ -378,12 +437,15 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
const roomIds = new Set(childRooms.map(r => r.roomId));
const space = this.matrixClient?.getRoom(spaceId);
// Add relevant DMs
space?.getJoinedMembers().forEach(member => {
DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => {
roomIds.add(roomId);
if (SettingsStore.getValue("feature_spaces.space_member_dms")) {
// Add relevant DMs
space?.getMembers().forEach(member => {
if (member.membership !== "join" && member.membership !== "invite") return;
DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => {
roomIds.add(roomId);
});
});
});
}
const newPath = new Set(parentPath).add(spaceId);
childSpaces.forEach(childSpace => {
@ -409,6 +471,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
// Update NotificationStates
this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => {
if (roomIds.has(room.roomId)) {
if (s !== HOME_SPACE && SettingsStore.getValue("feature_spaces.space_dm_badges")) return true;
return !DMRoomMap.shared().getUserIdForRoomId(room.roomId)
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite);
}
@ -426,8 +490,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(roomId));
}
if (!parent) {
const parents = Array.from(this.parentMap.get(roomId) || []);
parent = parents.find(p => this.matrixClient.getRoom(p));
const parentIds = Array.from(this.parentMap.get(roomId) || []);
for (const parentId of parentIds) {
const room = this.matrixClient.getRoom(parentId);
if (room) {
parent = room;
break;
}
}
}
// don't trigger a context switch when we are switching a space to match the chosen room
@ -475,6 +545,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}
};
private notifyIfOrderChanged(): void {
const rootSpaces = this.sortRootSpaces(this.rootSpaces);
if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) {
this.rootSpaces = rootSpaces;
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
}
}
private onRoomState = (ev: MatrixEvent) => {
const room = this.matrixClient.getRoom(ev.getRoomId());
if (!room) return;
@ -492,6 +570,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
// TODO confirm this after implementing parenting behaviour
if (room.isSpaceRoom()) {
this.onSpaceUpdate();
} else if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
this.onRoomUpdate(room);
}
this.emit(room.roomId);
break;
@ -504,8 +584,47 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}
};
private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEv?: MatrixEvent) => {
if (!room.isSpaceRoom()) return;
if (ev.getType() === EventType.SpaceOrder) {
this.spaceOrderLocalEchoMap.delete(room.roomId); // clear any local echo
const order = ev.getContent()?.order;
const lastOrder = lastEv?.getContent()?.order;
if (order !== lastOrder) {
this.notifyIfOrderChanged();
}
} else if (ev.getType() === EventType.Tag && !SettingsStore.getValue("feature_spaces.all_rooms")) {
// If the room was in favourites and now isn't or the opposite then update its position in the trees
const oldTags = lastEv?.getContent()?.tags || {};
const newTags = ev.getContent()?.tags || {};
if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) {
this.onRoomUpdate(room);
}
}
}
private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => {
if (ev.getType() === EventType.Direct) {
const lastContent = lastEvent.getContent();
const content = ev.getContent();
const diff = objectDiff<Record<string, string[]>>(lastContent, content);
// filter out keys which changed by reference only by checking whether the sets differ
const changed = diff.changed.filter(k => arrayHasDiff(lastContent[k], content[k]));
// DM tag changes, refresh relevant rooms
new Set([...diff.added, ...diff.removed, ...changed]).forEach(roomId => {
const room = this.matrixClient?.getRoom(roomId);
if (room) {
this.onRoomUpdate(room);
}
});
}
};
protected async reset() {
this.rootSpaces = [];
this.orphanedRooms = new Set();
this.parentMap = new EnhancedMap();
this.notificationStateMap = new Map();
this.spaceFilteredRooms = new Map();
@ -519,7 +638,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
if (this.matrixClient) {
this.matrixClient.removeListener("Room", this.onRoom);
this.matrixClient.removeListener("Room.myMembership", this.onRoom);
this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData);
this.matrixClient.removeListener("RoomState.events", this.onRoomState);
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
this.matrixClient.removeListener("accountData", this.onAccountData);
}
}
await this.reset();
}
@ -528,7 +651,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
if (!SettingsStore.getValue("feature_spaces")) return;
this.matrixClient.on("Room", this.onRoom);
this.matrixClient.on("Room.myMembership", this.onRoom);
this.matrixClient.on("Room.accountData", this.onRoomAccountData);
this.matrixClient.on("RoomState.events", this.onRoomState);
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
this.matrixClient.on("accountData", this.onAccountData);
}
await this.onSpaceUpdate(); // trigger an initial update
@ -553,7 +680,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
// 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.activeSpace && !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) {
} else if (
(!SettingsStore.getValue("feature_spaces.all_rooms") || this.activeSpace) &&
!this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)
) {
this.switchToRelatedSpace(roomId);
}
@ -568,10 +698,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.setActiveSpace(null, false);
}
break;
case Action.SwitchSpace:
if (payload.num === 0) {
this.setActiveSpace(null);
} else if (this.spacePanelSpaces.length >= payload.num) {
this.setActiveSpace(this.spacePanelSpaces[payload.num - 1]);
}
}
}
public getNotificationState(key: string): SpaceNotificationState {
public getNotificationState(key: SpaceKey): SpaceNotificationState {
if (this.notificationStateMap.has(key)) {
return this.notificationStateMap.get(key);
}
@ -602,6 +738,38 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}
childSpaces.forEach(s => this.traverseSpace(s.roomId, fn, includeRooms, newPath));
}
private getSpaceTagOrdering = (space: Room): string | undefined => {
if (this.spaceOrderLocalEchoMap.has(space.roomId)) return this.spaceOrderLocalEchoMap.get(space.roomId);
return validOrder(space.getAccountData(EventType.SpaceOrder)?.getContent()?.order);
};
private sortRootSpaces(spaces: Room[]): Room[] {
return sortBy(spaces, [this.getSpaceTagOrdering, "roomId"]);
}
private async setRootSpaceOrder(space: Room, order: string): Promise<void> {
this.spaceOrderLocalEchoMap.set(space.roomId, order);
try {
await this.matrixClient.setRoomAccountData(space.roomId, EventType.SpaceOrder, { order });
} catch (e) {
console.warn("Failed to set root space order", e);
if (this.spaceOrderLocalEchoMap.get(space.roomId) === order) {
this.spaceOrderLocalEchoMap.delete(space.roomId);
}
}
}
public moveRootSpace(fromIndex: number, toIndex: number): void {
const currentOrders = this.rootSpaces.map(this.getSpaceTagOrdering);
const changes = reorderLexicographically(currentOrders, fromIndex, toIndex);
changes.forEach(({ index, order }) => {
this.setRootSpaceOrder(this.rootSpaces[index], order);
});
this.notifyIfOrderChanged();
}
}
export default class SpaceStore {

View file

@ -0,0 +1,48 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const getSpaceCollapsedKey = (roomId: string, parents: Set<string>): string => {
const separator = "/";
let path = "";
if (parents) {
for (const entry of parents.entries()) {
path += entry + separator;
}
}
return `mx_space_collapsed_${path + roomId}`;
};
export default class SpaceTreeLevelLayoutStore {
private static internalInstance: SpaceTreeLevelLayoutStore;
public static get instance(): SpaceTreeLevelLayoutStore {
if (!SpaceTreeLevelLayoutStore.internalInstance) {
SpaceTreeLevelLayoutStore.internalInstance = new SpaceTreeLevelLayoutStore();
}
return SpaceTreeLevelLayoutStore.internalInstance;
}
public setSpaceCollapsedState(roomId: string, parents: Set<string>, collapsed: boolean) {
// XXX: localStorage doesn't allow booleans
localStorage.setItem(getSpaceCollapsedKey(roomId, parents), collapsed.toString());
}
public getSpaceCollapsedState(roomId: string, parents: Set<string>, fallback: boolean): boolean {
const collapsedLocalStorage = localStorage.getItem(getSpaceCollapsedKey(roomId, parents));
// XXX: localStorage doesn't allow booleans
return collapsedLocalStorage ? collapsedLocalStorage === "true" : fallback;
}
}

112
src/stores/UIStore.ts Normal file
View file

@ -0,0 +1,112 @@
/*
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 ResizeObserver from 'resize-observer-polyfill';
import ResizeObserverEntry from 'resize-observer-polyfill/src/ResizeObserverEntry';
export enum UI_EVENTS {
Resize = "resize"
}
export type ResizeObserverCallbackFunction = (entries: ResizeObserverEntry[]) => void;
export default class UIStore extends EventEmitter {
private static _instance: UIStore = null;
private resizeObserver: ResizeObserver;
private uiElementDimensions = new Map<string, DOMRectReadOnly>();
private trackedUiElements = new Map<Element, string>();
public windowWidth: number;
public windowHeight: number;
constructor() {
super();
// eslint-disable-next-line no-restricted-properties
this.windowWidth = window.innerWidth;
// eslint-disable-next-line no-restricted-properties
this.windowHeight = window.innerHeight;
this.resizeObserver = new ResizeObserver(this.resizeObserverCallback);
this.resizeObserver.observe(document.body);
}
public static get instance(): UIStore {
if (!UIStore._instance) {
UIStore._instance = new UIStore();
}
return UIStore._instance;
}
public static destroy(): void {
if (UIStore._instance) {
UIStore._instance.resizeObserver.disconnect();
UIStore._instance.removeAllListeners();
UIStore._instance = null;
}
}
public getElementDimensions(name: string): DOMRectReadOnly {
return this.uiElementDimensions.get(name);
}
public trackElementDimensions(name: string, element: Element): void {
this.trackedUiElements.set(element, name);
this.resizeObserver.observe(element);
}
public stopTrackingElementDimensions(name: string): void {
let trackedElement: Element;
this.trackedUiElements.forEach((trackedElementName, element) => {
if (trackedElementName === name) {
trackedElement = element;
}
});
if (trackedElement) {
this.resizeObserver.unobserve(trackedElement);
this.uiElementDimensions.delete(name);
this.trackedUiElements.delete(trackedElement);
}
}
public isTrackingElementDimensions(name: string): boolean {
return this.uiElementDimensions.has(name);
}
private resizeObserverCallback = (entries: ResizeObserverEntry[]) => {
const windowEntry = entries.find(entry => entry.target === document.body);
if (windowEntry) {
this.windowWidth = windowEntry.contentRect.width;
this.windowHeight = windowEntry.contentRect.height;
}
entries.forEach(entry => {
const trackedElementName = this.trackedUiElements.get(entry.target);
if (trackedElementName) {
this.uiElementDimensions.set(trackedElementName, entry.contentRect);
this.emit(trackedElementName, UI_EVENTS.Resize, entry);
}
});
this.emit(UI_EVENTS.Resize, entries);
}
}
window.mxUIStore = UIStore.instance;

View file

@ -16,8 +16,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";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { WidgetType } from "../widgets/WidgetType";
/**
* Acts as a place to get & set widget state, storing local echo state and

View file

@ -94,10 +94,10 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
* @param inTagId The tag ID in which the room resides
* @returns The preview, or null if none present.
*/
public getPreviewForRoom(room: Room, inTagId: TagID): string {
public async getPreviewForRoom(room: Room, inTagId: TagID): Promise<string> {
if (!room) return null; // invalid room, just return nothing
if (!this.previews.has(room.roomId)) this.generatePreview(room, inTagId);
if (!this.previews.has(room.roomId)) await this.generatePreview(room, inTagId);
const previews = this.previews.get(room.roomId);
if (!previews) return null;
@ -108,7 +108,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
return previews.get(inTagId);
}
private generatePreview(room: Room, tagId?: TagID) {
private async generatePreview(room: Room, tagId?: TagID) {
const events = room.timeline;
if (!events) return; // should only happen in tests
@ -130,6 +130,9 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
}
const event = events[i];
await this.matrixClient.decryptEventIfNeeded(event);
const previewDef = PREVIEWS[event.getType()];
if (!previewDef) continue;
if (previewDef.isState && isNullOrUndefined(event.getStateKey())) continue;
@ -173,8 +176,9 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
const event = payload.event; // TODO: Type out the dispatcher
if (!this.previews.has(event.getRoomId())) return; // not important
this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY);
const isHistoricalEvent = payload.hasOwnProperty("isLiveEvent") && !payload.isLiveEvent
if (!this.previews.has(event.getRoomId()) || isHistoricalEvent) return; // not important
await this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY);
}
}
}

View file

@ -426,8 +426,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
return; // don't do anything on rooms that aren't visible
}
if (cause === RoomUpdateCause.NewRoom && !this.prefilterConditions.every(c => c.isVisible(room))) {
return; // don't do anything on new rooms which ought not to be shown
if ((cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.PossibleTagChange) &&
!this.prefilterConditions.every(c => c.isVisible(room))
) {
return; // don't do anything on new/moved rooms which ought not to be shown
}
const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);

View file

@ -19,6 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { RoomListStoreClass } from "./RoomListStore";
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore";
import SettingsStore from "../../settings/SettingsStore";
/**
* Watches for changes in spaces to manage the filter on the provided RoomListStore
@ -28,6 +29,11 @@ export class SpaceWatcher {
private activeSpace: Room = SpaceStore.instance.activeSpace;
constructor(private store: RoomListStoreClass) {
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
this.filter = new SpaceFilterCondition();
this.updateFilter();
store.addFilter(this.filter);
}
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated);
}
@ -35,7 +41,7 @@ export class SpaceWatcher {
this.activeSpace = activeSpace;
if (this.filter) {
if (activeSpace) {
if (activeSpace || !SettingsStore.getValue("feature_spaces.all_rooms")) {
this.updateFilter();
} else {
this.store.removeFilter(this.filter);
@ -49,9 +55,11 @@ export class SpaceWatcher {
};
private updateFilter = () => {
SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => {
this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
});
if (this.activeSpace) {
SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => {
this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
});
}
this.filter.updateSpace(this.activeSpace);
};
}

View file

@ -17,6 +17,7 @@ limitations under the License.
import { Room } from "matrix-js-sdk/src/models/room";
import { TagID } from "../../models";
import { IAlgorithm } from "./IAlgorithm";
import { compare } from "../../../../utils/strings";
/**
* Sorts rooms according to the browser's determination of alphabetic.
@ -24,7 +25,7 @@ import { IAlgorithm } from "./IAlgorithm";
export class AlphabeticAlgorithm implements IAlgorithm {
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
return rooms.sort((a, b) => {
return a.name.localeCompare(b.name);
return compare(a.name, b.name);
});
}
}

View file

@ -17,7 +17,7 @@ limitations under the License.
import { Room } from "matrix-js-sdk/src/models/room";
import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
import { EventEmitter } from "events";
import { removeHiddenChars } from "matrix-js-sdk/src/utils";
import { normalize } from "matrix-js-sdk/src/utils";
import { throttle } from "lodash";
/**
@ -62,20 +62,10 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
if (!room.name) return false; // should realistically not happen: the js-sdk always calculates a name
return this.matches(room.name);
return this.matches(room.normalizedName);
}
private normalize(val: string): string {
// Note: we have to match the filter with the removeHiddenChars() room name because the
// function strips spaces and other characters (M becomes RN for example, in lowercase).
return removeHiddenChars(val.toLowerCase())
// Strip all punctuation
.replace(/[\\'!"#$%&()*+,\-./:;<=>?@[\]^_`{|}~\u2000-\u206f\u2e00-\u2e7f]/g, "")
// We also doubly convert to lowercase to work around oddities of the library.
.toLowerCase();
}
public matches(val: string): boolean {
return this.normalize(val).includes(this.normalize(this.search));
public matches(normalizedName: string): boolean {
return normalizedName.includes(normalize(this.search));
}
}

View file

@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
import { IDestroyable } from "../../../utils/IDestroyable";
import SpaceStore from "../../SpaceStore";
import SpaceStore, { HOME_SPACE } from "../../SpaceStore";
import { setHasDiff } from "../../../utils/sets";
/**
@ -29,7 +29,7 @@ import { setHasDiff } from "../../../utils/sets";
* + All DMs
*/
export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable {
private roomIds = new Set<Room>();
private roomIds = new Set<string>();
private space: Room = null;
public get kind(): FilterKind {
@ -55,12 +55,10 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
}
};
private getSpaceEventKey = (space: Room) => space.roomId;
private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE;
public updateSpace(space: Room) {
if (this.space) {
SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
}
SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate);
this.onStoreUpdate(); // initial update from the change to the space
}

View file

@ -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.
@ -51,7 +51,9 @@ import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import {getCustomTheme} from "../../theme";
import CountlyAnalytics from "../../CountlyAnalytics";
import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MatrixEvent, IEvent } from "matrix-js-sdk/src/models/event";
import { ELEMENT_CLIENT_ID } from "../../identifiers";
import { getUserLanguage } from "../../languageHandler";
// TODO: Destroy all of this code
@ -194,6 +196,9 @@ export class StopGapWidget extends EventEmitter {
currentUserId: MatrixClientPeg.get().getUserId(),
userDisplayName: OwnProfileStore.instance.displayName,
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
clientId: ELEMENT_CLIENT_ID,
clientTheme: SettingsStore.getValue("theme"),
clientLanguage: getUserLanguage(),
}, opts?.asPopout);
const parsed = new URL(templated);
@ -395,6 +400,7 @@ export class StopGapWidget extends EventEmitter {
}
private onEvent = (ev: MatrixEvent) => {
MatrixClientPeg.get().decryptEventIfNeeded(ev);
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
if (ev.getRoomId() !== this.eventListenerRoomId) return;
this.feedEvent(ev);
@ -409,7 +415,7 @@ export class StopGapWidget extends EventEmitter {
private feedEvent(ev: MatrixEvent) {
if (!this.messaging) return;
const raw = ev.event;
const raw = ev.event as IEvent;
this.messaging.feedEvent(raw).catch(e => {
console.error("Error sending event to widget: ", e);
});

View file

@ -44,6 +44,7 @@ import { CHAT_EFFECTS } from "../../effects";
import { containsEmoji } from "../../effects/utils";
import dis from "../../dispatcher/dispatcher";
import {tryTransformPermalinkToLocalHref} from "../../utils/permalinks/Permalinks";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
// TODO: Purge this from the universe
@ -144,6 +145,50 @@ export class StopGapWidgetDriver extends WidgetDriver {
return {roomId, eventId: r.event_id};
}
public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise<object[]> {
limit = limit > 0 ? Math.min(limit, 25) : 25; // arbitrary choice
const client = MatrixClientPeg.get();
const roomId = ActiveRoomObserver.activeRoomId;
const room = client.getRoom(roomId);
if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client");
const results: MatrixEvent[] = [];
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
for (let i = events.length - 1; i > 0; i--) {
if (results.length >= limit) break;
const ev = events[i];
if (ev.getType() !== eventType) continue;
if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()['msgtype']) continue;
results.push(ev);
}
return results.map(e => e.event);
}
public async readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise<object[]> {
limit = limit > 0 ? Math.min(limit, 100) : 100; // arbitrary choice
const client = MatrixClientPeg.get();
const roomId = ActiveRoomObserver.activeRoomId;
const room = client.getRoom(roomId);
if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client");
const results: MatrixEvent[] = [];
const state: Map<string, MatrixEvent> = room.currentState.events.get(eventType);
if (state) {
if (stateKey === "" || !!stateKey) {
const forKey = state.get(stateKey);
if (forKey) results.push(forKey);
} else {
results.push(...Array.from(state.values()));
}
}
return results.slice(0, limit).map(e => e.event);
}
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>) {
const oidcState = WidgetPermissionStore.instance.getOIDCState(
this.forWidget, this.forWidgetKind, this.inRoomId,

View file

@ -25,6 +25,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { SettingLevel } from "../../settings/SettingLevel";
import { arrayFastClone } from "../../utils/arrays";
import { UPDATE_EVENT } from "../AsyncStore";
import { compare } from "../../utils/strings";
export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout";
@ -240,7 +241,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
if (orderA === orderB) {
// We just need a tiebreak
return a.id.localeCompare(b.id);
return compare(a.id, b.id);
}
return orderA - orderB;
@ -331,7 +332,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
}
public getContainerWidgets(room: Room, container: Container): IApp[] {
return this.byRoom[room.roomId]?.[container]?.ordered || [];
return this.byRoom[room?.roomId]?.[container]?.ordered || [];
}
public isInContainer(room: Room, widget: IApp, container: Container): boolean {