Merge branch 'develop' into fayed/fix-verification-dialog

This commit is contained in:
Faye Duxovni 2021-10-05 08:48:52 -04:00
commit 651e943b31
145 changed files with 5361 additions and 1938 deletions

View file

@ -1,110 +0,0 @@
/*
Copyright 2018 New Vector Ltd
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 { MatrixClientPeg } from '../MatrixClientPeg';
import { WidgetMessagingStore } from "./widgets/WidgetMessagingStore";
/**
* Stores information about the widgets active in the app right now:
* * What widget is set to remain always-on-screen, if any
* Only one widget may be 'always on screen' at any one time.
* * Negotiated capabilities for active apps
*/
class ActiveWidgetStore extends EventEmitter {
constructor() {
super();
this._persistentWidgetId = null;
// What room ID each widget is associated with (if it's a room widget)
this._roomIdByWidgetId = {};
this.onRoomStateEvents = this.onRoomStateEvents.bind(this);
this.dispatcherRef = null;
}
start() {
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
}
stop() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
}
this._roomIdByWidgetId = {};
}
onRoomStateEvents(ev, state) {
// XXX: This listens for state events in order to remove the active widget.
// Everything else relies on views listening for events and calling setters
// on this class which is terrible. This store should just listen for events
// and keep itself up to date.
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
if (ev.getType() !== 'im.vector.modular.widgets') return;
if (ev.getStateKey() === this._persistentWidgetId) {
this.destroyPersistentWidget(this._persistentWidgetId);
}
}
destroyPersistentWidget(id) {
if (id !== this._persistentWidgetId) return;
const toDeleteId = this._persistentWidgetId;
WidgetMessagingStore.instance.stopMessagingById(id);
this.setWidgetPersistence(toDeleteId, false);
this.delRoomId(toDeleteId);
}
setWidgetPersistence(widgetId, val) {
if (this._persistentWidgetId === widgetId && !val) {
this._persistentWidgetId = null;
} else if (this._persistentWidgetId !== widgetId && val) {
this._persistentWidgetId = widgetId;
}
this.emit('update');
}
getWidgetPersistence(widgetId) {
return this._persistentWidgetId === widgetId;
}
getPersistentWidgetId() {
return this._persistentWidgetId;
}
getRoomId(widgetId) {
return this._roomIdByWidgetId[widgetId];
}
setRoomId(widgetId, roomId) {
this._roomIdByWidgetId[widgetId] = roomId;
this.emit('update');
}
delRoomId(widgetId) {
delete this._roomIdByWidgetId[widgetId];
this.emit('update');
}
}
if (global.singletonActiveWidgetStore === undefined) {
global.singletonActiveWidgetStore = new ActiveWidgetStore();
}
export default global.singletonActiveWidgetStore;

View file

@ -0,0 +1,112 @@
/*
Copyright 2018 New Vector Ltd
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 { MatrixEvent } from "matrix-js-sdk";
import { MatrixClientPeg } from '../MatrixClientPeg';
import { WidgetMessagingStore } from "./widgets/WidgetMessagingStore";
export enum ActiveWidgetStoreEvent {
Update = "update",
}
/**
* Stores information about the widgets active in the app right now:
* * What widget is set to remain always-on-screen, if any
* Only one widget may be 'always on screen' at any one time.
* * Negotiated capabilities for active apps
*/
export default class ActiveWidgetStore extends EventEmitter {
private static internalInstance: ActiveWidgetStore;
private persistentWidgetId: string;
// What room ID each widget is associated with (if it's a room widget)
private roomIdByWidgetId = new Map<string, string>();
public static get instance(): ActiveWidgetStore {
if (!ActiveWidgetStore.internalInstance) {
ActiveWidgetStore.internalInstance = new ActiveWidgetStore();
}
return ActiveWidgetStore.internalInstance;
}
public start(): void {
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
}
public stop(): void {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
}
this.roomIdByWidgetId.clear();
}
private onRoomStateEvents = (ev: MatrixEvent): void => {
// XXX: This listens for state events in order to remove the active widget.
// Everything else relies on views listening for events and calling setters
// on this class which is terrible. This store should just listen for events
// and keep itself up to date.
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
if (ev.getType() !== 'im.vector.modular.widgets') return;
if (ev.getStateKey() === this.persistentWidgetId) {
this.destroyPersistentWidget(this.persistentWidgetId);
}
};
public destroyPersistentWidget(id: string): void {
if (id !== this.persistentWidgetId) return;
const toDeleteId = this.persistentWidgetId;
WidgetMessagingStore.instance.stopMessagingById(id);
this.setWidgetPersistence(toDeleteId, false);
this.delRoomId(toDeleteId);
}
public setWidgetPersistence(widgetId: string, val: boolean): void {
if (this.persistentWidgetId === widgetId && !val) {
this.persistentWidgetId = null;
} else if (this.persistentWidgetId !== widgetId && val) {
this.persistentWidgetId = widgetId;
}
this.emit(ActiveWidgetStoreEvent.Update);
}
public getWidgetPersistence(widgetId: string): boolean {
return this.persistentWidgetId === widgetId;
}
public getPersistentWidgetId(): string {
return this.persistentWidgetId;
}
public getRoomId(widgetId: string): string {
return this.roomIdByWidgetId.get(widgetId);
}
public setRoomId(widgetId: string, roomId: string): void {
this.roomIdByWidgetId.set(widgetId, roomId);
this.emit(ActiveWidgetStoreEvent.Update);
}
public delRoomId(widgetId: string): void {
this.roomIdByWidgetId.delete(widgetId);
this.emit(ActiveWidgetStoreEvent.Update);
}
}
window.mxActiveWidgetStore = ActiveWidgetStore.instance;

View file

@ -14,13 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
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 { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { IRoomCapability } from "matrix-js-sdk/src/client";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
@ -41,12 +39,6 @@ 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 { shouldShowSpaceSettings } from "../utils/space";
import ToastStore from "./ToastStore";
import { _t } from "../languageHandler";
import GenericToast from "../components/views/toasts/GenericToast";
import Modal from "../Modal";
import InfoDialog from "../components/views/dialogs/InfoDialog";
import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
type SpaceKey = string | symbol;
@ -93,7 +85,7 @@ const validOrder = (order: string): string | undefined => {
// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id`
export const getChildOrder = (order: string, creationTs: number, roomId: string): Array<Many<ListIteratee<any>>> => {
return [validOrder(order), creationTs, roomId];
return [validOrder(order) ?? NaN, creationTs, roomId]; // NaN has lodash sort it at the end in asc
};
const getRoomFn: FetchRoomFn = (room: Room) => {
@ -191,7 +183,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
* 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 (space === this.activeSpace || (space && !space.isSpaceRoom())) return;
if (!this.matrixClient || space === this.activeSpace || (space && !space.isSpaceRoom())) return;
this._activeSpace = space;
this.emit(UPDATE_SELECTED_SPACE, this.activeSpace);
@ -205,7 +197,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
// 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" &&
this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join" &&
this.matrixClient.getRoom(roomId)?.getMyMembership() === "join" &&
this.getSpaceFilteredRoomIds(space).has(roomId)
) {
defaultDispatcher.dispatch({
@ -233,71 +225,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY);
}
// New in Spaces beta toast for Restricted Join Rule
const lsKey = "mx_SpaceBeta_restrictedJoinRuleToastSeen";
if (contextSwitch && space?.getJoinRule() === JoinRule.Invite && shouldShowSpaceSettings(space) &&
space.getJoinedMemberCount() > 1 && !localStorage.getItem(lsKey)
&& this.restrictedJoinRuleSupport?.preferred
) {
const toastKey = "restrictedjoinrule";
ToastStore.sharedInstance().addOrReplaceToast({
key: toastKey,
title: _t("New in the Spaces beta"),
props: {
description: _t("Help people in spaces to find and join private rooms"),
acceptLabel: _t("Learn more"),
onAccept: () => {
localStorage.setItem(lsKey, "true");
ToastStore.sharedInstance().dismissToast(toastKey);
Modal.createTrackedDialog("New in the Spaces beta", "restricted join rule", InfoDialog, {
title: _t("Help space members find private rooms"),
description: <>
<p>{ _t("To help space members find and join a private room, " +
"go to that room's Security & Privacy settings.") }</p>
{ /* Reuses classes from TabbedView for simplicity, non-interactive */ }
<div className="mx_TabbedView_tabsOnLeft" style={{ width: "190px", position: "relative" }}>
<div className="mx_TabbedView_tabLabel">
<span className="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_settingsIcon" />
<span className="mx_TabbedView_tabLabel_text">{ _t("General") }</span>
</div>
<div className="mx_TabbedView_tabLabel mx_TabbedView_tabLabel_active">
<span className="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_securityIcon" />
<span className="mx_TabbedView_tabLabel_text">{ _t("Security & Privacy") }</span>
</div>
<div className="mx_TabbedView_tabLabel">
<span className="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_rolesIcon" />
<span className="mx_TabbedView_tabLabel_text">{ _t("Roles & Permissions") }</span>
</div>
</div>
<p>{ _t("This makes it easy for rooms to stay private to a space, " +
"while letting people in the space find and join them. " +
"All new rooms in a space will have this option available.") }</p>
</>,
button: _t("OK"),
hasCloseButton: false,
fixedWidth: true,
});
},
rejectLabel: _t("Skip"),
onReject: () => {
localStorage.setItem(lsKey, "true");
ToastStore.sharedInstance().dismissToast(toastKey);
},
},
component: GenericToast,
priority: 35,
});
}
if (space) {
this.loadSuggestedRooms(space);
}
}
private async loadSuggestedRooms(space) {
private async loadSuggestedRooms(space: Room): Promise<void> {
const suggestedRooms = await this.fetchSuggestedRooms(space);
if (this._activeSpace === space) {
this._suggestedRooms = suggestedRooms;
@ -402,6 +335,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
};
private rebuild = throttle(() => {
if (!this.matrixClient) return;
const [visibleSpaces, visibleRooms] = partitionSpacesAndRooms(this.matrixClient.getVisibleRooms());
const [joinedSpaces, invitedSpaces] = visibleSpaces.reduce((arr, s) => {
if (s.getMyMembership() === "join") {
@ -595,7 +530,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
// Update NotificationStates
this.getNotificationState(s).setRooms(visibleRooms.filter(room => {
if (!roomIds.has(room.roomId)) return false;
if (!roomIds.has(room.roomId) || room.isSpaceRoom()) return false;
if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
return s === HOME_SPACE;
@ -673,6 +608,9 @@ 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) {
// user's active space has gone away, go back to home
this.setActiveSpace(null, true);
}
};

View file

@ -142,14 +142,14 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
// If a persistent widget is active, check to see if it's just been removed.
// If it has, it needs to destroyed otherwise unmounting the node won't kill it
const persistentWidgetId = ActiveWidgetStore.getPersistentWidgetId();
const persistentWidgetId = ActiveWidgetStore.instance.getPersistentWidgetId();
if (persistentWidgetId) {
if (
ActiveWidgetStore.getRoomId(persistentWidgetId) === room.roomId &&
ActiveWidgetStore.instance.getRoomId(persistentWidgetId) === room.roomId &&
!roomInfo.widgets.some(w => w.id === persistentWidgetId)
) {
logger.log(`Persistent widget ${persistentWidgetId} removed from room ${room.roomId}: destroying.`);
ActiveWidgetStore.destroyPersistentWidget(persistentWidgetId);
ActiveWidgetStore.instance.destroyPersistentWidget(persistentWidgetId);
}
}
@ -195,7 +195,7 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
// A persistent conference widget indicates that we're participating
const widgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
return widgets.some(w => ActiveWidgetStore.getWidgetPersistence(w.id));
return widgets.some(w => ActiveWidgetStore.instance.getWidgetPersistence(w.id));
}
}

View file

@ -15,20 +15,17 @@ limitations under the License.
*/
import { GenericEchoChamber, implicitlyReverted, PROPERTY_UPDATED } from "./GenericEchoChamber";
import { getRoomNotifsState, setRoomNotifsState } from "../../RoomNotifs";
import { getRoomNotifsState, RoomNotifState, 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>();
export class RoomEchoChamber extends GenericEchoChamber<RoomEchoContext, CachedRoomKey, RoomNotifState> {
private properties = new Map<CachedRoomKey, RoomNotifState>();
public constructor(context: RoomEchoContext) {
super(context, (k) => this.properties.get(k));
@ -50,8 +47,8 @@ export class RoomEchoChamber extends GenericEchoChamber<RoomEchoContext, CachedR
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;
const currentVolume = this.properties.get(CachedRoomKey.NotificationVolume) as RoomNotifState;
const newVolume = getRoomNotifsState(this.context.room.roomId) as RoomNotifState;
if (currentVolume !== newVolume) {
this.updateNotificationVolume();
}
@ -66,11 +63,11 @@ export class RoomEchoChamber extends GenericEchoChamber<RoomEchoContext, CachedR
// ---- helpers below here ----
public get notificationVolume(): Volume {
public get notificationVolume(): RoomNotifState {
return this.getValue(CachedRoomKey.NotificationVolume);
}
public set notificationVolume(v: Volume) {
public set notificationVolume(v: RoomNotifState) {
this.setValue(_t("Change notification settings"), CachedRoomKey.NotificationVolume, v, async () => {
return setRoomNotifsState(this.context.room.roomId, v);
}, implicitlyReverted);

View file

@ -20,7 +20,7 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import * as RoomNotifs from '../../RoomNotifs';
import * as Unread from '../../Unread';
import { NotificationState } from "./NotificationState";
@ -91,7 +91,7 @@ export class RoomNotificationState extends NotificationState implements IDestroy
this._color = NotificationColor.Unsent;
this._symbol = "!";
this._count = 1; // not used, technically
} else if (RoomNotifs.getRoomNotifsState(this.room.roomId) === RoomNotifs.MUTE) {
} else if (RoomNotifs.getRoomNotifsState(this.room.roomId) === RoomNotifs.RoomNotifState.Mute) {
// When muted we suppress all notification states, even if we have context on them.
this._color = NotificationColor.None;
this._symbol = null;
@ -101,8 +101,8 @@ export class RoomNotificationState extends NotificationState implements IDestroy
this._symbol = "!";
this._count = 1; // not used, technically
} else {
const redNotifs = RoomNotifs.getUnreadNotificationCount(this.room, 'highlight');
const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.room, 'total');
const redNotifs = RoomNotifs.getUnreadNotificationCount(this.room, NotificationCountType.Highlight);
const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.room, NotificationCountType.Total);
// For a 'true count' we pick the grey notifications first because they include the
// red notifications. If we don't have a grey count for some reason we use the red

View file

@ -266,7 +266,7 @@ export class StopGapWidget extends EventEmitter {
WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);
if (!this.appTileProps.userWidget && this.appTileProps.room) {
ActiveWidgetStore.setRoomId(this.mockWidget.id, this.appTileProps.room.roomId);
ActiveWidgetStore.instance.setRoomId(this.mockWidget.id, this.appTileProps.room.roomId);
}
// Always attach a handler for ViewRoom, but permission check it internally
@ -319,7 +319,7 @@ export class StopGapWidget extends EventEmitter {
if (WidgetType.JITSI.matches(this.mockWidget.type)) {
CountlyAnalytics.instance.trackJoinCall(this.appTileProps.room.roomId, true, true);
}
ActiveWidgetStore.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value);
ActiveWidgetStore.instance.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value);
ev.preventDefault();
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack
}
@ -406,13 +406,13 @@ export class StopGapWidget extends EventEmitter {
}
public stop(opts = { forceDestroy: false }) {
if (!opts?.forceDestroy && ActiveWidgetStore.getPersistentWidgetId() === this.mockWidget.id) {
if (!opts?.forceDestroy && ActiveWidgetStore.instance.getPersistentWidgetId() === this.mockWidget.id) {
logger.log("Skipping destroy - persistent widget");
return;
}
if (!this.started) return;
WidgetMessagingStore.instance.stopMessaging(this.mockWidget);
ActiveWidgetStore.delRoomId(this.mockWidget.id);
ActiveWidgetStore.instance.delRoomId(this.mockWidget.id);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().off('event', this.onEvent);