Merge branch 'develop' into sort-imports

Signed-off-by: Aaron Raimist <aaron@raim.ist>
This commit is contained in:
Aaron Raimist 2021-12-09 08:34:20 +00:00
commit 7b94e13a84
642 changed files with 30052 additions and 8035 deletions

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.
@ -23,7 +23,6 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import { arrayHasDiff } from "../utils/arrays";
import { SettingLevel } from "../settings/SettingLevel";
import SpaceStore from "./SpaceStore";
import { Action } from "../dispatcher/actions";
import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
@ -45,6 +44,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
SettingsStore.monitorSetting("breadcrumb_rooms", null);
SettingsStore.monitorSetting("breadcrumbs", null);
SettingsStore.monitorSetting("feature_breadcrumbs_v2", null);
}
public static get instance(): BreadcrumbsStore {
@ -59,8 +59,9 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
return this.state.enabled && this.meetsRoomRequirement;
}
private get meetsRoomRequirement(): boolean {
return this.matrixClient && this.matrixClient.getVisibleRooms().length >= 20;
public get meetsRoomRequirement(): boolean {
if (SettingsStore.getValue("feature_breadcrumbs_v2")) return true;
return this.matrixClient?.getVisibleRooms().length >= 20;
}
protected async onAction(payload: ActionPayload) {
@ -70,10 +71,12 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
const settingUpdatedPayload = payload as SettingUpdatedPayload;
if (settingUpdatedPayload.settingName === 'breadcrumb_rooms') {
await this.updateRooms();
} else if (settingUpdatedPayload.settingName === 'breadcrumbs') {
} else if (settingUpdatedPayload.settingName === 'breadcrumbs' ||
settingUpdatedPayload.settingName === 'feature_breadcrumbs_v2'
) {
await this.updateState({ enabled: SettingsStore.getValue("breadcrumbs", null) });
}
} else if (payload.action === 'view_room') {
} else if (payload.action === Action.ViewRoom) {
if (payload.auto_join && !this.matrixClient.getRoom(payload.room_id)) {
// Queue the room instead of pushing it immediately. We're probably just
// waiting for a room join to complete.
@ -127,7 +130,6 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
}
private async appendRoom(room: Room) {
if (SpaceStore.spacesEnabled && room.isSpaceRoom()) return; // hide space rooms
let updated = false;
const rooms = (this.state.rooms || []).slice(); // cheap clone

View file

@ -14,21 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Room } from "matrix-js-sdk/src/models/room";
import * as utils from "matrix-js-sdk/src/utils";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { logger } from "matrix-js-sdk/src/logger";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import { ActionPayload } from "../dispatcher/payloads";
import { Action } from "../dispatcher/actions";
import { Room } from "matrix-js-sdk/src/models/room";
import { EffectiveMembership, getEffectiveMembership } from "../utils/membership";
import SettingsStore from "../settings/SettingsStore";
import * as utils from "matrix-js-sdk/src/utils";
import { UPDATE_EVENT } from "./AsyncStore";
import FlairStore from "./FlairStore";
import GroupFilterOrderStore from "./GroupFilterOrderStore";
import GroupStore from "./GroupStore";
import dis from "../dispatcher/dispatcher";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { logger } from "matrix-js-sdk/src/logger";
interface IState {
// nothing of value - we use account data
@ -149,7 +150,7 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
const chat = this.getGeneralChat(payload.tag);
if (chat) {
dis.dispatch({
action: 'view_room',
action: Action.ViewRoom,
room_id: chat.roomId,
});
}

View file

@ -17,6 +17,7 @@ import { Store } from 'flux/utils';
import { EventType } from "matrix-js-sdk/src/@types/event";
import dis from '../dispatcher/dispatcher';
import { Action } from '../dispatcher/actions';
import GroupStore from './GroupStore';
import Analytics from '../Analytics';
import * as RoomNotifs from "../RoomNotifs";
@ -55,7 +56,7 @@ class GroupFilterOrderStore extends Store {
__onDispatch(payload) { // eslint-disable-line @typescript-eslint/naming-convention
switch (payload.action) {
// Initialise state after initial sync
case 'view_room': {
case Action.ViewRoom: {
const relatedGroupIds = GroupStore.getGroupIdsForRoomId(payload.room_id);
this._updateBadges(relatedGroupIds);
break;

View file

@ -148,7 +148,7 @@ export default class RightPanelStore extends Store<ActionPayload> {
__onDispatch(payload: ActionPayload) { // eslint-disable-line @typescript-eslint/naming-convention
switch (payload.action) {
case 'view_room':
case Action.ViewRoom:
if (payload.room_id === this.lastRoomId) break; // skip this transition, probably a permalink
// fallthrough
case 'view_group':

View file

@ -25,6 +25,7 @@ export enum RightPanelPhases {
RoomSummary = 'RoomSummary',
Widget = 'Widget',
PinnedMessages = "PinnedMessages",
Timeline = "Timeline",
Room3pidMemberInfo = 'Room3pidMemberInfo',
// Group stuff
@ -53,6 +54,7 @@ export const RIGHT_PANEL_PHASES_NO_ARGS = [
RightPanelPhases.RoomMemberList,
RightPanelPhases.GroupMemberList,
RightPanelPhases.GroupRoomList,
RightPanelPhases.Timeline,
];
// Subset of phases visible in the Space View

View file

@ -106,7 +106,7 @@ class RoomViewStore extends Store<ActionPayload> {
// - event_id: '$213456782:matrix.org'
// - event_offset: 100
// - highlighted: true
case 'view_room':
case Action.ViewRoom:
this.viewRoom(payload);
break;
// for these events blank out the roomId as we are no longer in the RoomView
@ -157,7 +157,7 @@ class RoomViewStore extends Store<ActionPayload> {
if (payload.event
&& payload.event.getRoomId() !== this.state.roomId) {
dis.dispatch({
action: 'view_room',
action: Action.ViewRoom,
room_id: payload.event.getRoomId(),
replyingToEvent: payload.event,
});
@ -250,7 +250,7 @@ class RoomViewStore extends Store<ActionPayload> {
}
dis.dispatch({
action: 'view_room',
action: Action.ViewRoom,
room_id: roomId,
event_id: payload.event_id,
highlighted: payload.highlighted,

View file

@ -37,7 +37,7 @@ export default class TypingStore {
this.reset();
}
static sharedInstance(): TypingStore {
public static sharedInstance(): TypingStore {
if (window.mxTypingStore === undefined) {
window.mxTypingStore = new TypingStore();
}
@ -48,7 +48,7 @@ export default class TypingStore {
* Clears all cached typing states. Intended to be called when the
* MatrixClientPeg client changes.
*/
reset() {
public reset() {
this.typingStates = {
// "roomId": {
// isTyping: bool, // Whether the user is typing or not
@ -63,9 +63,12 @@ export default class TypingStore {
* @param {string} roomId The room ID to set the typing state in.
* @param {boolean} isTyping Whether the user is typing or not.
*/
setSelfTyping(roomId: string, isTyping: boolean): void {
public setSelfTyping(roomId: string, threadId: string | null, isTyping: boolean): void {
if (!SettingsStore.getValue('sendTypingNotifications')) return;
if (SettingsStore.getValue('lowBandwidth')) return;
// Disable typing notification for threads for the initial launch
// before we figure out a better user experience for them
if (SettingsStore.getValue("feature_thread") && threadId) return;
let currentTyping = this.typingStates[roomId];
if ((!isTyping && !currentTyping) || (currentTyping && currentTyping.isTyping === isTyping)) {
@ -96,7 +99,7 @@ export default class TypingStore {
if (!currentTyping.userTimer.isRunning()) {
currentTyping.userTimer.restart().finished().then(() => {
this.setSelfTyping(roomId, false);
this.setSelfTyping(roomId, threadId, false);
});
} else currentTyping.userTimer.restart();
}

View file

@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { GenericEchoChamber, implicitlyReverted, PROPERTY_UPDATED } from "./GenericEchoChamber";
import { getRoomNotifsState, RoomNotifState, setRoomNotifsState } from "../../RoomNotifs";
import { RoomEchoContext } from "./RoomEchoContext";
import { _t } from "../../languageHandler";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType } from "matrix-js-sdk/src/@types/event";
export enum CachedRoomKey {
NotificationVolume,
@ -47,7 +47,7 @@ export class RoomEchoChamber extends GenericEchoChamber<RoomEchoContext, CachedR
}
private onAccountData = (event: MatrixEvent) => {
if (event.getType() === "m.push_rules") {
if (event.getType() === EventType.PushRules) {
const currentVolume = this.properties.get(CachedRoomKey.NotificationVolume) as RoomNotifState;
const newVolume = getRoomNotifsState(this.context.room.roomId) as RoomNotifState;
if (currentVolume !== newVolume) {

View file

@ -20,7 +20,7 @@ import { NotificationColor } from "./NotificationColor";
import { TagID } from "../room-list/models";
import { arrayDiff } from "../../utils/arrays";
import { RoomNotificationState } from "./RoomNotificationState";
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "./NotificationState";
import { NotificationState, NotificationStateEvents } from "./NotificationState";
export type FetchRoomFn = (room: Room) => RoomNotificationState;
@ -51,11 +51,11 @@ export class ListNotificationState extends NotificationState {
const state = this.states[oldRoom.roomId];
if (!state) continue; // We likely just didn't have a badge (race condition)
delete this.states[oldRoom.roomId];
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
state.off(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
}
for (const newRoom of diff.added) {
const state = this.getRoomFn(newRoom);
state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
state.on(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
this.states[newRoom.roomId] = state;
}
@ -71,7 +71,7 @@ export class ListNotificationState extends NotificationState {
public destroy() {
super.destroy();
for (const state of Object.values(this.states)) {
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
state.off(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
}
this.states = {};
}

View file

@ -14,15 +14,23 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventEmitter } from "events";
import { NotificationColor } from "./NotificationColor";
import { IDestroyable } from "../../utils/IDestroyable";
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
export const NOTIFICATION_STATE_UPDATE = "update";
export interface INotificationStateSnapshotParams {
symbol: string | null;
count: number;
color: NotificationColor;
}
export abstract class NotificationState extends EventEmitter implements IDestroyable {
protected _symbol: string;
export enum NotificationStateEvents {
Update = "update",
}
export abstract class NotificationState extends TypedEventEmitter<NotificationStateEvents>
implements INotificationStateSnapshotParams, IDestroyable {
protected _symbol: string | null;
protected _count: number;
protected _color: NotificationColor;
@ -56,7 +64,7 @@ export abstract class NotificationState extends EventEmitter implements IDestroy
protected emitIfUpdated(snapshot: NotificationStateSnapshot) {
if (snapshot.isDifferentFrom(this)) {
this.emit(NOTIFICATION_STATE_UPDATE);
this.emit(NotificationStateEvents.Update);
}
}
@ -65,7 +73,7 @@ export abstract class NotificationState extends EventEmitter implements IDestroy
}
public destroy(): void {
this.removeAllListeners(NOTIFICATION_STATE_UPDATE);
this.removeAllListeners(NotificationStateEvents.Update);
}
}
@ -74,13 +82,13 @@ export class NotificationStateSnapshot {
private readonly count: number;
private readonly color: NotificationColor;
constructor(state: NotificationState) {
constructor(state: INotificationStateSnapshotParams) {
this.symbol = state.symbol;
this.count = state.count;
this.color = state.color;
}
public isDifferentFrom(other: NotificationState): boolean {
public isDifferentFrom(other: INotificationStateSnapshotParams): boolean {
const before = { count: this.count, symbol: this.symbol, color: this.color };
const after = { count: other.count, symbol: other.symbol, color: other.color };
return JSON.stringify(before) !== JSON.stringify(after);

View file

@ -24,6 +24,7 @@ import { FetchRoomFn, ListNotificationState } from "./ListNotificationState";
import { RoomNotificationState } from "./RoomNotificationState";
import { SummarizedNotificationState } from "./SummarizedNotificationState";
import { VisibilityProvider } from "../room-list/filters/VisibilityProvider";
import { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState";
interface IState {}
@ -31,6 +32,7 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
private static internalInstance = new RoomNotificationStateStore();
private roomMap = new Map<Room, RoomNotificationState>();
private roomThreadsMap = new Map<Room, ThreadsRoomNotificationState>();
private listMap = new Map<TagID, ListNotificationState>();
private constructor() {
@ -86,10 +88,22 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
public getRoomState(room: Room): RoomNotificationState {
if (!this.roomMap.has(room)) {
this.roomMap.set(room, new RoomNotificationState(room));
// Not very elegant, but that way we ensure that we start tracking
// threads notification at the same time at rooms.
// There are multiple entry points, and it's unclear which one gets
// called first
this.roomThreadsMap.set(room, new ThreadsRoomNotificationState(room));
}
return this.roomMap.get(room);
}
public getThreadsRoomState(room: Room): ThreadsRoomNotificationState {
if (!this.roomThreadsMap.has(room)) {
this.roomThreadsMap.set(room, new ThreadsRoomNotificationState(room));
}
return this.roomThreadsMap.get(room);
}
public static get instance(): RoomNotificationStateStore {
return RoomNotificationStateStore.internalInstance;
}

View file

@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { NotificationColor } from "./NotificationColor";
import { arrayDiff } from "../../utils/arrays";
import { RoomNotificationState } from "./RoomNotificationState";
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "./NotificationState";
import { NotificationState, NotificationStateEvents } from "./NotificationState";
import { FetchRoomFn } from "./ListNotificationState";
export class SpaceNotificationState extends NotificationState {
@ -42,11 +42,11 @@ export class SpaceNotificationState extends NotificationState {
const state = this.states[oldRoom.roomId];
if (!state) continue; // We likely just didn't have a badge (race condition)
delete this.states[oldRoom.roomId];
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
state.off(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
}
for (const newRoom of diff.added) {
const state = this.getRoomFn(newRoom);
state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
state.on(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
this.states[newRoom.roomId] = state;
}
@ -60,7 +60,7 @@ export class SpaceNotificationState extends NotificationState {
public destroy() {
super.destroy();
for (const state of Object.values(this.states)) {
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
state.off(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
}
this.states = {};
}

View file

@ -0,0 +1,69 @@
/*
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 { NotificationColor } from "./NotificationColor";
import { IDestroyable } from "../../utils/IDestroyable";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { NotificationState } from "./NotificationState";
import { Thread, ThreadEvent } from "matrix-js-sdk/src/models/thread";
import { Room } from "matrix-js-sdk/src/models/room";
export class ThreadNotificationState extends NotificationState implements IDestroyable {
protected _symbol = null;
protected _count = 0;
protected _color = NotificationColor.None;
constructor(public readonly room: Room, public readonly thread: Thread) {
super();
this.thread.on(ThreadEvent.NewReply, this.handleNewThreadReply);
this.thread.on(ThreadEvent.ViewThread, this.resetThreadNotification);
}
public destroy(): void {
super.destroy();
this.thread.off(ThreadEvent.NewReply, this.handleNewThreadReply);
this.thread.off(ThreadEvent.ViewThread, this.resetThreadNotification);
}
private handleNewThreadReply(thread: Thread, event: MatrixEvent) {
const client = MatrixClientPeg.get();
const isOwn = client.getUserId() === event.getSender();
if (!isOwn) {
const actions = client.getPushActionsForEvent(event, true);
const color = !!actions.tweaks.highlight
? NotificationColor.Red
: NotificationColor.Grey;
this.updateNotificationState(color);
}
}
private resetThreadNotification = (): void => {
this.updateNotificationState(NotificationColor.None);
};
private updateNotificationState(color: NotificationColor) {
const snapshot = this.snapshot();
this._color = color;
// finally, publish an update if needed
this.emitIfUpdated(snapshot);
}
}

View file

@ -0,0 +1,72 @@
/*
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 { IDestroyable } from "../../utils/IDestroyable";
import { Room } from "matrix-js-sdk/src/models/room";
import { NotificationState, NotificationStateEvents } from "./NotificationState";
import { Thread, ThreadEvent } from "matrix-js-sdk/src/models/thread";
import { ThreadNotificationState } from "./ThreadNotificationState";
import { NotificationColor } from "./NotificationColor";
export class ThreadsRoomNotificationState extends NotificationState implements IDestroyable {
private threadsState = new Map<Thread, ThreadNotificationState>();
protected _symbol = null;
protected _count = 0;
protected _color = NotificationColor.None;
constructor(public readonly room: Room) {
super();
this.room.on(ThreadEvent.New, this.onNewThread);
}
public destroy(): void {
super.destroy();
this.room.on(ThreadEvent.New, this.onNewThread);
for (const [, notificationState] of this.threadsState) {
notificationState.off(NotificationStateEvents.Update, this.onThreadUpdate);
}
}
private onNewThread = (thread: Thread): void => {
const notificationState = new ThreadNotificationState(this.room, thread);
this.threadsState.set(
thread,
notificationState,
);
notificationState.on(NotificationStateEvents.Update, this.onThreadUpdate);
};
private onThreadUpdate = (): void => {
let color = NotificationColor.None;
for (const [, notificationState] of this.threadsState) {
if (notificationState.color === NotificationColor.Red) {
color = NotificationColor.Red;
break;
} else if (notificationState.color === NotificationColor.Grey) {
color = NotificationColor.Grey;
}
}
this.updateNotificationState(color);
};
private updateNotificationState(color: NotificationColor): void {
const snapshot = this.snapshot();
this._color = color;
// finally, publish an update if needed
this.emitIfUpdated(snapshot);
}
}

View file

@ -37,7 +37,7 @@ import { NameFilterCondition } from "./filters/NameFilterCondition";
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
import { VisibilityProvider } from "./filters/VisibilityProvider";
import { SpaceWatcher } from "./SpaceWatcher";
import SpaceStore from "../SpaceStore";
import SpaceStore from "../spaces/SpaceStore";
import { Action } from "../../dispatcher/actions";
import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";

View file

@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomListStoreClass } from "./RoomListStore";
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
import SpaceStore, { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../SpaceStore";
import SpaceStore from "../spaces/SpaceStore";
import { MetaSpace, SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
/**
* Watches for changes in spaces to manage the filter on the provided RoomListStore
@ -26,11 +25,11 @@ import SpaceStore, { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../Spa
export class SpaceWatcher {
private readonly filter = new SpaceFilterCondition();
// we track these separately to the SpaceStore as we need to observe transitions
private activeSpace: Room = SpaceStore.instance.activeSpace;
private activeSpace: SpaceKey = SpaceStore.instance.activeSpace;
private allRoomsInHome: boolean = SpaceStore.instance.allRoomsInHome;
constructor(private store: RoomListStoreClass) {
if (!this.allRoomsInHome || this.activeSpace) {
if (SpaceWatcher.needsFilter(this.activeSpace, this.allRoomsInHome)) {
this.updateFilter();
store.addFilter(this.filter);
}
@ -38,21 +37,26 @@ export class SpaceWatcher {
SpaceStore.instance.on(UPDATE_HOME_BEHAVIOUR, this.onHomeBehaviourUpdated);
}
private onSelectedSpaceUpdated = (activeSpace?: Room, allRoomsInHome = this.allRoomsInHome) => {
private static needsFilter(spaceKey: SpaceKey, allRoomsInHome: boolean): boolean {
return !(spaceKey === MetaSpace.Home && allRoomsInHome);
}
private onSelectedSpaceUpdated = (activeSpace: SpaceKey, allRoomsInHome = this.allRoomsInHome) => {
if (activeSpace === this.activeSpace && allRoomsInHome === this.allRoomsInHome) return; // nop
const oldActiveSpace = this.activeSpace;
const oldAllRoomsInHome = this.allRoomsInHome;
const neededFilter = SpaceWatcher.needsFilter(this.activeSpace, this.allRoomsInHome);
const needsFilter = SpaceWatcher.needsFilter(activeSpace, allRoomsInHome);
this.activeSpace = activeSpace;
this.allRoomsInHome = allRoomsInHome;
if (activeSpace || !allRoomsInHome) {
if (needsFilter) {
this.updateFilter();
}
if (oldAllRoomsInHome && !oldActiveSpace) {
if (!neededFilter && needsFilter) {
this.store.addFilter(this.filter);
} else if (allRoomsInHome && !activeSpace) {
} else if (neededFilter && !needsFilter) {
this.store.removeFilter(this.filter);
}
};
@ -62,8 +66,8 @@ export class SpaceWatcher {
};
private updateFilter = () => {
if (this.activeSpace) {
SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => {
if (this.activeSpace[0] === "!") {
SpaceStore.instance.traverseSpace(this.activeSpace, roomId => {
this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
});
}

View file

@ -35,7 +35,7 @@ import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } f
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
import { getListAlgorithmInstance } from "./list-ordering";
import { VisibilityProvider } from "../filters/VisibilityProvider";
import SpaceStore from "../../SpaceStore";
import SpaceStore from "../../spaces/SpaceStore";
/**
* Fired when the Algorithm has determined a list has been updated.
@ -721,7 +721,8 @@ export class Algorithm extends EventEmitter {
cause = RoomUpdateCause.Timeline;
didTagChange = true;
} else {
cause = RoomUpdateCause.Timeline;
// This is a tag change update and no tags were changed, nothing to do!
return false;
}
if (didTagChange && isSticky) {

View file

@ -19,7 +19,8 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
import { IDestroyable } from "../../../utils/IDestroyable";
import SpaceStore, { HOME_SPACE } from "../../SpaceStore";
import SpaceStore from "../../spaces/SpaceStore";
import { MetaSpace, SpaceKey } from "../../spaces";
import { setHasDiff } from "../../../utils/sets";
/**
@ -30,7 +31,7 @@ import { setHasDiff } from "../../../utils/sets";
*/
export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable {
private roomIds = new Set<string>();
private space: Room = null;
private space: SpaceKey = MetaSpace.Home;
public get kind(): FilterKind {
return FilterKind.Prefilter;
@ -55,15 +56,13 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
}
};
private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE;
public updateSpace(space: Room) {
SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate);
public updateSpace(space: SpaceKey) {
SpaceStore.instance.off(this.space, this.onStoreUpdate);
SpaceStore.instance.on(this.space = space, this.onStoreUpdate);
this.onStoreUpdate(); // initial update from the change to the space
}
public destroy(): void {
SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
SpaceStore.instance.off(this.space, this.onStoreUpdate);
}
}

View file

@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import CallHandler from "../../../CallHandler";
import { RoomListCustomisations } from "../../../customisations/RoomList";
import VoipUserMapper from "../../../VoipUserMapper";
import SpaceStore from "../../SpaceStore";
import SpaceStore from "../../spaces/SpaceStore";
export class VisibilityProvider {
private static internalInstance: VisibilityProvider;
@ -44,7 +44,7 @@ export class VisibilityProvider {
}
if (
CallHandler.sharedInstance().getSupportsVirtualRooms() &&
CallHandler.instance.getSupportsVirtualRooms() &&
VoipUserMapper.sharedInstance().isVirtualRoom(room)
) {
return false;

View file

@ -56,7 +56,9 @@ export class MessageEventPreview implements IPreview {
}
if (hasHtml) {
body = getHtmlText(body);
const sanitised = getHtmlText(body.replace(/<br\/?>/gi, "\n")); // replace line breaks before removing them
// run it through DOMParser to fixup encoded html entities
body = new DOMParser().parseFromString(sanitised, "text/html").documentElement.textContent;
}
body = sanitizeForTranslation(body);

View file

@ -18,55 +18,52 @@ 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 { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
import { IRoomCapability } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
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 RoomViewStore from "./RoomViewStore";
import { Action } from "../dispatcher/actions";
import { arrayHasDiff, arrayHasOrderChange } from "../utils/arrays";
import { objectDiff } from "../utils/objects";
import { reorderLexicographically } from "../utils/stringOrderField";
import { TAG_ORDER } from "../components/views/rooms/RoomList";
import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
type SpaceKey = string | symbol;
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
import defaultDispatcher from "../../dispatcher/dispatcher";
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 RoomViewStore from "../RoomViewStore";
import { Action } from "../../dispatcher/actions";
import { arrayHasDiff, arrayHasOrderChange } from "../../utils/arrays";
import { objectDiff } from "../../utils/objects";
import { reorderLexicographically } from "../../utils/stringOrderField";
import { TAG_ORDER } from "../../components/views/rooms/RoomList";
import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";
import {
ISuggestedRoom,
MetaSpace,
SpaceKey,
UPDATE_HOME_BEHAVIOUR,
UPDATE_INVITED_SPACES,
UPDATE_SELECTED_SPACE,
UPDATE_SUGGESTED_ROOMS,
UPDATE_TOP_LEVEL_SPACES,
} from ".";
import { getCachedRoomIDForAlias } from "../../RoomAliasCache";
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");
export const UPDATE_HOME_BEHAVIOUR = Symbol("home-behaviour");
// Space Room ID/HOME_SPACE will be emitted when a Space's children change
export interface ISuggestedRoom extends IHierarchyRoom {
viaServers: string[];
}
const metaSpaceOrder: MetaSpace[] = [MetaSpace.Home, MetaSpace.Favourites, MetaSpace.People, MetaSpace.Orphans];
const MAX_SUGGESTED_ROOMS = 20;
// This setting causes the page to reload and can be costly if read frequently, so read it here only
const spacesEnabled = !SettingsStore.getValue("showCommunitiesInsteadOfSpaces");
const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "HOME_SPACE"}`;
const getSpaceContextKey = (space: SpaceKey) => `mx_space_context_${space}`;
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
return arr.reduce((result, room: Room) => {
@ -104,30 +101,41 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
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<SpaceKey, Set<string>>();
// The space currently selected in the Space Panel - if null then Home is selected
private _activeSpace?: Room = null;
// The space currently selected in the Space Panel
private _activeSpace?: SpaceKey = MetaSpace.Home; // set properly by onReady
private _suggestedRooms: ISuggestedRoom[] = [];
private _invitedSpaces = new Set<Room>();
private spaceOrderLocalEchoMap = new Map<string, string>();
private _restrictedJoinRuleSupport?: IRoomCapability;
private _allRoomsInHome: boolean = SettingsStore.getValue("Spaces.allRoomsInHome");
private _enabledMetaSpaces: MetaSpace[] = []; // set by onReady
constructor() {
super(defaultDispatcher, {});
SettingsStore.monitorSetting("Spaces.allRoomsInHome", null);
SettingsStore.monitorSetting("Spaces.enabledMetaSpaces", null);
}
public get invitedSpaces(): Room[] {
return Array.from(this._invitedSpaces);
}
public get enabledMetaSpaces(): MetaSpace[] {
return this._enabledMetaSpaces;
}
public get spacePanelSpaces(): Room[] {
return this.rootSpaces;
}
public get activeSpace(): Room | null {
return this._activeSpace || null;
public get activeSpace(): SpaceKey {
return this._activeSpace;
}
public get activeSpaceRoom(): Room | null {
if (this._activeSpace[0] !== "!") return null;
return this.matrixClient?.getRoom(this._activeSpace);
}
public get suggestedRooms(): ISuggestedRoom[] {
@ -138,12 +146,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
return this._allRoomsInHome;
}
public setActiveRoomInSpace(space: Room | null): void {
if (space && !space.isSpaceRoom()) return;
public setActiveRoomInSpace(space: SpaceKey): void {
if (space[0] === "!" && !this.matrixClient?.getRoom(space)?.isSpaceRoom()) return;
if (space !== this.activeSpace) this.setActiveSpace(space);
if (space) {
const roomId = this.getNotificationState(space.roomId).getFirstRoomWithNotifications();
const roomId = this.getNotificationState(space).getFirstRoomWithNotifications();
defaultDispatcher.dispatch({
action: "view_room",
room_id: roomId,
@ -183,12 +191,20 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
* @param contextSwitch whether to switch the user's context,
* should not be done when the space switch is done implicitly due to another event like switching room.
*/
public setActiveSpace(space: Room | null, contextSwitch = true) {
if (!this.matrixClient || space === this.activeSpace || (space && !space.isSpaceRoom())) return;
public setActiveSpace(space: SpaceKey, contextSwitch = true) {
if (!space || !this.matrixClient || space === this.activeSpace) return;
let cliSpace: Room;
if (space[0] === "!") {
cliSpace = this.matrixClient.getRoom(space);
if (!cliSpace?.isSpaceRoom()) return;
} else if (!this.enabledMetaSpaces.includes(space as MetaSpace)) {
return;
}
this._activeSpace = space;
this.emit(UPDATE_SELECTED_SPACE, this.activeSpace);
this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []);
this.emit(UPDATE_SUGGESTED_ROOMS, this._suggestedRooms = []);
if (contextSwitch) {
// view last selected room from space
@ -197,7 +213,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 (cliSpace?.getMyMembership() !== "invite" &&
this.matrixClient.getRoom(roomId)?.getMyMembership() === "join" &&
this.getSpaceFilteredRoomIds(space).has(roomId)
) {
@ -206,36 +222,33 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
room_id: roomId,
context_switch: true,
});
} else if (space) {
} else if (cliSpace) {
defaultDispatcher.dispatch({
action: "view_room",
room_id: space.roomId,
room_id: space,
context_switch: true,
});
} else {
defaultDispatcher.dispatch({
action: "view_home_page",
context_switch: true,
});
}
}
// persist space selected
if (space) {
window.localStorage.setItem(ACTIVE_SPACE_LS_KEY, space.roomId);
} else {
window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY);
}
window.localStorage.setItem(ACTIVE_SPACE_LS_KEY, space);
if (space) {
this.loadSuggestedRooms(space);
if (cliSpace) {
this.loadSuggestedRooms(cliSpace);
}
}
private async loadSuggestedRooms(space: Room): Promise<void> {
const suggestedRooms = await this.fetchSuggestedRooms(space);
if (this._activeSpace === space) {
if (this._activeSpace === space.roomId) {
this._suggestedRooms = suggestedRooms;
this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
this.emit(UPDATE_SUGGESTED_ROOMS, this._suggestedRooms);
}
}
@ -336,11 +349,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
return this.parentMap.get(roomId) || new Set();
}
public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => {
if (!space && this.allRoomsInHome) {
public getSpaceFilteredRoomIds = (space: SpaceKey): Set<string> => {
if (space === MetaSpace.Home && this.allRoomsInHome) {
return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId));
}
return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
return this.spaceFilteredRooms.get(space) || new Set();
};
private rebuild = throttle(() => {
@ -419,12 +432,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.parentMap = backrefs;
// if the currently selected space no longer exists, remove its selection
if (this._activeSpace && detachedNodes.has(this._activeSpace)) {
this.setActiveSpace(null, false);
if (this._activeSpace[0] === "!" && detachedNodes.has(this.matrixClient.getRoom(this._activeSpace))) {
this.goToFirstSpace();
}
this.onRoomsUpdate(); // TODO only do this if a change has happened
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces, this.enabledMetaSpaces);
// build initial state of invited spaces as we would have missed the emitted events about the room at launch
this._invitedSpaces = new Set(this.sortRootSpaces(invitedSpaces));
@ -439,19 +452,22 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
if (this.allRoomsInHome) 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
|| DMRoomMap.shared().getUserIdForRoomId(room.roomId); // put all DMs in the Home Space
};
// 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);
const enabledMetaSpaces = new Set(this.enabledMetaSpaces);
// TODO more metaspace stuffs
if (enabledMetaSpaces.has(MetaSpace.Home)) {
if (this.showInHomeSpace(room)) {
this.spaceFilteredRooms.get(MetaSpace.Home)?.add(room.roomId);
this.emit(MetaSpace.Home);
} else if (!this.orphanedRooms.has(room.roomId)) {
this.spaceFilteredRooms.get(MetaSpace.Home)?.delete(room.roomId);
this.emit(MetaSpace.Home);
}
}
};
@ -468,18 +484,41 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
const oldFilteredRooms = this.spaceFilteredRooms;
this.spaceFilteredRooms = new Map();
if (!this.allRoomsInHome) {
const enabledMetaSpaces = new Set(this.enabledMetaSpaces);
// populate the Home metaspace if it is enabled and is not set to all rooms
if (enabledMetaSpaces.has(MetaSpace.Home) && !this.allRoomsInHome) {
// 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)));
this.spaceFilteredRooms.set(MetaSpace.Home, new Set(invites.map(r => r.roomId)));
visibleRooms.forEach(room => {
if (this.showInHomeSpace(room)) {
this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId);
this.spaceFilteredRooms.get(MetaSpace.Home).add(room.roomId);
}
});
}
// populate the Favourites metaspace if it is enabled
if (enabledMetaSpaces.has(MetaSpace.Favourites)) {
const favourites = visibleRooms.filter(r => r.tags[DefaultTagID.Favourite]);
this.spaceFilteredRooms.set(MetaSpace.Favourites, new Set(favourites.map(r => r.roomId)));
}
// populate the People metaspace if it is enabled
if (enabledMetaSpaces.has(MetaSpace.People)) {
const people = visibleRooms.filter(r => DMRoomMap.shared().getUserIdForRoomId(r.roomId));
this.spaceFilteredRooms.set(MetaSpace.People, new Set(people.map(r => r.roomId)));
}
// populate the Orphans metaspace if it is enabled
if (enabledMetaSpaces.has(MetaSpace.Orphans)) {
const orphans = visibleRooms.filter(r => {
// filter out DMs and rooms with >0 parents
return !this.parentMap.get(r.roomId)?.size && !DMRoomMap.shared().getUserIdForRoomId(r.roomId);
});
this.spaceFilteredRooms.set(MetaSpace.Orphans, new Set(orphans.map(r => r.roomId)));
}
const hiddenChildren = new EnhancedMap<string, Set<string>>();
visibleRooms.forEach(room => {
if (room.getMyMembership() !== "join") return;
@ -539,15 +578,23 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.emit(k);
});
let dmBadgeSpace: MetaSpace;
// only show badges on dms on the most relevant space if such exists
if (enabledMetaSpaces.has(MetaSpace.People)) {
dmBadgeSpace = MetaSpace.People;
} else if (enabledMetaSpaces.has(MetaSpace.Home)) {
dmBadgeSpace = MetaSpace.Home;
}
this.spaceFilteredRooms.forEach((roomIds, s) => {
if (this.allRoomsInHome && s === HOME_SPACE) return; // we'll be using the global notification state, skip
if (this.allRoomsInHome && s === MetaSpace.Home) return; // we'll be using the global notification state, skip
// Update NotificationStates
this.getNotificationState(s).setRooms(visibleRooms.filter(room => {
if (!roomIds.has(room.roomId) || room.isSpaceRoom()) return false;
if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
return s === HOME_SPACE;
if (dmBadgeSpace && DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
return s === dmBadgeSpace;
}
return true;
@ -558,23 +605,22 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
private switchToRelatedSpace = (roomId: string) => {
if (this.suggestedRooms.find(r => r.room_id === roomId)) return;
let parent = this.getCanonicalParent(roomId);
// try to find the canonical parent first
let parent: SpaceKey = this.getCanonicalParent(roomId)?.roomId;
// otherwise, try to find a root space which contains this room
if (!parent) {
parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(roomId));
parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(roomId))?.roomId;
}
// otherwise, try to find a metaspace which contains this room
if (!parent) {
const parentIds = Array.from(this.parentMap.get(roomId) || []);
for (const parentId of parentIds) {
const room = this.matrixClient.getRoom(parentId);
if (room) {
parent = room;
break;
}
}
// search meta spaces in reverse as Home is the first and least specific one
parent = [...this.enabledMetaSpaces].reverse().find(s => this.getSpaceFilteredRoomIds(s).has(roomId));
}
// don't trigger a context switch when we are switching a space to match the chosen room
this.setActiveSpace(parent || null, false);
this.setActiveSpace(parent ?? MetaSpace.Home, false); // TODO
};
private onRoom = (room: Room, newMembership?: string, oldMembership?: string) => {
@ -596,7 +642,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
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);
this.emit(UPDATE_SUGGESTED_ROOMS, this._suggestedRooms);
}
// if the room currently being viewed was just joined then switch to its related space
@ -621,10 +667,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
if (membership === "join" && room.roomId === RoomViewStore.getRoomId()) {
// if the user was looking at the space and then joined: select that space
this.setActiveSpace(room, false);
} else if (membership === "leave" && room.roomId === this.activeSpace?.roomId) {
this.setActiveSpace(room.roomId, false);
} else if (membership === "leave" && room.roomId === this.activeSpace) {
// user's active space has gone away, go back to home
this.setActiveSpace(null, true);
this.goToFirstSpace(true);
}
};
@ -632,7 +678,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
const rootSpaces = this.sortRootSpaces(this.rootSpaces);
if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) {
this.rootSpaces = rootSpaces;
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces, this.enabledMetaSpaces);
}
}
@ -647,7 +693,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.emit(room.roomId);
}
if (room === this.activeSpace && // current space
if (room.roomId === this.activeSpace && // current space
this.matrixClient.getRoom(ev.getStateKey())?.getMyMembership() !== "join" && // target not joined
ev.getPrevContent().suggested !== ev.getContent().suggested // suggested flag changed
) {
@ -693,7 +739,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
if (order !== lastOrder) {
this.notifyIfOrderChanged();
}
} else if (ev.getType() === EventType.Tag && !this.allRoomsInHome) {
} else if (ev.getType() === EventType.Tag) {
// 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 || {};
@ -703,9 +749,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}
};
private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => {
private onAccountData = (ev: MatrixEvent, prevEvent?: MatrixEvent) => {
if (!this.allRoomsInHome && ev.getType() === EventType.Direct) {
const lastContent = lastEvent.getContent();
const lastContent = prevEvent?.getContent() ?? {};
const content = ev.getContent();
const diff = objectDiff<Record<string, string[]>>(lastContent, content);
@ -727,9 +773,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.parentMap = new EnhancedMap();
this.notificationStateMap = new Map();
this.spaceFilteredRooms = new Map();
this._activeSpace = null;
this._activeSpace = MetaSpace.Home; // set properly by onReady
this._suggestedRooms = [];
this._invitedSpaces = new Set();
this._enabledMetaSpaces = [];
}
protected async onNotReady() {
@ -759,16 +806,27 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
?.["m.room_versions"]?.["org.matrix.msc3244.room_capabilities"]?.["restricted"];
});
const enabledMetaSpaces = SettingsStore.getValue("Spaces.enabledMetaSpaces");
this._enabledMetaSpaces = metaSpaceOrder.filter(k => enabledMetaSpaces[k]) as MetaSpace[];
await this.onSpaceUpdate(); // trigger an initial update
// restore selected state from last session if any and still valid
const lastSpaceId = window.localStorage.getItem(ACTIVE_SPACE_LS_KEY);
if (lastSpaceId) {
if (lastSpaceId && (
lastSpaceId[0] === "!" ? this.matrixClient.getRoom(lastSpaceId) : enabledMetaSpaces[lastSpaceId]
)) {
// don't context switch here as it may break permalinks
this.setActiveSpace(this.matrixClient.getRoom(lastSpaceId), false);
this.setActiveSpace(lastSpaceId, false);
} else {
this.goToFirstSpace();
}
}
private goToFirstSpace(contextSwitch = false) {
this.setActiveSpace(this.enabledMetaSpaces[0] ?? this.spacePanelSpaces[0]?.roomId, contextSwitch);
}
protected async onAction(payload: ActionPayload) {
if (!spacesEnabled) return;
switch (payload.action) {
@ -776,17 +834,20 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
// Don't auto-switch rooms when reacting to a context-switch
// as this is not helpful and can create loops of rooms/space switching
if (payload.context_switch) break;
let roomId = payload.room_id;
if (payload.room_alias && !roomId) {
roomId = getCachedRoomIDForAlias(payload.room_alias);
}
if (!roomId) return; // we'll get re-fired with the room ID shortly
const roomId = payload.room_id;
const room = this.matrixClient?.getRoom(roomId);
if (room?.isSpaceRoom()) {
// 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.allRoomsInHome || this.activeSpace) &&
!this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)
) {
this.setActiveSpace(room.roomId, false);
} else if (!this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) {
this.switchToRelatedSpace(roomId);
}
@ -797,32 +858,62 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
break;
}
case "after_leave_room":
if (this._activeSpace && payload.room_id === this._activeSpace.roomId) {
this.setActiveSpace(null, false);
case "view_home_page":
if (!payload.context_switch && this.enabledMetaSpaces.includes(MetaSpace.Home)) {
this.setActiveSpace(MetaSpace.Home, false);
window.localStorage.setItem(getSpaceContextKey(this.activeSpace), "");
}
break;
case Action.SwitchSpace:
// 1 is Home, 2-9 are the spaces after Home
if (payload.num === 1) {
this.setActiveSpace(null);
} else if (payload.num > 0 && this.spacePanelSpaces.length > payload.num - 2) {
this.setActiveSpace(this.spacePanelSpaces[payload.num - 2]);
case "after_leave_room":
if (this._activeSpace[0] === "!" && payload.room_id === this._activeSpace) {
// User has left the current space, go to first space
this.goToFirstSpace();
}
break;
case Action.SwitchSpace: {
// Metaspaces start at 1, Spaces follow
if (payload.num < 1 || payload.num > 9) break;
const numMetaSpaces = this.enabledMetaSpaces.length;
if (payload.num <= numMetaSpaces) {
this.setActiveSpace(this.enabledMetaSpaces[payload.num - 1]);
} else if (this.spacePanelSpaces.length > payload.num - numMetaSpaces - 1) {
this.setActiveSpace(this.spacePanelSpaces[payload.num - numMetaSpaces - 1].roomId);
}
break;
}
case Action.SettingUpdated: {
const settingUpdatedPayload = payload as SettingUpdatedPayload;
if (settingUpdatedPayload.settingName === "Spaces.allRoomsInHome") {
const newValue = SettingsStore.getValue("Spaces.allRoomsInHome");
if (this.allRoomsInHome !== newValue) {
this._allRoomsInHome = newValue;
this.emit(UPDATE_HOME_BEHAVIOUR, this.allRoomsInHome);
this.rebuild(); // rebuild everything
switch (settingUpdatedPayload.settingName) {
case "Spaces.allRoomsInHome": {
const newValue = SettingsStore.getValue("Spaces.allRoomsInHome");
if (this.allRoomsInHome !== newValue) {
this._allRoomsInHome = newValue;
this.emit(UPDATE_HOME_BEHAVIOUR, this.allRoomsInHome);
this.rebuild(); // rebuild everything
}
break;
}
case "Spaces.enabledMetaSpaces": {
const newValue = SettingsStore.getValue("Spaces.enabledMetaSpaces");
const enabledMetaSpaces = metaSpaceOrder.filter(k => newValue[k]) as MetaSpace[];
if (arrayHasDiff(this._enabledMetaSpaces, enabledMetaSpaces)) {
this._enabledMetaSpaces = enabledMetaSpaces;
// if a metaspace currently being viewed was remove, go to another one
if (this.activeSpace[0] !== "!" &&
!enabledMetaSpaces.includes(this.activeSpace as MetaSpace)
) {
this.goToFirstSpace();
}
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces, this.enabledMetaSpaces);
this.rebuild(); // rebuild everything
}
break;
}
}
break;
}
}
}

View file

@ -0,0 +1,55 @@
/*
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 { Room } from "matrix-js-sdk/src/models/room";
import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
import { _t } from "../../languageHandler";
// The consts & types are moved out here to prevent cyclical imports
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");
export const UPDATE_HOME_BEHAVIOUR = Symbol("home-behaviour");
export const UPDATE_SUGGESTED_ROOMS = Symbol("suggested-rooms");
// Space Key will be emitted when a Space's children change
export enum MetaSpace {
Home = "home-space",
Favourites = "favourites-space",
People = "people-space",
Orphans = "orphans-space",
}
export const getMetaSpaceName = (spaceKey: MetaSpace, allRoomsInHome = false): string => {
switch (spaceKey) {
case MetaSpace.Home:
return allRoomsInHome ? _t("All rooms") : _t("Home");
case MetaSpace.Favourites:
return _t("Favourites");
case MetaSpace.People:
return _t("People");
case MetaSpace.Orphans:
return _t("Other rooms");
}
};
export type SpaceKey = MetaSpace | Room["roomId"];
export interface ISuggestedRoom extends IHierarchyRoom {
viaServers: string[];
}

View file

@ -48,6 +48,7 @@ import { WidgetType } from "../../widgets/WidgetType";
import ActiveWidgetStore from "../ActiveWidgetStore";
import { objectShallowClone } from "../../utils/objects";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions";
import { ElementWidgetActions, IViewRoomApiRequest } from "./ElementWidgetActions";
import { ModalWidgetStore } from "../ModalWidgetStore";
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
@ -291,7 +292,7 @@ export class StopGapWidget extends EventEmitter {
// at this point we can change rooms, so do that
defaultDispatcher.dispatch({
action: 'view_room',
action: Action.ViewRoom,
room_id: targetRoomId,
});

View file

@ -29,11 +29,6 @@ import {
WidgetEventCapability,
WidgetKind,
} from "matrix-widget-api";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { IEvent, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import { iterableDiff, iterableUnion } from "../../utils/iterables";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import ActiveRoomObserver from "../../ActiveRoomObserver";
@ -43,10 +38,16 @@ import WidgetCapabilitiesPromptDialog from "../../components/views/dialogs/Widge
import { WidgetPermissionCustomisations } from "../../customisations/WidgetPermissions";
import { OIDCState, WidgetPermissionStore } from "./WidgetPermissionStore";
import { WidgetType } from "../../widgets/WidgetType";
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
import { CHAT_EFFECTS } from "../../effects";
import { containsEmoji } from "../../effects/utils";
import dis from "../../dispatcher/dispatcher";
import { tryTransformPermalinkToLocalHref } from "../../utils/permalinks/Permalinks";
import { IContent, IEvent, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import SettingsStore from "../../settings/SettingsStore";
// TODO: Purge this from the universe
@ -141,7 +142,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
public async sendEvent(
eventType: string,
content: any,
content: IContent,
stateKey: string = null,
targetRoomId: string = null,
): Promise<ISendEventDetails> {
@ -164,7 +165,12 @@ export class StopGapWidgetDriver extends WidgetDriver {
if (eventType === EventType.RoomMessage) {
CHAT_EFFECTS.forEach((effect) => {
if (containsEmoji(content, effect.emojis)) {
dis.dispatch({ action: `effects.${effect.command}` });
// For initial threads launch, chat effects are disabled
// see #19731
const isNotThread = content["m.relates_to"].rel_type !== RelationType.Thread;
if (!SettingsStore.getValue("feature_thread") || isNotThread) {
dis.dispatch({ action: `effects.${effect.command}` });
}
}
});
}

View file

@ -39,8 +39,7 @@ export enum Container {
// changes needed", though this may change in the future.
Right = "right",
// ... more as needed. Note that most of this code assumes that there
// are only two containers, and that only the top container is special.
Center = "center"
}
export interface IStoredLayout {
@ -175,7 +174,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
}
};
private recalculateRoom(room: Room) {
public recalculateRoom(room: Room) {
const widgets = WidgetStore.instance.getApps(room.roomId);
if (!widgets?.length) {
this.byRoom[room.roomId] = {};
@ -196,18 +195,26 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
}
const roomLayout: ILayoutStateEvent = layoutEv ? layoutEv.getContent() : null;
// We essentially just need to find the top container's widgets because we
// only have two containers. Anything not in the top widget by the end of this
// function will go into the right container.
// We filter for the center container first.
// (An error is raised, if there are multiple widgets marked for the center container)
// For the right and top container multiple widgets are allowed.
const topWidgets: IApp[] = [];
const rightWidgets: IApp[] = [];
const centerWidgets: IApp[] = [];
for (const widget of widgets) {
const stateContainer = roomLayout?.widgets?.[widget.id]?.container;
const manualContainer = userLayout?.widgets?.[widget.id]?.container;
const isLegacyPinned = !!legacyPinned?.[widget.id];
const defaultContainer = WidgetType.JITSI.matches(widget.type) ? Container.Top : Container.Right;
if ((manualContainer) ? manualContainer === Container.Center : stateContainer === Container.Center) {
if (centerWidgets.length) {
console.error("Tried to push a second widget into the center container");
} else {
centerWidgets.push(widget);
}
// The widget won't need to be put in any other container.
continue;
}
let targetContainer = defaultContainer;
if (!!manualContainer || !!stateContainer) {
targetContainer = (manualContainer) ? manualContainer : stateContainer;
@ -324,6 +331,11 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
ordered: rightWidgets,
};
}
if (centerWidgets.length) {
this.byRoom[room.roomId][Container.Center] = {
ordered: centerWidgets,
};
}
const afterChanges = JSON.stringify(this.byRoom[room.roomId]);
if (afterChanges !== beforeChanges) {
@ -340,7 +352,11 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
}
public canAddToContainer(room: Room, container: Container): boolean {
return this.getContainerWidgets(room, container).length < MAX_PINNED;
switch (container) {
case Container.Top: return this.getContainerWidgets(room, container).length < MAX_PINNED;
case Container.Right: return this.getContainerWidgets(room, container).length < MAX_PINNED;
case Container.Center: return this.getContainerWidgets(room, container).length < 1;
}
}
public getResizerDistributions(room: Room, container: Container): string[] { // yes, string.
@ -424,11 +440,42 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
public moveToContainer(room: Room, widget: IApp, toContainer: Container) {
const allWidgets = this.getAllWidgets(room);
if (!allWidgets.some(([w]) => w.id === widget.id)) return; // invalid
// Prepare other containers (potentially move widgets to obay the following rules)
switch (toContainer) {
case Container.Right:
// new "right" widget
break;
case Container.Center:
// new "center" widget => all other widgets go into "right"
for (const w of this.getContainerWidgets(room, Container.Top)) {
this.moveToContainer(room, w, Container.Right);
}
for (const w of this.getContainerWidgets(room, Container.Center)) {
this.moveToContainer(room, w, Container.Right);
}
break;
case Container.Top:
// new "top" widget => the center widget moves into "right"
if (this.hasMaximisedWidget(room)) {
this.moveToContainer(room, this.getContainerWidgets(room, Container.Center)[0], Container.Right);
}
break;
}
// move widgets into requested container.
this.updateUserLayout(room, {
[widget.id]: { container: toContainer },
});
}
public hasMaximisedWidget(room: Room) {
return this.getContainerWidgets(room, Container.Center).length > 0;
}
public hasPinnedWidgets(room: Room) {
return this.getContainerWidgets(room, Container.Top).length > 0;
}
public canCopyLayoutToRoom(room: Room): boolean {
if (!this.matrixClient) return false; // not ready yet
return room.currentState.maySendStateEvent(WIDGET_LAYOUT_EVENT_TYPE, this.matrixClient.getUserId());