Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into t3chguy/room-list/13981
Conflicts: src/@types/global.d.ts src/stores/RoomViewStore.tsx
This commit is contained in:
commit
80dff8706c
342 changed files with 9433 additions and 4222 deletions
|
@ -42,7 +42,7 @@ export const UPDATE_EVENT = "update";
|
|||
* help prevent lock conflicts.
|
||||
*/
|
||||
export abstract class AsyncStore<T extends Object> extends EventEmitter {
|
||||
private storeState: T;
|
||||
private storeState: Readonly<T>;
|
||||
private lock = new AwaitLock();
|
||||
private readonly dispatcherRef: string;
|
||||
|
||||
|
@ -62,7 +62,7 @@ export abstract class AsyncStore<T extends Object> extends EventEmitter {
|
|||
* The current state of the store. Cannot be mutated.
|
||||
*/
|
||||
protected get state(): T {
|
||||
return Object.freeze(this.storeState);
|
||||
return this.storeState;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -79,7 +79,7 @@ export abstract class AsyncStore<T extends Object> extends EventEmitter {
|
|||
protected async updateState(newState: T | Object) {
|
||||
await this.lock.acquireAsync();
|
||||
try {
|
||||
this.storeState = Object.assign(<T>{}, this.storeState, newState);
|
||||
this.storeState = Object.freeze(Object.assign(<T>{}, this.storeState, newState));
|
||||
this.emit(UPDATE_EVENT, this);
|
||||
} finally {
|
||||
await this.lock.release();
|
||||
|
@ -94,7 +94,7 @@ export abstract class AsyncStore<T extends Object> extends EventEmitter {
|
|||
protected async reset(newState: T | Object = null, quiet = false) {
|
||||
await this.lock.acquireAsync();
|
||||
try {
|
||||
this.storeState = <T>(newState || {});
|
||||
this.storeState = Object.freeze(<T>(newState || {}));
|
||||
if (!quiet) this.emit(UPDATE_EVENT, this);
|
||||
} finally {
|
||||
await this.lock.release();
|
||||
|
|
|
@ -17,12 +17,25 @@ limitations under the License.
|
|||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { AsyncStore } from "./AsyncStore";
|
||||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
import { Dispatcher } from "flux";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
|
||||
export abstract class AsyncStoreWithClient<T extends Object> extends AsyncStore<T> {
|
||||
protected matrixClient: MatrixClient;
|
||||
|
||||
protected abstract async onAction(payload: ActionPayload);
|
||||
|
||||
protected constructor(dispatcher: Dispatcher<ActionPayload>, initialState: T = <T>{}) {
|
||||
super(dispatcher, initialState);
|
||||
|
||||
if (MatrixClientPeg.get()) {
|
||||
this.matrixClient = MatrixClientPeg.get();
|
||||
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
this.onReady();
|
||||
}
|
||||
}
|
||||
|
||||
protected async onReady() {
|
||||
// Default implementation is to do nothing.
|
||||
}
|
||||
|
@ -35,13 +48,21 @@ export abstract class AsyncStoreWithClient<T extends Object> extends AsyncStore<
|
|||
await this.onAction(payload);
|
||||
|
||||
if (payload.action === 'MatrixActions.sync') {
|
||||
// Filter out anything that isn't the first PREPARED sync.
|
||||
// Only set the client on the transition into the PREPARED state.
|
||||
// Everything after this is unnecessary (we only need to know once we have a client)
|
||||
// and we intentionally don't set the client before this point to avoid stores
|
||||
// updating for every event emitted during the cached sync.
|
||||
if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.matrixClient = payload.matrixClient;
|
||||
await this.onReady();
|
||||
if (this.matrixClient !== payload.matrixClient) {
|
||||
if (this.matrixClient) {
|
||||
await this.onNotReady();
|
||||
}
|
||||
this.matrixClient = payload.matrixClient;
|
||||
await this.onReady();
|
||||
}
|
||||
} else if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') {
|
||||
if (this.matrixClient) {
|
||||
await this.onNotReady();
|
||||
|
|
|
@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import SettingsStore, { SettingLevel } from "../settings/SettingsStore";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import { arrayHasDiff } from "../utils/arrays";
|
||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
|
||||
const MAX_ROOMS = 20; // arbitrary
|
||||
const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up
|
||||
|
@ -55,7 +56,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
private get meetsRoomRequirement(): boolean {
|
||||
return this.matrixClient.getVisibleRooms().length >= 20;
|
||||
return this.matrixClient && this.matrixClient.getVisibleRooms().length >= 20;
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload) {
|
||||
|
|
|
@ -22,6 +22,7 @@ import SettingsStore from "../settings/SettingsStore";
|
|||
import RoomListStore, {LISTS_UPDATE_EVENT} from "./room-list/RoomListStore";
|
||||
import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore";
|
||||
import {isCustomTag} from "./room-list/models";
|
||||
import {objectHasDiff} from "../utils/objects";
|
||||
|
||||
function commonPrefix(a, b) {
|
||||
const len = Math.min(a.length, b.length);
|
||||
|
@ -107,7 +108,10 @@ class CustomRoomTagStore extends EventEmitter {
|
|||
}
|
||||
|
||||
_onListsUpdated = () => {
|
||||
this._setState({tags: this._getUpdatedTags()});
|
||||
const newTags = this._getUpdatedTags();
|
||||
if (!this._state.tags || objectHasDiff(this._state.tags, newTags)) {
|
||||
this._setState({tags: newTags});
|
||||
}
|
||||
};
|
||||
|
||||
_onDispatch(payload) {
|
||||
|
@ -134,7 +138,7 @@ class CustomRoomTagStore extends EventEmitter {
|
|||
|
||||
_getUpdatedTags() {
|
||||
if (!SettingsStore.isFeatureEnabled("feature_custom_tags")) {
|
||||
return;
|
||||
return {}; // none
|
||||
}
|
||||
|
||||
const newTagNames = Object.keys(RoomListStore.instance.orderedLists).filter(t => isCustomTag(t)).sort();
|
||||
|
|
50
src/stores/NonUrgentToastStore.ts
Normal file
50
src/stores/NonUrgentToastStore.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
Copyright 2020 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 { ComponentClass } from "../@types/common";
|
||||
import { UPDATE_EVENT } from "./AsyncStore";
|
||||
|
||||
export type ToastReference = symbol;
|
||||
|
||||
export default class NonUrgentToastStore extends EventEmitter {
|
||||
private static _instance: NonUrgentToastStore;
|
||||
|
||||
private toasts = new Map<ToastReference, ComponentClass>();
|
||||
|
||||
public static get instance(): NonUrgentToastStore {
|
||||
if (!NonUrgentToastStore._instance) {
|
||||
NonUrgentToastStore._instance = new NonUrgentToastStore();
|
||||
}
|
||||
return NonUrgentToastStore._instance;
|
||||
}
|
||||
|
||||
public get components(): ComponentClass[] {
|
||||
return Array.from(this.toasts.values());
|
||||
}
|
||||
|
||||
public addToast(c: ComponentClass): ToastReference {
|
||||
const ref: ToastReference = Symbol();
|
||||
this.toasts.set(ref, c);
|
||||
this.emit(UPDATE_EVENT);
|
||||
return ref;
|
||||
}
|
||||
|
||||
public removeToast(ref: ToastReference) {
|
||||
this.toasts.delete(ref);
|
||||
this.emit(UPDATE_EVENT);
|
||||
}
|
||||
}
|
|
@ -111,8 +111,6 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
|
|||
await this.updateState({displayName: profileInfo.displayname, avatarUrl: profileInfo.avatar_url});
|
||||
};
|
||||
|
||||
// TSLint wants this to be a member, but we don't want that.
|
||||
// tslint:disable-next-line
|
||||
private onStateEvents = throttle(async (ev: MatrixEvent) => {
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) {
|
||||
|
|
|
@ -17,162 +17,179 @@ limitations under the License.
|
|||
import dis from '../dispatcher/dispatcher';
|
||||
import {pendingVerificationRequestForUser} from '../verification';
|
||||
import {Store} from 'flux/utils';
|
||||
import SettingsStore, {SettingLevel} from "../settings/SettingsStore";
|
||||
import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "./RightPanelStorePhases";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import {RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS} from "./RightPanelStorePhases";
|
||||
import {ActionPayload} from "../dispatcher/payloads";
|
||||
import {Action} from '../dispatcher/actions';
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
|
||||
const INITIAL_STATE = {
|
||||
interface RightPanelStoreState {
|
||||
// Whether or not to show the right panel at all. We split out rooms and groups
|
||||
// because they're different flows for the user to follow.
|
||||
showRoomPanel: SettingsStore.getValue("showRightPanelInRoom"),
|
||||
showGroupPanel: SettingsStore.getValue("showRightPanelInGroup"),
|
||||
showRoomPanel: boolean;
|
||||
showGroupPanel: boolean;
|
||||
|
||||
// The last phase (screen) the right panel was showing
|
||||
lastRoomPhase: SettingsStore.getValue("lastRightPanelPhaseForRoom"),
|
||||
lastGroupPhase: SettingsStore.getValue("lastRightPanelPhaseForGroup"),
|
||||
lastRoomPhase: RightPanelPhases;
|
||||
lastGroupPhase: RightPanelPhases;
|
||||
|
||||
// Extra information about the last phase
|
||||
lastRoomPhaseParams: {[key: string]: any};
|
||||
}
|
||||
|
||||
const INITIAL_STATE: RightPanelStoreState = {
|
||||
showRoomPanel: SettingsStore.getValue("showRightPanelInRoom"),
|
||||
showGroupPanel: SettingsStore.getValue("showRightPanelInGroup"),
|
||||
lastRoomPhase: SettingsStore.getValue("lastRightPanelPhaseForRoom"),
|
||||
lastGroupPhase: SettingsStore.getValue("lastRightPanelPhaseForGroup"),
|
||||
lastRoomPhaseParams: {},
|
||||
};
|
||||
|
||||
const GROUP_PHASES = Object.keys(RIGHT_PANEL_PHASES).filter(k => k.startsWith("Group"));
|
||||
const GROUP_PHASES = [
|
||||
RightPanelPhases.GroupMemberList,
|
||||
RightPanelPhases.GroupRoomList,
|
||||
RightPanelPhases.GroupRoomInfo,
|
||||
RightPanelPhases.GroupMemberInfo,
|
||||
];
|
||||
|
||||
const MEMBER_INFO_PHASES = [
|
||||
RIGHT_PANEL_PHASES.RoomMemberInfo,
|
||||
RIGHT_PANEL_PHASES.Room3pidMemberInfo,
|
||||
RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||
RightPanelPhases.RoomMemberInfo,
|
||||
RightPanelPhases.Room3pidMemberInfo,
|
||||
RightPanelPhases.EncryptionPanel,
|
||||
];
|
||||
|
||||
/**
|
||||
* A class for tracking the state of the right panel between layouts and
|
||||
* sessions.
|
||||
*/
|
||||
export default class RightPanelStore extends Store {
|
||||
static _instance;
|
||||
export default class RightPanelStore extends Store<ActionPayload> {
|
||||
private static instance: RightPanelStore;
|
||||
private state: RightPanelStoreState;
|
||||
|
||||
constructor() {
|
||||
super(dis);
|
||||
|
||||
// Initialise state
|
||||
this._state = INITIAL_STATE;
|
||||
this.state = INITIAL_STATE;
|
||||
}
|
||||
|
||||
get isOpenForRoom(): boolean {
|
||||
return this._state.showRoomPanel;
|
||||
return this.state.showRoomPanel;
|
||||
}
|
||||
|
||||
get isOpenForGroup(): boolean {
|
||||
return this._state.showGroupPanel;
|
||||
return this.state.showGroupPanel;
|
||||
}
|
||||
|
||||
get roomPanelPhase(): string {
|
||||
return this._state.lastRoomPhase;
|
||||
get roomPanelPhase(): RightPanelPhases {
|
||||
return this.state.lastRoomPhase;
|
||||
}
|
||||
|
||||
get groupPanelPhase(): string {
|
||||
return this._state.lastGroupPhase;
|
||||
get groupPanelPhase(): RightPanelPhases {
|
||||
return this.state.lastGroupPhase;
|
||||
}
|
||||
|
||||
get visibleRoomPanelPhase(): string {
|
||||
get visibleRoomPanelPhase(): RightPanelPhases {
|
||||
return this.isOpenForRoom ? this.roomPanelPhase : null;
|
||||
}
|
||||
|
||||
get visibleGroupPanelPhase(): string {
|
||||
get visibleGroupPanelPhase(): RightPanelPhases {
|
||||
return this.isOpenForGroup ? this.groupPanelPhase : null;
|
||||
}
|
||||
|
||||
get roomPanelPhaseParams(): any {
|
||||
return this._state.lastRoomPhaseParams || {};
|
||||
return this.state.lastRoomPhaseParams || {};
|
||||
}
|
||||
|
||||
_setState(newState) {
|
||||
this._state = Object.assign(this._state, newState);
|
||||
private setState(newState: Partial<RightPanelStoreState>) {
|
||||
this.state = Object.assign(this.state, newState);
|
||||
|
||||
SettingsStore.setValue(
|
||||
"showRightPanelInRoom",
|
||||
null,
|
||||
SettingLevel.DEVICE,
|
||||
this._state.showRoomPanel,
|
||||
this.state.showRoomPanel,
|
||||
);
|
||||
SettingsStore.setValue(
|
||||
"showRightPanelInGroup",
|
||||
null,
|
||||
SettingLevel.DEVICE,
|
||||
this._state.showGroupPanel,
|
||||
this.state.showGroupPanel,
|
||||
);
|
||||
|
||||
if (RIGHT_PANEL_PHASES_NO_ARGS.includes(this._state.lastRoomPhase)) {
|
||||
if (RIGHT_PANEL_PHASES_NO_ARGS.includes(this.state.lastRoomPhase)) {
|
||||
SettingsStore.setValue(
|
||||
"lastRightPanelPhaseForRoom",
|
||||
null,
|
||||
SettingLevel.DEVICE,
|
||||
this._state.lastRoomPhase,
|
||||
this.state.lastRoomPhase,
|
||||
);
|
||||
}
|
||||
if (RIGHT_PANEL_PHASES_NO_ARGS.includes(this._state.lastGroupPhase)) {
|
||||
if (RIGHT_PANEL_PHASES_NO_ARGS.includes(this.state.lastGroupPhase)) {
|
||||
SettingsStore.setValue(
|
||||
"lastRightPanelPhaseForGroup",
|
||||
null,
|
||||
SettingLevel.DEVICE,
|
||||
this._state.lastGroupPhase,
|
||||
this.state.lastGroupPhase,
|
||||
);
|
||||
}
|
||||
|
||||
this.__emitChange();
|
||||
}
|
||||
|
||||
__onDispatch(payload) {
|
||||
__onDispatch(payload: ActionPayload) {
|
||||
switch (payload.action) {
|
||||
case 'view_room':
|
||||
case 'view_group':
|
||||
// Reset to the member list if we're viewing member info
|
||||
if (MEMBER_INFO_PHASES.includes(this._state.lastRoomPhase)) {
|
||||
this._setState({lastRoomPhase: RIGHT_PANEL_PHASES.RoomMemberList, lastRoomPhaseParams: {}});
|
||||
if (MEMBER_INFO_PHASES.includes(this.state.lastRoomPhase)) {
|
||||
this.setState({lastRoomPhase: RightPanelPhases.RoomMemberList, lastRoomPhaseParams: {}});
|
||||
}
|
||||
|
||||
// Do the same for groups
|
||||
if (this._state.lastGroupPhase === RIGHT_PANEL_PHASES.GroupMemberInfo) {
|
||||
this._setState({lastGroupPhase: RIGHT_PANEL_PHASES.GroupMemberList});
|
||||
if (this.state.lastGroupPhase === RightPanelPhases.GroupMemberInfo) {
|
||||
this.setState({lastGroupPhase: RightPanelPhases.GroupMemberList});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'set_right_panel_phase': {
|
||||
case Action.SetRightPanelPhase: {
|
||||
let targetPhase = payload.phase;
|
||||
let refireParams = payload.refireParams;
|
||||
// redirect to EncryptionPanel if there is an ongoing verification request
|
||||
if (targetPhase === RIGHT_PANEL_PHASES.RoomMemberInfo && payload.refireParams) {
|
||||
if (targetPhase === RightPanelPhases.RoomMemberInfo && payload.refireParams) {
|
||||
const {member} = payload.refireParams;
|
||||
const pendingRequest = pendingVerificationRequestForUser(member);
|
||||
if (pendingRequest) {
|
||||
targetPhase = RIGHT_PANEL_PHASES.EncryptionPanel;
|
||||
targetPhase = RightPanelPhases.EncryptionPanel;
|
||||
refireParams = {
|
||||
verificationRequest: pendingRequest,
|
||||
member,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!RIGHT_PANEL_PHASES[targetPhase]) {
|
||||
if (!RightPanelPhases[targetPhase]) {
|
||||
console.warn(`Tried to switch right panel to unknown phase: ${targetPhase}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (GROUP_PHASES.includes(targetPhase)) {
|
||||
if (targetPhase === this._state.lastGroupPhase) {
|
||||
this._setState({
|
||||
showGroupPanel: !this._state.showGroupPanel,
|
||||
if (targetPhase === this.state.lastGroupPhase) {
|
||||
this.setState({
|
||||
showGroupPanel: !this.state.showGroupPanel,
|
||||
});
|
||||
} else {
|
||||
this._setState({
|
||||
this.setState({
|
||||
lastGroupPhase: targetPhase,
|
||||
showGroupPanel: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (targetPhase === this._state.lastRoomPhase && !refireParams) {
|
||||
this._setState({
|
||||
showRoomPanel: !this._state.showRoomPanel,
|
||||
if (targetPhase === this.state.lastRoomPhase && !refireParams) {
|
||||
this.setState({
|
||||
showRoomPanel: !this.state.showRoomPanel,
|
||||
});
|
||||
} else {
|
||||
this._setState({
|
||||
this.setState({
|
||||
lastRoomPhase: targetPhase,
|
||||
showRoomPanel: true,
|
||||
lastRoomPhaseParams: refireParams || {},
|
||||
|
@ -182,27 +199,27 @@ export default class RightPanelStore extends Store {
|
|||
|
||||
// Let things like the member info panel actually open to the right member.
|
||||
dis.dispatch({
|
||||
action: 'after_right_panel_phase_change',
|
||||
action: Action.AfterRightPanelPhaseChange,
|
||||
phase: targetPhase,
|
||||
...(refireParams || {}),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'toggle_right_panel':
|
||||
case Action.ToggleRightPanel:
|
||||
if (payload.type === "room") {
|
||||
this._setState({ showRoomPanel: !this._state.showRoomPanel });
|
||||
this.setState({ showRoomPanel: !this.state.showRoomPanel });
|
||||
} else { // group
|
||||
this._setState({ showGroupPanel: !this._state.showGroupPanel });
|
||||
this.setState({ showGroupPanel: !this.state.showGroupPanel });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static getSharedInstance(): RightPanelStore {
|
||||
if (!RightPanelStore._instance) {
|
||||
RightPanelStore._instance = new RightPanelStore();
|
||||
if (!RightPanelStore.instance) {
|
||||
RightPanelStore.instance = new RightPanelStore();
|
||||
}
|
||||
return RightPanelStore._instance;
|
||||
return RightPanelStore.instance;
|
||||
}
|
||||
}
|
|
@ -15,28 +15,28 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
// These are in their own file because of circular imports being a problem.
|
||||
export const RIGHT_PANEL_PHASES = Object.freeze({
|
||||
export enum RightPanelPhases {
|
||||
// Room stuff
|
||||
RoomMemberList: 'RoomMemberList',
|
||||
FilePanel: 'FilePanel',
|
||||
NotificationPanel: 'NotificationPanel',
|
||||
RoomMemberInfo: 'RoomMemberInfo',
|
||||
EncryptionPanel: 'EncryptionPanel',
|
||||
RoomMemberList = 'RoomMemberList',
|
||||
FilePanel = 'FilePanel',
|
||||
NotificationPanel = 'NotificationPanel',
|
||||
RoomMemberInfo = 'RoomMemberInfo',
|
||||
EncryptionPanel = 'EncryptionPanel',
|
||||
|
||||
Room3pidMemberInfo: 'Room3pidMemberInfo',
|
||||
Room3pidMemberInfo = 'Room3pidMemberInfo',
|
||||
// Group stuff
|
||||
GroupMemberList: 'GroupMemberList',
|
||||
GroupRoomList: 'GroupRoomList',
|
||||
GroupRoomInfo: 'GroupRoomInfo',
|
||||
GroupMemberInfo: 'GroupMemberInfo',
|
||||
});
|
||||
GroupMemberList = 'GroupMemberList',
|
||||
GroupRoomList = 'GroupRoomList',
|
||||
GroupRoomInfo = 'GroupRoomInfo',
|
||||
GroupMemberInfo = 'GroupMemberInfo',
|
||||
}
|
||||
|
||||
// These are the phases that are safe to persist (the ones that don't require additional
|
||||
// arguments).
|
||||
export const RIGHT_PANEL_PHASES_NO_ARGS = [
|
||||
RIGHT_PANEL_PHASES.NotificationPanel,
|
||||
RIGHT_PANEL_PHASES.FilePanel,
|
||||
RIGHT_PANEL_PHASES.RoomMemberList,
|
||||
RIGHT_PANEL_PHASES.GroupMemberList,
|
||||
RIGHT_PANEL_PHASES.GroupRoomList,
|
||||
RightPanelPhases.NotificationPanel,
|
||||
RightPanelPhases.FilePanel,
|
||||
RightPanelPhases.RoomMemberList,
|
||||
RightPanelPhases.GroupMemberList,
|
||||
RightPanelPhases.GroupRoomList,
|
||||
];
|
|
@ -279,12 +279,22 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
console.log("Failed to join room:", msg);
|
||||
if (err.name === "ConnectionError") {
|
||||
msg = _t("There was an error joining the room");
|
||||
}
|
||||
if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') {
|
||||
} 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, {
|
||||
|
@ -294,6 +304,16 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
});
|
||||
}
|
||||
|
||||
private getInvitingUserId(roomId: string): string {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(roomId);
|
||||
if (room && room.getMyMembership() === "invite") {
|
||||
const myMember = room.getMember(cli.getUserId());
|
||||
const inviteEvent = myMember ? myMember.events.member : null;
|
||||
return inviteEvent && inviteEvent.getSender();
|
||||
}
|
||||
}
|
||||
|
||||
private joinRoomError(payload: ActionPayload) {
|
||||
this.setState({
|
||||
joining: false,
|
||||
|
|
|
@ -15,9 +15,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import EventEmitter from "events";
|
||||
import React, {JSXElementConstructor} from "react";
|
||||
import React from "react";
|
||||
import { ComponentClass } from "../@types/common";
|
||||
|
||||
export interface IToast<C extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> {
|
||||
export interface IToast<C extends ComponentClass> {
|
||||
key: string;
|
||||
// higher priority number will be shown on top of lower priority
|
||||
priority: number;
|
||||
|
@ -55,7 +56,7 @@ export default class ToastStore extends EventEmitter {
|
|||
*
|
||||
* @param {object} newToast The new toast
|
||||
*/
|
||||
addOrReplaceToast<C extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>>(newToast: IToast<C>) {
|
||||
addOrReplaceToast<C extends ComponentClass>(newToast: IToast<C>) {
|
||||
const oldIndex = this.toasts.findIndex(t => t.key === newToast.key);
|
||||
if (oldIndex === -1) {
|
||||
let newIndex = this.toasts.length;
|
||||
|
|
31
src/stores/local-echo/EchoChamber.ts
Normal file
31
src/stores/local-echo/EchoChamber.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
Copyright 2020 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 { RoomEchoChamber } from "./RoomEchoChamber";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { EchoStore } from "./EchoStore";
|
||||
|
||||
/**
|
||||
* Semantic access to local echo
|
||||
*/
|
||||
export class EchoChamber {
|
||||
private constructor() {
|
||||
}
|
||||
|
||||
public static forRoom(room: Room): RoomEchoChamber {
|
||||
return EchoStore.instance.getOrCreateChamberForRoom(room);
|
||||
}
|
||||
}
|
87
src/stores/local-echo/EchoContext.ts
Normal file
87
src/stores/local-echo/EchoContext.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
Copyright 2020 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 { EchoTransaction, RunFn, TransactionStatus } from "./EchoTransaction";
|
||||
import { arrayFastClone } from "../../utils/arrays";
|
||||
import { IDestroyable } from "../../utils/IDestroyable";
|
||||
import { Whenable } from "../../utils/Whenable";
|
||||
|
||||
export enum ContextTransactionState {
|
||||
NotStarted,
|
||||
PendingErrors,
|
||||
AllSuccessful
|
||||
}
|
||||
|
||||
export abstract class EchoContext extends Whenable<ContextTransactionState> implements IDestroyable {
|
||||
private _transactions: EchoTransaction[] = [];
|
||||
private _state = ContextTransactionState.NotStarted;
|
||||
|
||||
public get transactions(): EchoTransaction[] {
|
||||
return arrayFastClone(this._transactions);
|
||||
}
|
||||
|
||||
public get state(): ContextTransactionState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public get firstFailedTime(): Date {
|
||||
const failedTxn = this.transactions.find(t => t.didPreviouslyFail || t.status === TransactionStatus.Error);
|
||||
if (failedTxn) return failedTxn.startTime;
|
||||
return null;
|
||||
}
|
||||
|
||||
public disownTransaction(txn: EchoTransaction) {
|
||||
const idx = this._transactions.indexOf(txn);
|
||||
if (idx >= 0) this._transactions.splice(idx, 1);
|
||||
txn.destroy();
|
||||
this.checkTransactions();
|
||||
}
|
||||
|
||||
public beginTransaction(auditName: string, runFn: RunFn): EchoTransaction {
|
||||
const txn = new EchoTransaction(auditName, runFn);
|
||||
this._transactions.push(txn);
|
||||
txn.whenAnything(this.checkTransactions);
|
||||
|
||||
// We have no intent to call the transaction again if it succeeds (in fact, it'll
|
||||
// be really angry at us if we do), so call that the end of the road for the events.
|
||||
txn.when(TransactionStatus.Success, () => txn.destroy());
|
||||
|
||||
return txn;
|
||||
}
|
||||
|
||||
private checkTransactions = () => {
|
||||
let status = ContextTransactionState.AllSuccessful;
|
||||
for (const txn of this.transactions) {
|
||||
if (txn.status === TransactionStatus.Error || txn.didPreviouslyFail) {
|
||||
status = ContextTransactionState.PendingErrors;
|
||||
break;
|
||||
} else if (txn.status === TransactionStatus.Pending) {
|
||||
status = ContextTransactionState.NotStarted;
|
||||
// no break as we might hit something which broke
|
||||
}
|
||||
}
|
||||
this._state = status;
|
||||
this.notifyCondition(status);
|
||||
};
|
||||
|
||||
public destroy() {
|
||||
for (const txn of this.transactions) {
|
||||
txn.destroy();
|
||||
}
|
||||
this._transactions = [];
|
||||
super.destroy();
|
||||
}
|
||||
}
|
104
src/stores/local-echo/EchoStore.ts
Normal file
104
src/stores/local-echo/EchoStore.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
Copyright 2020 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 { GenericEchoChamber } from "./GenericEchoChamber";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomEchoChamber } from "./RoomEchoChamber";
|
||||
import { RoomEchoContext } from "./RoomEchoContext";
|
||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import { ContextTransactionState, EchoContext } from "./EchoContext";
|
||||
import NonUrgentToastStore, { ToastReference } from "../NonUrgentToastStore";
|
||||
import NonUrgentEchoFailureToast from "../../components/views/toasts/NonUrgentEchoFailureToast";
|
||||
|
||||
interface IState {
|
||||
toastRef: ToastReference;
|
||||
}
|
||||
|
||||
type ContextKey = string;
|
||||
|
||||
const roomContextKey = (room: Room): ContextKey => `room-${room.roomId}`;
|
||||
|
||||
export class EchoStore extends AsyncStoreWithClient<IState> {
|
||||
private static _instance: EchoStore;
|
||||
|
||||
private caches = new Map<ContextKey, GenericEchoChamber<any, any, any>>();
|
||||
|
||||
constructor() {
|
||||
super(defaultDispatcher);
|
||||
}
|
||||
|
||||
public static get instance(): EchoStore {
|
||||
if (!EchoStore._instance) {
|
||||
EchoStore._instance = new EchoStore();
|
||||
}
|
||||
return EchoStore._instance;
|
||||
}
|
||||
|
||||
public get contexts(): EchoContext[] {
|
||||
return Array.from(this.caches.values()).map(e => e.context);
|
||||
}
|
||||
|
||||
public getOrCreateChamberForRoom(room: Room): RoomEchoChamber {
|
||||
if (this.caches.has(roomContextKey(room))) {
|
||||
return this.caches.get(roomContextKey(room)) as RoomEchoChamber;
|
||||
}
|
||||
|
||||
const context = new RoomEchoContext(room);
|
||||
context.whenAnything(() => this.checkContexts());
|
||||
|
||||
const echo = new RoomEchoChamber(context);
|
||||
echo.setClient(this.matrixClient);
|
||||
this.caches.set(roomContextKey(room), echo);
|
||||
|
||||
return echo;
|
||||
}
|
||||
|
||||
private async checkContexts() {
|
||||
let hasOrHadError = false;
|
||||
for (const echo of this.caches.values()) {
|
||||
hasOrHadError = echo.context.state === ContextTransactionState.PendingErrors;
|
||||
if (hasOrHadError) break;
|
||||
}
|
||||
|
||||
if (hasOrHadError && !this.state.toastRef) {
|
||||
const ref = NonUrgentToastStore.instance.addToast(NonUrgentEchoFailureToast);
|
||||
await this.updateState({toastRef: ref});
|
||||
} else if (!hasOrHadError && this.state.toastRef) {
|
||||
NonUrgentToastStore.instance.removeToast(this.state.toastRef);
|
||||
await this.updateState({toastRef: null});
|
||||
}
|
||||
}
|
||||
|
||||
protected async onReady(): Promise<any> {
|
||||
if (!this.caches) return; // can only happen during initialization
|
||||
for (const echo of this.caches.values()) {
|
||||
echo.setClient(this.matrixClient);
|
||||
}
|
||||
}
|
||||
|
||||
protected async onNotReady(): Promise<any> {
|
||||
for (const echo of this.caches.values()) {
|
||||
echo.setClient(null);
|
||||
}
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload): Promise<any> {
|
||||
// We have nothing to actually listen for
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
72
src/stores/local-echo/EchoTransaction.ts
Normal file
72
src/stores/local-echo/EchoTransaction.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
Copyright 2020 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 { Whenable } from "../../utils/Whenable";
|
||||
|
||||
export type RunFn = () => Promise<void>;
|
||||
|
||||
export enum TransactionStatus {
|
||||
Pending,
|
||||
Success,
|
||||
Error,
|
||||
}
|
||||
|
||||
export class EchoTransaction extends Whenable<TransactionStatus> {
|
||||
private _status = TransactionStatus.Pending;
|
||||
private didFail = false;
|
||||
|
||||
public readonly startTime = new Date();
|
||||
|
||||
public constructor(
|
||||
public readonly auditName,
|
||||
public runFn: RunFn,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get didPreviouslyFail(): boolean {
|
||||
return this.didFail;
|
||||
}
|
||||
|
||||
public get status(): TransactionStatus {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
public run() {
|
||||
if (this.status === TransactionStatus.Success) {
|
||||
throw new Error("Cannot re-run a successful echo transaction");
|
||||
}
|
||||
this.setStatus(TransactionStatus.Pending);
|
||||
this.runFn()
|
||||
.then(() => this.setStatus(TransactionStatus.Success))
|
||||
.catch(() => this.setStatus(TransactionStatus.Error));
|
||||
}
|
||||
|
||||
public cancel() {
|
||||
// Success basically means "done"
|
||||
this.setStatus(TransactionStatus.Success);
|
||||
}
|
||||
|
||||
private setStatus(status: TransactionStatus) {
|
||||
this._status = status;
|
||||
if (status === TransactionStatus.Error) {
|
||||
this.didFail = true;
|
||||
} else if (status === TransactionStatus.Success) {
|
||||
this.didFail = false;
|
||||
}
|
||||
this.notifyCondition(status);
|
||||
}
|
||||
}
|
91
src/stores/local-echo/GenericEchoChamber.ts
Normal file
91
src/stores/local-echo/GenericEchoChamber.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
Copyright 2020 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 { EchoContext } from "./EchoContext";
|
||||
import { EchoTransaction, RunFn, TransactionStatus } from "./EchoTransaction";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
export async function implicitlyReverted() {
|
||||
// do nothing :D
|
||||
}
|
||||
|
||||
export const PROPERTY_UPDATED = "property_updated";
|
||||
|
||||
export abstract class GenericEchoChamber<C extends EchoContext, K, V> extends EventEmitter {
|
||||
private cache = new Map<K, {txn: EchoTransaction, val: V}>();
|
||||
protected matrixClient: MatrixClient;
|
||||
|
||||
protected constructor(public readonly context: C, private lookupFn: (key: K) => V) {
|
||||
super();
|
||||
}
|
||||
|
||||
public setClient(client: MatrixClient) {
|
||||
const oldClient = this.matrixClient;
|
||||
this.matrixClient = client;
|
||||
this.onClientChanged(oldClient, client);
|
||||
}
|
||||
|
||||
protected abstract onClientChanged(oldClient: MatrixClient, newClient: MatrixClient);
|
||||
|
||||
/**
|
||||
* Gets a value. If the key is in flight, the cached value will be returned. If
|
||||
* the key is not in flight then the lookupFn provided to this class will be
|
||||
* called instead.
|
||||
* @param key The key to look up.
|
||||
* @returns The value for the key.
|
||||
*/
|
||||
public getValue(key: K): V {
|
||||
return this.cache.has(key) ? this.cache.get(key).val : this.lookupFn(key);
|
||||
}
|
||||
|
||||
private cacheVal(key: K, val: V, txn: EchoTransaction) {
|
||||
this.cache.set(key, {txn, val});
|
||||
this.emit(PROPERTY_UPDATED, key);
|
||||
}
|
||||
|
||||
private decacheKey(key: K) {
|
||||
if (this.cache.has(key)) {
|
||||
this.context.disownTransaction(this.cache.get(key).txn);
|
||||
this.cache.delete(key);
|
||||
this.emit(PROPERTY_UPDATED, key);
|
||||
}
|
||||
}
|
||||
|
||||
protected markEchoReceived(key: K) {
|
||||
if (this.cache.has(key)) {
|
||||
const txn = this.cache.get(key).txn;
|
||||
this.context.disownTransaction(txn);
|
||||
txn.cancel();
|
||||
}
|
||||
this.decacheKey(key);
|
||||
}
|
||||
|
||||
public setValue(auditName: string, key: K, targetVal: V, runFn: RunFn, revertFn: RunFn) {
|
||||
// Cancel any pending transactions for the same key
|
||||
if (this.cache.has(key)) {
|
||||
this.cache.get(key).txn.cancel();
|
||||
}
|
||||
|
||||
const ctxn = this.context.beginTransaction(auditName, runFn);
|
||||
this.cacheVal(key, targetVal, ctxn); // set the cache now as it won't be updated by the .when() ladder below.
|
||||
|
||||
ctxn.when(TransactionStatus.Pending, () => this.cacheVal(key, targetVal, ctxn))
|
||||
.when(TransactionStatus.Error, () => revertFn());
|
||||
|
||||
ctxn.run();
|
||||
}
|
||||
}
|
78
src/stores/local-echo/RoomEchoChamber.ts
Normal file
78
src/stores/local-echo/RoomEchoChamber.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
Copyright 2020 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 { GenericEchoChamber, implicitlyReverted, PROPERTY_UPDATED } from "./GenericEchoChamber";
|
||||
import { getRoomNotifsState, setRoomNotifsState } from "../../RoomNotifs";
|
||||
import { RoomEchoContext } from "./RoomEchoContext";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { Volume } from "../../RoomNotifsTypes";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
export type CachedRoomValues = Volume;
|
||||
|
||||
export enum CachedRoomKey {
|
||||
NotificationVolume,
|
||||
}
|
||||
|
||||
export class RoomEchoChamber extends GenericEchoChamber<RoomEchoContext, CachedRoomKey, CachedRoomValues> {
|
||||
private properties = new Map<CachedRoomKey, CachedRoomValues>();
|
||||
|
||||
public constructor(context: RoomEchoContext) {
|
||||
super(context, (k) => this.properties.get(k));
|
||||
}
|
||||
|
||||
protected onClientChanged(oldClient, newClient) {
|
||||
this.properties.clear();
|
||||
if (oldClient) {
|
||||
oldClient.removeListener("accountData", this.onAccountData);
|
||||
}
|
||||
if (newClient) {
|
||||
// Register the listeners first
|
||||
newClient.on("accountData", this.onAccountData);
|
||||
|
||||
// Then populate the properties map
|
||||
this.updateNotificationVolume();
|
||||
}
|
||||
}
|
||||
|
||||
private onAccountData = (event: MatrixEvent) => {
|
||||
if (event.getType() === "m.push_rules") {
|
||||
const currentVolume = this.properties.get(CachedRoomKey.NotificationVolume) as Volume;
|
||||
const newVolume = getRoomNotifsState(this.context.room.roomId) as Volume;
|
||||
if (currentVolume !== newVolume) {
|
||||
this.updateNotificationVolume();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private updateNotificationVolume() {
|
||||
this.properties.set(CachedRoomKey.NotificationVolume, getRoomNotifsState(this.context.room.roomId));
|
||||
this.markEchoReceived(CachedRoomKey.NotificationVolume);
|
||||
this.emit(PROPERTY_UPDATED, CachedRoomKey.NotificationVolume);
|
||||
}
|
||||
|
||||
// ---- helpers below here ----
|
||||
|
||||
public get notificationVolume(): Volume {
|
||||
return this.getValue(CachedRoomKey.NotificationVolume);
|
||||
}
|
||||
|
||||
public set notificationVolume(v: Volume) {
|
||||
this.setValue(_t("Change notification settings"), CachedRoomKey.NotificationVolume, v, async () => {
|
||||
return setRoomNotifsState(this.context.room.roomId, v);
|
||||
}, implicitlyReverted);
|
||||
}
|
||||
}
|
24
src/stores/local-echo/RoomEchoContext.ts
Normal file
24
src/stores/local-echo/RoomEchoContext.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
Copyright 2020 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 { EchoContext } from "./EchoContext";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
export class RoomEchoContext extends EchoContext {
|
||||
constructor(public readonly room: Room) {
|
||||
super();
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
|||
this.room.on("Room.receipt", this.handleReadReceipt);
|
||||
this.room.on("Room.timeline", this.handleRoomEventUpdate);
|
||||
this.room.on("Room.redaction", this.handleRoomEventUpdate);
|
||||
this.room.on("Room.myMembership", this.handleMembershipUpdate);
|
||||
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
|
||||
MatrixClientPeg.get().on("accountData", this.handleAccountDataUpdate);
|
||||
this.updateNotificationState();
|
||||
|
@ -45,6 +46,7 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
|||
this.room.removeListener("Room.receipt", this.handleReadReceipt);
|
||||
this.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
|
||||
this.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
|
||||
this.room.removeListener("Room.myMembership", this.handleMembershipUpdate);
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate);
|
||||
MatrixClientPeg.get().removeListener("accountData", this.handleAccountDataUpdate);
|
||||
|
@ -57,6 +59,10 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
|||
this.updateNotificationState();
|
||||
};
|
||||
|
||||
private handleMembershipUpdate = () => {
|
||||
this.updateNotificationState();
|
||||
};
|
||||
|
||||
private handleRoomEventUpdate = (event: MatrixEvent) => {
|
||||
const roomId = event.getRoomId();
|
||||
|
||||
|
|
|
@ -21,37 +21,55 @@ import { DefaultTagID, TagID } from "../room-list/models";
|
|||
import { FetchRoomFn, ListNotificationState } from "./ListNotificationState";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomNotificationState } from "./RoomNotificationState";
|
||||
|
||||
const INSPECIFIC_TAG = "INSPECIFIC_TAG";
|
||||
type INSPECIFIC_TAG = "INSPECIFIC_TAG";
|
||||
import { SummarizedNotificationState } from "./SummarizedNotificationState";
|
||||
|
||||
interface IState {}
|
||||
|
||||
export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new RoomNotificationStateStore();
|
||||
|
||||
private roomMap = new Map<Room, Map<TagID | INSPECIFIC_TAG, RoomNotificationState>>();
|
||||
private roomMap = new Map<Room, RoomNotificationState>();
|
||||
private listMap = new Map<TagID, ListNotificationState>();
|
||||
|
||||
private constructor() {
|
||||
super(defaultDispatcher, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new list notification state. The consumer is expected to set the rooms
|
||||
* on the notification state, and destroy the state when it no longer needs it.
|
||||
* @param tagId The tag to create the notification state for.
|
||||
* Gets a snapshot of notification state for all visible rooms. The number of states recorded
|
||||
* on the SummarizedNotificationState is equivalent to rooms.
|
||||
*/
|
||||
public get globalState(): SummarizedNotificationState {
|
||||
// If we're not ready yet, just return an empty state
|
||||
if (!this.matrixClient) return new SummarizedNotificationState();
|
||||
|
||||
// Only count visible rooms to not torment the user with notification counts in rooms they can't see.
|
||||
// This will include highlights from the previous version of the room internally
|
||||
const globalState = new SummarizedNotificationState();
|
||||
for (const room of this.matrixClient.getVisibleRooms()) {
|
||||
globalState.add(this.getRoomState(room));
|
||||
}
|
||||
return globalState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of the list state class for the given tag.
|
||||
* @param tagId The tag to get the notification state for.
|
||||
* @returns The notification state for the tag.
|
||||
*/
|
||||
public getListState(tagId: TagID): ListNotificationState {
|
||||
// Note: we don't cache these notification states as the consumer is expected to call
|
||||
// .setRooms() on the returned object, which could confuse other consumers.
|
||||
if (this.listMap.has(tagId)) {
|
||||
return this.listMap.get(tagId);
|
||||
}
|
||||
|
||||
// TODO: Update if/when invites move out of the room list.
|
||||
const useTileCount = tagId === DefaultTagID.Invite;
|
||||
const getRoomFn: FetchRoomFn = (room: Room) => {
|
||||
return this.getRoomState(room, tagId);
|
||||
return this.getRoomState(room);
|
||||
};
|
||||
return new ListNotificationState(useTileCount, tagId, getRoomFn);
|
||||
const state = new ListNotificationState(useTileCount, tagId, getRoomFn);
|
||||
this.listMap.set(tagId, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,22 +77,13 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
|||
* attempt to destroy the returned state as it may be shared with other
|
||||
* consumers.
|
||||
* @param room The room to get the notification state for.
|
||||
* @param inTagId Optional tag ID to scope the notification state to.
|
||||
* @returns The room's notification state.
|
||||
*/
|
||||
public getRoomState(room: Room, inTagId?: TagID): RoomNotificationState {
|
||||
public getRoomState(room: Room): RoomNotificationState {
|
||||
if (!this.roomMap.has(room)) {
|
||||
this.roomMap.set(room, new Map<TagID | INSPECIFIC_TAG, RoomNotificationState>());
|
||||
this.roomMap.set(room, new RoomNotificationState(room));
|
||||
}
|
||||
|
||||
const targetTag = inTagId ? inTagId : INSPECIFIC_TAG;
|
||||
|
||||
const forRoomMap = this.roomMap.get(room);
|
||||
if (!forRoomMap.has(targetTag)) {
|
||||
forRoomMap.set(inTagId ? inTagId : INSPECIFIC_TAG, new RoomNotificationState(room));
|
||||
}
|
||||
|
||||
return forRoomMap.get(targetTag);
|
||||
return this.roomMap.get(room);
|
||||
}
|
||||
|
||||
public static get instance(): RoomNotificationStateStore {
|
||||
|
@ -82,10 +91,8 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
protected async onNotReady(): Promise<any> {
|
||||
for (const roomMap of this.roomMap.values()) {
|
||||
for (const roomState of roomMap.values()) {
|
||||
roomState.destroy();
|
||||
}
|
||||
for (const roomState of this.roomMap.values()) {
|
||||
roomState.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
62
src/stores/notifications/SummarizedNotificationState.ts
Normal file
62
src/stores/notifications/SummarizedNotificationState.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
Copyright 2020 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 { NotificationState } from "./NotificationState";
|
||||
|
||||
/**
|
||||
* Summarizes a number of states into a unique snapshot. To populate, call
|
||||
* the add() function with the notification states to be included.
|
||||
*
|
||||
* Useful for community notification counts, global notification counts, etc.
|
||||
*/
|
||||
export class SummarizedNotificationState extends NotificationState {
|
||||
private totalStatesWithUnread = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._symbol = null;
|
||||
this._count = 0;
|
||||
this._color = NotificationColor.None;
|
||||
}
|
||||
|
||||
public get numUnreadStates(): number {
|
||||
return this.totalStatesWithUnread;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a notification state to this snapshot, taking the loudest NotificationColor
|
||||
* of the two. By default this will not adopt the symbol of the other notification
|
||||
* state to prevent the count from being lost in typical usage.
|
||||
* @param other The other notification state to append.
|
||||
* @param includeSymbol If true, the notification state's symbol will be taken if one
|
||||
* is present.
|
||||
*/
|
||||
public add(other: NotificationState, includeSymbol = false) {
|
||||
if (other.symbol && includeSymbol) {
|
||||
this._symbol = other.symbol;
|
||||
}
|
||||
if (other.count) {
|
||||
this._count += other.count;
|
||||
}
|
||||
if (other.color > this.color) {
|
||||
this._color = other.color;
|
||||
}
|
||||
if (other.hasUnreadCount) {
|
||||
this.totalStatesWithUnread++;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,42 +19,24 @@ import { ActionPayload } from "../../dispatcher/payloads";
|
|||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { MessageEventPreview } from "./previews/MessageEventPreview";
|
||||
import { NameEventPreview } from "./previews/NameEventPreview";
|
||||
import { TagID } from "./models";
|
||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
import { TopicEventPreview } from "./previews/TopicEventPreview";
|
||||
import { MembershipEventPreview } from "./previews/MembershipEventPreview";
|
||||
import { HistoryVisibilityEventPreview } from "./previews/HistoryVisibilityEventPreview";
|
||||
import { CallInviteEventPreview } from "./previews/CallInviteEventPreview";
|
||||
import { CallAnswerEventPreview } from "./previews/CallAnswerEventPreview";
|
||||
import { CallHangupEvent } from "./previews/CallHangupEvent";
|
||||
import { EncryptionEventPreview } from "./previews/EncryptionEventPreview";
|
||||
import { ThirdPartyInviteEventPreview } from "./previews/ThirdPartyInviteEventPreview";
|
||||
import { StickerEventPreview } from "./previews/StickerEventPreview";
|
||||
import { ReactionEventPreview } from "./previews/ReactionEventPreview";
|
||||
import { CreationEventPreview } from "./previews/CreationEventPreview";
|
||||
import { UPDATE_EVENT } from "../AsyncStore";
|
||||
|
||||
// Emitted event for when a room's preview has changed. First argument will the room for which
|
||||
// the change happened.
|
||||
export const ROOM_PREVIEW_CHANGED = "room_preview_changed";
|
||||
|
||||
const PREVIEWS = {
|
||||
'm.room.message': {
|
||||
isState: false,
|
||||
previewer: new MessageEventPreview(),
|
||||
},
|
||||
'm.room.name': {
|
||||
isState: true,
|
||||
previewer: new NameEventPreview(),
|
||||
},
|
||||
'm.room.topic': {
|
||||
isState: true,
|
||||
previewer: new TopicEventPreview(),
|
||||
},
|
||||
'm.room.member': {
|
||||
isState: true,
|
||||
previewer: new MembershipEventPreview(),
|
||||
},
|
||||
'm.room.history_visibility': {
|
||||
isState: true,
|
||||
previewer: new HistoryVisibilityEventPreview(),
|
||||
},
|
||||
'm.call.invite': {
|
||||
isState: false,
|
||||
previewer: new CallInviteEventPreview(),
|
||||
|
@ -67,14 +49,6 @@ const PREVIEWS = {
|
|||
isState: false,
|
||||
previewer: new CallHangupEvent(),
|
||||
},
|
||||
'm.room.encryption': {
|
||||
isState: true,
|
||||
previewer: new EncryptionEventPreview(),
|
||||
},
|
||||
'm.room.third_party_invite': {
|
||||
isState: true,
|
||||
previewer: new ThirdPartyInviteEventPreview(),
|
||||
},
|
||||
'm.sticker': {
|
||||
isState: false,
|
||||
previewer: new StickerEventPreview(),
|
||||
|
@ -83,10 +57,6 @@ const PREVIEWS = {
|
|||
isState: false,
|
||||
previewer: new ReactionEventPreview(),
|
||||
},
|
||||
'm.room.create': {
|
||||
isState: true,
|
||||
previewer: new CreationEventPreview(),
|
||||
},
|
||||
};
|
||||
|
||||
// The maximum number of events we're willing to look back on to get a preview.
|
||||
|
@ -97,12 +67,15 @@ type TAG_ANY = "im.vector.any";
|
|||
const TAG_ANY: TAG_ANY = "im.vector.any";
|
||||
|
||||
interface IState {
|
||||
[roomId: string]: Map<TagID | TAG_ANY, string | null>; // null indicates the preview is empty / irrelevant
|
||||
// Empty because we don't actually use the state
|
||||
}
|
||||
|
||||
export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new MessagePreviewStore();
|
||||
|
||||
// null indicates the preview is empty / irrelevant
|
||||
private previews = new Map<string, Map<TagID|TAG_ANY, string|null>>();
|
||||
|
||||
private constructor() {
|
||||
super(defaultDispatcher, {});
|
||||
}
|
||||
|
@ -120,10 +93,9 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
|||
public getPreviewForRoom(room: Room, inTagId: TagID): string {
|
||||
if (!room) return null; // invalid room, just return nothing
|
||||
|
||||
const val = this.state[room.roomId];
|
||||
if (!val) this.generatePreview(room, inTagId);
|
||||
if (!this.previews.has(room.roomId)) this.generatePreview(room, inTagId);
|
||||
|
||||
const previews = this.state[room.roomId];
|
||||
const previews = this.previews.get(room.roomId);
|
||||
if (!previews) return null;
|
||||
|
||||
if (!previews.has(inTagId)) {
|
||||
|
@ -136,11 +108,10 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
|||
const events = room.timeline;
|
||||
if (!events) return; // should only happen in tests
|
||||
|
||||
let map = this.state[room.roomId];
|
||||
let map = this.previews.get(room.roomId);
|
||||
if (!map) {
|
||||
map = new Map<TagID | TAG_ANY, string | null>();
|
||||
|
||||
// We set the state later with the map, so no need to send an update now
|
||||
this.previews.set(room.roomId, map);
|
||||
}
|
||||
|
||||
// Set the tags so we know what to generate
|
||||
|
@ -176,16 +147,18 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
if (changed) {
|
||||
// Update state for good measure - causes emit for update
|
||||
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
|
||||
this.updateState({[room.roomId]: map});
|
||||
// We've muted the underlying Map, so just emit that we've changed.
|
||||
this.previews.set(room.roomId, map);
|
||||
this.emit(UPDATE_EVENT, this);
|
||||
this.emit(ROOM_PREVIEW_CHANGED, room);
|
||||
}
|
||||
return; // we're done
|
||||
}
|
||||
|
||||
// At this point, we didn't generate a preview so clear it
|
||||
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
|
||||
this.updateState({[room.roomId]: null});
|
||||
this.previews.set(room.roomId, new Map<TagID|TAG_ANY, string|null>());
|
||||
this.emit(UPDATE_EVENT, this);
|
||||
this.emit(ROOM_PREVIEW_CHANGED, room);
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload) {
|
||||
|
@ -193,7 +166,7 @@ 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 (!Object.keys(this.state).includes(event.getRoomId())) return; // not important
|
||||
if (!this.previews.has(event.getRoomId())) return; // not important
|
||||
this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@ import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
|||
import RoomListLayoutStore from "./RoomListLayoutStore";
|
||||
import { MarkedExecution } from "../../utils/MarkedExecution";
|
||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||
import { NameFilterCondition } from "./filters/NameFilterCondition";
|
||||
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
|
||||
|
||||
interface IState {
|
||||
tagsEnabled?: boolean;
|
||||
|
@ -54,7 +56,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
private algorithm = new Algorithm();
|
||||
private filterConditions: IFilterCondition[] = [];
|
||||
private tagWatcher = new TagWatcher(this);
|
||||
private updateFn = new MarkedExecution(() => this.emit(LISTS_UPDATE_EVENT));
|
||||
private updateFn = new MarkedExecution(() => {
|
||||
for (const tagId of Object.keys(this.unfilteredLists)) {
|
||||
RoomNotificationStateStore.instance.getListState(tagId).setRooms(this.unfilteredLists[tagId]);
|
||||
}
|
||||
this.emit(LISTS_UPDATE_EVENT);
|
||||
});
|
||||
|
||||
private readonly watchedSettings = [
|
||||
'feature_custom_tags',
|
||||
|
@ -71,6 +78,11 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
this.algorithm.on(FILTER_CHANGED, this.onAlgorithmFilterUpdated);
|
||||
}
|
||||
|
||||
public get unfilteredLists(): ITagMap {
|
||||
if (!this.algorithm) return {}; // No tags yet.
|
||||
return this.algorithm.getUnfilteredRooms();
|
||||
}
|
||||
|
||||
public get orderedLists(): ITagMap {
|
||||
if (!this.algorithm) return {}; // No tags yet.
|
||||
return this.algorithm.getOrderedRooms();
|
||||
|
@ -168,6 +180,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload) {
|
||||
// If we're not remotely ready, don't even bother scheduling the dispatch handling.
|
||||
// This is repeated in the handler just in case things change between a decision here and
|
||||
// when the timer fires.
|
||||
const logicallyReady = this.matrixClient && this.initialListsGenerated;
|
||||
if (!logicallyReady) return;
|
||||
|
||||
// When we're running tests we can't reliably use setImmediate out of timing concerns.
|
||||
// As such, we use a more synchronous model.
|
||||
if (RoomListStoreClass.TEST_MODE) {
|
||||
|
@ -582,6 +600,20 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
this.updateFn.trigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first (and ideally only) name filter condition. If one isn't present,
|
||||
* this returns null.
|
||||
* @returns The first name filter condition, or null if none.
|
||||
*/
|
||||
public getFirstNameFilterCondition(): NameFilterCondition | null {
|
||||
for (const filter of this.filterConditions) {
|
||||
if (filter instanceof NameFilterCondition) {
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tags for a room identified by the store. The returned set
|
||||
* should never be empty, and will contain DefaultTagID.Untagged if
|
||||
|
|
|
@ -212,7 +212,18 @@ export class Algorithm extends EventEmitter {
|
|||
// We specifically do NOT use the ordered rooms set as it contains the sticky room, which
|
||||
// means we'll be off by 1 when the user is switching rooms. This leads to visual jumping
|
||||
// when the user is moving south in the list (not north, because of math).
|
||||
let position = this.getOrderedRoomsWithoutSticky()[tag].indexOf(val);
|
||||
const tagList = this.getOrderedRoomsWithoutSticky()[tag] || []; // can be null if filtering
|
||||
let position = tagList.indexOf(val);
|
||||
|
||||
// We do want to see if a tag change happened though - if this did happen then we'll want
|
||||
// to force the position to zero (top) to ensure we can properly handle it.
|
||||
const wasSticky = this._lastStickyRoom.room ? this._lastStickyRoom.room.roomId === val.roomId : false;
|
||||
if (this._lastStickyRoom.tag && tag !== this._lastStickyRoom.tag && wasSticky && position < 0) {
|
||||
console.warn(`Sticky room ${val.roomId} changed tags during sticky room handling`);
|
||||
position = 0;
|
||||
}
|
||||
|
||||
// Sanity check the position to make sure the room is qualified for being sticky
|
||||
if (position < 0) throw new Error(`${val.roomId} does not appear to be known and cannot be sticky`);
|
||||
|
||||
// 🐉 Here be dragons.
|
||||
|
@ -465,6 +476,10 @@ export class Algorithm extends EventEmitter {
|
|||
return this.filteredRooms;
|
||||
}
|
||||
|
||||
public getUnfilteredRooms(): ITagMap {
|
||||
return this._cachedStickyRooms || this.cachedRooms;
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns the same as getOrderedRooms(), but without the sticky room
|
||||
* map as it causes issues for sticky room handling (see sticky room handling
|
||||
|
@ -711,7 +726,9 @@ export class Algorithm extends EventEmitter {
|
|||
const algorithm: OrderingAlgorithm = this.algorithms[rmTag];
|
||||
if (!algorithm) throw new Error(`No algorithm for ${rmTag}`);
|
||||
await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
|
||||
this.cachedRooms[rmTag] = algorithm.orderedRooms;
|
||||
this._cachedRooms[rmTag] = algorithm.orderedRooms;
|
||||
this.recalculateFilteredRoomsForTag(rmTag); // update filter to re-sort the list
|
||||
this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed
|
||||
}
|
||||
for (const addTag of diff.added) {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
|
@ -721,7 +738,7 @@ export class Algorithm extends EventEmitter {
|
|||
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
|
||||
if (!algorithm) throw new Error(`No algorithm for ${addTag}`);
|
||||
await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom);
|
||||
this.cachedRooms[addTag] = algorithm.orderedRooms;
|
||||
this._cachedRooms[addTag] = algorithm.orderedRooms;
|
||||
}
|
||||
|
||||
// Update the tag map so we don't regen it in a moment
|
||||
|
@ -817,7 +834,7 @@ export class Algorithm extends EventEmitter {
|
|||
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
|
||||
|
||||
await algorithm.handleRoomUpdate(room, cause);
|
||||
this.cachedRooms[tag] = algorithm.orderedRooms;
|
||||
this._cachedRooms[tag] = algorithm.orderedRooms;
|
||||
|
||||
// Flag that we've done something
|
||||
this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list
|
||||
|
|
|
@ -90,7 +90,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
|||
private getRoomCategory(room: Room): NotificationColor {
|
||||
// It's fine for us to call this a lot because it's cached, and we shouldn't be
|
||||
// wasting anything by doing so as the store holds single references
|
||||
const state = RoomNotificationStateStore.instance.getRoomState(room, this.tagId);
|
||||
const state = RoomNotificationStateStore.instance.getRoomState(room);
|
||||
return state.color;
|
||||
}
|
||||
|
||||
|
@ -123,6 +123,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
|||
const category = this.getRoomCategory(room);
|
||||
this.alterCategoryPositionBy(category, 1, this.indices);
|
||||
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
|
||||
await this.sortCategory(category);
|
||||
} else if (cause === RoomUpdateCause.RoomRemoved) {
|
||||
const roomIdx = this.getRoomIndex(room);
|
||||
if (roomIdx === -1) {
|
||||
|
@ -135,6 +136,9 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
|||
} else {
|
||||
throw new Error(`Unhandled splice: ${cause}`);
|
||||
}
|
||||
|
||||
// changes have been made if we made it here, so say so
|
||||
return true;
|
||||
}
|
||||
|
||||
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
||||
|
|
|
@ -18,6 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
|||
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition";
|
||||
import { EventEmitter } from "events";
|
||||
import { removeHiddenChars } from "matrix-js-sdk/src/utils";
|
||||
import { throttle } from "lodash";
|
||||
|
||||
/**
|
||||
* A filter condition for the room list which reveals rooms of a particular
|
||||
|
@ -41,9 +42,13 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
|
|||
|
||||
public set search(val: string) {
|
||||
this._search = val;
|
||||
this.emit(FILTER_CHANGED);
|
||||
this.callUpdate();
|
||||
}
|
||||
|
||||
private callUpdate = throttle(() => {
|
||||
this.emit(FILTER_CHANGED);
|
||||
}, 200, {trailing: true, leading: true});
|
||||
|
||||
public isVisible(room: Room): boolean {
|
||||
const lcFilter = this.search.toLowerCase();
|
||||
if (this.search[0] === '#') {
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class CreationEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (isSelf(event)) {
|
||||
return _t("You created the room");
|
||||
} else {
|
||||
return _t("%(senderName)s created the room", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class EncryptionEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (isSelf(event)) {
|
||||
return _t("You made the chat encrypted");
|
||||
} else {
|
||||
return _t("%(senderName)s made the chat encrypted", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class HistoryVisibilityEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
const visibility = event.getContent()['history_visibility'];
|
||||
const isUs = isSelf(event);
|
||||
|
||||
if (visibility === 'invited' || visibility === 'joined') {
|
||||
return isUs
|
||||
? _t("You made history visible to new members")
|
||||
: _t("%(senderName)s made history visible to new members", {senderName: getSenderName(event)});
|
||||
} else if (visibility === 'world_readable') {
|
||||
return isUs
|
||||
? _t("You made history visible to anyone")
|
||||
: _t("%(senderName)s made history visible to anyone", {senderName: getSenderName(event)});
|
||||
} else { // shared, default
|
||||
return isUs
|
||||
? _t("You made history visible to future members")
|
||||
: _t("%(senderName)s made history visible to future members", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getTargetName, isSelfTarget } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class MembershipEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
const newMembership = event.getContent()['membership'];
|
||||
const oldMembership = event.getPrevContent()['membership'];
|
||||
const reason = event.getContent()['reason'];
|
||||
const isUs = isSelfTarget(event);
|
||||
|
||||
if (newMembership === 'invite') {
|
||||
return isUs
|
||||
? _t("You were invited")
|
||||
: _t("%(targetName)s was invited", {targetName: getTargetName(event)});
|
||||
} else if (newMembership === 'leave' && oldMembership !== 'invite') {
|
||||
if (event.getSender() === event.getStateKey()) {
|
||||
return isUs
|
||||
? _t("You left")
|
||||
: _t("%(targetName)s left", {targetName: getTargetName(event)});
|
||||
} else {
|
||||
if (reason) {
|
||||
return isUs
|
||||
? _t("You were kicked (%(reason)s)", {reason})
|
||||
: _t("%(targetName)s was kicked (%(reason)s)", {targetName: getTargetName(event), reason});
|
||||
} else {
|
||||
return isUs
|
||||
? _t("You were kicked")
|
||||
: _t("%(targetName)s was kicked", {targetName: getTargetName(event)});
|
||||
}
|
||||
}
|
||||
} else if (newMembership === 'leave' && oldMembership === 'invite') {
|
||||
if (event.getSender() === event.getStateKey()) {
|
||||
return isUs
|
||||
? _t("You rejected the invite")
|
||||
: _t("%(targetName)s rejected the invite", {targetName: getTargetName(event)});
|
||||
} else {
|
||||
return isUs
|
||||
? _t("You were uninvited")
|
||||
: _t("%(targetName)s was uninvited", {targetName: getTargetName(event)});
|
||||
}
|
||||
} else if (newMembership === 'ban') {
|
||||
if (reason) {
|
||||
return isUs
|
||||
? _t("You were banned (%(reason)s)", {reason})
|
||||
: _t("%(targetName)s was banned (%(reason)s)", {targetName: getTargetName(event), reason});
|
||||
} else {
|
||||
return isUs
|
||||
? _t("You were banned")
|
||||
: _t("%(targetName)s was banned", {targetName: getTargetName(event)});
|
||||
}
|
||||
} else if (newMembership === 'join' && oldMembership !== 'join') {
|
||||
return isUs
|
||||
? _t("You joined")
|
||||
: _t("%(targetName)s joined", {targetName: getTargetName(event)});
|
||||
} else {
|
||||
const isDisplayNameChange = event.getContent()['displayname'] !== event.getPrevContent()['displayname'];
|
||||
const isAvatarChange = event.getContent()['avatar_url'] !== event.getPrevContent()['avatar_url'];
|
||||
if (isDisplayNameChange) {
|
||||
return isUs
|
||||
? _t("You changed your name")
|
||||
: _t("%(targetName)s changed their name", {targetName: getTargetName(event)});
|
||||
} else if (isAvatarChange) {
|
||||
return isUs
|
||||
? _t("You changed your avatar")
|
||||
: _t("%(targetName)s changed their avatar", {targetName: getTargetName(event)});
|
||||
} else {
|
||||
return null; // no change
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -59,7 +59,7 @@ export class MessageEventPreview implements IPreview {
|
|||
}
|
||||
|
||||
if (msgtype === 'm.emote') {
|
||||
return _t("%(senderName)s %(emote)s", {senderName: getSenderName(event), emote: body});
|
||||
return _t("* %(senderName)s %(emote)s", {senderName: getSenderName(event), emote: body});
|
||||
}
|
||||
|
||||
if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class NameEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (isSelf(event)) {
|
||||
return _t("You changed the room name");
|
||||
} else {
|
||||
return _t("%(senderName)s changed the room name", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
|
||||
export class ThirdPartyInviteEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (!isValid3pidInvite(event)) {
|
||||
const targetName = event.getPrevContent().display_name || _t("Someone");
|
||||
if (isSelf(event)) {
|
||||
return _t("You uninvited %(targetName)s", {targetName});
|
||||
} else {
|
||||
return _t("%(senderName)s uninvited %(targetName)s", {senderName: getSenderName(event), targetName});
|
||||
}
|
||||
} else {
|
||||
const targetName = event.getContent().display_name;
|
||||
if (isSelf(event)) {
|
||||
return _t("You invited %(targetName)s", {targetName});
|
||||
} else {
|
||||
return _t("%(senderName)s invited %(targetName)s", {senderName: getSenderName(event), targetName});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class TopicEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (isSelf(event)) {
|
||||
return _t("You changed the room topic");
|
||||
} else {
|
||||
return _t("%(senderName)s changed the room topic", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue