Merge remote-tracking branch 'origin/develop' into gsouquet/fix-backdrop-filter
* origin/develop: (1278 commits) Add a little padding Keep number field in focus when pressing dialpad buttons (#6520) Remove old version Fix video call persisting when widget removed Update link to matrix-js-sdk CONTRIBUTING file (#6557) $toast-bg-color -> $system $system-... -> $system Iterate PR based on feedback Remove unnecessary code Use AccessibleTooltipButton Just upload the PR object itself Edit PR Description instead of commenting publish the right directory Fix Netflify builds from fork PRs This doesn't need to be here as it was moved into CallViewButtons Make scrollbar dot transparent Iterate PR based on feedback Don't set hidden RRs labs setting at account level Add a comment for weirdly placed div Add full class names to animations.scss ...
This commit is contained in:
commit
5f9b55eaa9
690 changed files with 26605 additions and 12563 deletions
|
@ -22,6 +22,9 @@ import defaultDispatcher from "../dispatcher/dispatcher";
|
|||
import { arrayHasDiff } from "../utils/arrays";
|
||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
import SpaceStore from "./SpaceStore";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
|
||||
|
||||
const MAX_ROOMS = 20; // arbitrary
|
||||
const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up
|
||||
|
@ -62,10 +65,11 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
|||
protected async onAction(payload: ActionPayload) {
|
||||
if (!this.matrixClient) return;
|
||||
|
||||
if (payload.action === 'setting_updated') {
|
||||
if (payload.settingName === 'breadcrumb_rooms') {
|
||||
if (payload.action === Action.SettingUpdated) {
|
||||
const settingUpdatedPayload = payload as SettingUpdatedPayload;
|
||||
if (settingUpdatedPayload.settingName === 'breadcrumb_rooms') {
|
||||
await this.updateRooms();
|
||||
} else if (payload.settingName === 'breadcrumbs') {
|
||||
} else if (settingUpdatedPayload.settingName === 'breadcrumbs') {
|
||||
await this.updateState({ enabled: SettingsStore.getValue("breadcrumbs", null) });
|
||||
}
|
||||
} else if (payload.action === 'view_room') {
|
||||
|
@ -122,7 +126,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
private async appendRoom(room: Room) {
|
||||
if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) return; // hide space rooms
|
||||
if (SpaceStore.spacesEnabled && room.isSpaceRoom()) return; // hide space rooms
|
||||
let updated = false;
|
||||
const rooms = (this.state.rooms || []).slice(); // cheap clone
|
||||
|
||||
|
|
|
@ -14,12 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
import { Store } from 'flux/utils';
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import dis from '../dispatcher/dispatcher';
|
||||
import GroupStore from './GroupStore';
|
||||
import Analytics from '../Analytics';
|
||||
import * as RoomNotifs from "../RoomNotifs";
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { CreateEventField } from "../components/views/dialogs/CreateSpaceFromCommunityDialog";
|
||||
|
||||
const INITIAL_STATE = {
|
||||
orderedTags: null,
|
||||
|
@ -49,7 +51,7 @@ class GroupFilterOrderStore extends Store {
|
|||
this.__emitChange();
|
||||
}
|
||||
|
||||
__onDispatch(payload) {
|
||||
__onDispatch(payload) { // eslint-disable-line @typescript-eslint/naming-convention
|
||||
switch (payload.action) {
|
||||
// Initialise state after initial sync
|
||||
case 'view_room': {
|
||||
|
@ -235,8 +237,12 @@ class GroupFilterOrderStore extends Store {
|
|||
(t) => (t[0] !== '+' || groupIds.includes(t)) && !removedTags.has(t),
|
||||
);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const migratedCommunities = new Set(cli.getRooms().map(r => {
|
||||
return r.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent()[CreateEventField];
|
||||
}).filter(Boolean));
|
||||
const groupIdsToAdd = groupIds.filter(
|
||||
(groupId) => !tags.includes(groupId) && !removedTags.has(groupId),
|
||||
(groupId) => !tags.includes(groupId) && !removedTags.has(groupId) && !migratedCommunities.has(groupId),
|
||||
);
|
||||
|
||||
return tagsToKeep.concat(groupIdsToAdd);
|
||||
|
|
|
@ -20,11 +20,11 @@ import FlairStore from './FlairStore';
|
|||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import dis from '../dispatcher/dispatcher';
|
||||
|
||||
function parseMembersResponse(response) {
|
||||
export function parseMembersResponse(response) {
|
||||
return response.chunk.map((apiMember) => groupMemberFromApiObject(apiMember));
|
||||
}
|
||||
|
||||
function parseRoomsResponse(response) {
|
||||
export function parseRoomsResponse(response) {
|
||||
return response.chunk.map((apiRoom) => groupRoomFromApiObject(apiRoom));
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ class LifecycleStore extends Store<ActionPayload> {
|
|||
this.__emitChange();
|
||||
}
|
||||
|
||||
protected __onDispatch(payload: ActionPayload) {
|
||||
protected __onDispatch(payload: ActionPayload) { // eslint-disable-line @typescript-eslint/naming-convention
|
||||
switch (payload.action) {
|
||||
case 'do_after_sync_prepared':
|
||||
this.setState({
|
||||
|
@ -56,7 +56,7 @@ class LifecycleStore extends Store<ActionPayload> {
|
|||
deferredAction: null,
|
||||
});
|
||||
break;
|
||||
case 'syncstate': {
|
||||
case 'sync_state': {
|
||||
if (payload.state !== 'PREPARED') {
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -144,7 +144,7 @@ export default class RightPanelStore extends Store<ActionPayload> {
|
|||
this.__emitChange();
|
||||
}
|
||||
|
||||
__onDispatch(payload: ActionPayload) {
|
||||
__onDispatch(payload: ActionPayload) { // eslint-disable-line @typescript-eslint/naming-convention
|
||||
switch (payload.action) {
|
||||
case 'view_room':
|
||||
if (payload.room_id === this.lastRoomId) break; // skip this transition, probably a permalink
|
||||
|
|
|
@ -96,7 +96,7 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
this.__emitChange();
|
||||
}
|
||||
|
||||
__onDispatch(payload) {
|
||||
__onDispatch(payload) { // eslint-disable-line @typescript-eslint/naming-convention
|
||||
switch (payload.action) {
|
||||
// view_room:
|
||||
// - room_alias: '#somealias:matrix.org'
|
||||
|
@ -325,8 +325,8 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
msg = _t("There was an error joining the room");
|
||||
} else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') {
|
||||
msg = <div>
|
||||
{_t("Sorry, your homeserver is too old to participate in this room.")}<br />
|
||||
{_t("Please contact your homeserver administrator.")}
|
||||
{ _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);
|
||||
|
@ -429,7 +429,7 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
}
|
||||
}
|
||||
|
||||
let singletonRoomViewStore = null;
|
||||
let singletonRoomViewStore: RoomViewStore = null;
|
||||
if (!singletonRoomViewStore) {
|
||||
singletonRoomViewStore = new RoomViewStore();
|
||||
}
|
||||
|
|
|
@ -14,10 +14,14 @@ 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 { ISpaceSummaryRoom } 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";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
|
@ -31,13 +35,19 @@ import { RoomNotificationStateStore } from "./notifications/RoomNotificationStat
|
|||
import { DefaultTagID } from "./room-list/models";
|
||||
import { EnhancedMap, mapDiff } from "../utils/maps";
|
||||
import { setHasDiff } from "../utils/sets";
|
||||
import { ISpaceSummaryEvent, ISpaceSummaryRoom } from "../components/structures/SpaceRoomDirectory";
|
||||
import RoomViewStore from "./RoomViewStore";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
import { arrayHasDiff } from "../utils/arrays";
|
||||
import { arrayHasDiff, arrayHasOrderChange } from "../utils/arrays";
|
||||
import { objectDiff } from "../utils/objects";
|
||||
import { arrayHasOrderChange } from "../utils/arrays";
|
||||
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;
|
||||
|
||||
|
@ -51,6 +61,7 @@ export const SUGGESTED_ROOMS = Symbol("suggested-rooms");
|
|||
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
|
||||
export const UPDATE_INVITED_SPACES = Symbol("invited-spaces");
|
||||
export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
|
||||
export const UPDATE_HOME_BEHAVIOUR = Symbol("home-behaviour");
|
||||
// Space Room ID/HOME_SPACE will be emitted when a Space's children change
|
||||
|
||||
export interface ISuggestedRoom extends ISpaceSummaryRoom {
|
||||
|
@ -59,8 +70,10 @@ export interface ISuggestedRoom extends ISpaceSummaryRoom {
|
|||
|
||||
const MAX_SUGGESTED_ROOMS = 20;
|
||||
|
||||
const homeSpaceKey = SettingsStore.getValue("feature_spaces.all_rooms") ? "ALL_ROOMS" : "HOME_SPACE";
|
||||
const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || homeSpaceKey}`;
|
||||
// This setting causes the page to reload and can be costly if read frequently, so read it here only
|
||||
const spacesEnabled = SettingsStore.getValue("feature_spaces");
|
||||
|
||||
const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "HOME_SPACE"}`;
|
||||
|
||||
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
|
||||
return arr.reduce((result, room: Room) => {
|
||||
|
@ -88,10 +101,6 @@ const getRoomFn: FetchRoomFn = (room: Room) => {
|
|||
};
|
||||
|
||||
export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
constructor() {
|
||||
super(defaultDispatcher, {});
|
||||
}
|
||||
|
||||
// The spaces representing the roots of the various tree-like hierarchies
|
||||
private rootSpaces: Room[] = [];
|
||||
// The list of rooms not present in any currently joined spaces
|
||||
|
@ -107,6 +116,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
private _suggestedRooms: ISuggestedRoom[] = [];
|
||||
private _invitedSpaces = new Set<Room>();
|
||||
private spaceOrderLocalEchoMap = new Map<string, string>();
|
||||
private _restrictedJoinRuleSupport?: IRoomCapability;
|
||||
private _allRoomsInHome: boolean = SettingsStore.getValue("Spaces.allRoomsInHome");
|
||||
|
||||
constructor() {
|
||||
super(defaultDispatcher, {});
|
||||
|
||||
SettingsStore.monitorSetting("Spaces.allRoomsInHome", null);
|
||||
}
|
||||
|
||||
public get invitedSpaces(): Room[] {
|
||||
return Array.from(this._invitedSpaces);
|
||||
|
@ -124,6 +141,48 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
return this._suggestedRooms;
|
||||
}
|
||||
|
||||
public get allRoomsInHome(): boolean {
|
||||
return this._allRoomsInHome;
|
||||
}
|
||||
|
||||
public async setActiveRoomInSpace(space: Room | null): Promise<void> {
|
||||
if (space && !space.isSpaceRoom()) return;
|
||||
if (space !== this.activeSpace) await this.setActiveSpace(space);
|
||||
|
||||
if (space) {
|
||||
const roomId = this.getNotificationState(space.roomId).getFirstRoomWithNotifications();
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_id: roomId,
|
||||
context_switch: true,
|
||||
});
|
||||
} else {
|
||||
const lists = RoomListStore.instance.unfilteredLists;
|
||||
for (let i = 0; i < TAG_ORDER.length; i++) {
|
||||
const t = TAG_ORDER[i];
|
||||
const listRooms = lists[t];
|
||||
const unreadRoom = listRooms.find((r: Room) => {
|
||||
if (this.showInHomeSpace(r)) {
|
||||
const state = RoomNotificationStateStore.instance.getRoomState(r);
|
||||
return state.isUnread;
|
||||
}
|
||||
});
|
||||
if (unreadRoom) {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_id: unreadRoom.roomId,
|
||||
context_switch: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get restrictedJoinRuleSupport(): IRoomCapability {
|
||||
return this._restrictedJoinRuleSupport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active space, updates room list filters,
|
||||
* optionally switches the user's room back to where they were when they last viewed that space.
|
||||
|
@ -132,7 +191,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 async setActiveSpace(space: Room | null, contextSwitch = true) {
|
||||
if (space === this.activeSpace || (space && !space?.isSpaceRoom())) return;
|
||||
if (space === this.activeSpace || (space && !space.isSpaceRoom())) return;
|
||||
|
||||
this._activeSpace = space;
|
||||
this.emit(UPDATE_SELECTED_SPACE, this.activeSpace);
|
||||
|
@ -146,7 +205,8 @@ 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({
|
||||
action: "view_room",
|
||||
|
@ -173,6 +233,65 @@ 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 style={{ width: "190px" }}>
|
||||
<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) {
|
||||
const suggestedRooms = await this.fetchSuggestedRooms(space);
|
||||
if (this._activeSpace === space) {
|
||||
|
@ -184,10 +303,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
|
||||
public fetchSuggestedRooms = async (space: Room, limit = MAX_SUGGESTED_ROOMS): Promise<ISuggestedRoom[]> => {
|
||||
try {
|
||||
const data: {
|
||||
rooms: ISpaceSummaryRoom[];
|
||||
events: ISpaceSummaryEvent[];
|
||||
} = await this.matrixClient.getSpaceSummary(space.roomId, 0, true, false, limit);
|
||||
const data = await this.matrixClient.getSpaceSummary(space.roomId, 0, true, false, limit);
|
||||
|
||||
const viaMap = new EnhancedMap<string, Set<string>>();
|
||||
data.events.forEach(ev => {
|
||||
|
@ -219,7 +335,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}, roomId);
|
||||
}
|
||||
|
||||
private getChildren(spaceId: string): Room[] {
|
||||
public getChildren(spaceId: string): Room[] {
|
||||
const room = this.matrixClient?.getRoom(spaceId);
|
||||
const childEvents = room?.currentState.getStateEvents(EventType.SpaceChild).filter(ev => ev.getContent()?.via);
|
||||
return sortBy(childEvents, ev => {
|
||||
|
@ -262,8 +378,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
return sortBy(parents, r => r.roomId)?.[0] || null;
|
||||
}
|
||||
|
||||
public getKnownParents(roomId: string): Set<string> {
|
||||
return this.parentMap.get(roomId) || new Set();
|
||||
}
|
||||
|
||||
public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => {
|
||||
if (!space && SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||
if (!space && this.allRoomsInHome) {
|
||||
return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId));
|
||||
}
|
||||
return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
|
||||
|
@ -360,7 +480,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
};
|
||||
|
||||
private showInHomeSpace = (room: Room) => {
|
||||
if (SettingsStore.getValue("feature_spaces.all_rooms")) return true;
|
||||
if (this.allRoomsInHome) return true;
|
||||
if (room.isSpaceRoom()) return false;
|
||||
return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space
|
||||
|| DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space
|
||||
|
@ -392,7 +512,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
const oldFilteredRooms = this.spaceFilteredRooms;
|
||||
this.spaceFilteredRooms = new Map();
|
||||
|
||||
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||
if (!this.allRoomsInHome) {
|
||||
// put all room invites in the Home Space
|
||||
const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite");
|
||||
this.spaceFilteredRooms.set(HOME_SPACE, new Set<string>(invites.map(room => room.roomId)));
|
||||
|
@ -419,15 +539,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
const roomIds = new Set(childRooms.map(r => r.roomId));
|
||||
const space = this.matrixClient?.getRoom(spaceId);
|
||||
|
||||
if (SettingsStore.getValue("feature_spaces.space_member_dms")) {
|
||||
// Add relevant DMs
|
||||
space?.getMembers().forEach(member => {
|
||||
if (member.membership !== "join" && member.membership !== "invite") return;
|
||||
DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => {
|
||||
roomIds.add(roomId);
|
||||
});
|
||||
// Add relevant DMs
|
||||
space?.getMembers().forEach(member => {
|
||||
if (member.membership !== "join" && member.membership !== "invite") return;
|
||||
DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => {
|
||||
roomIds.add(roomId);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const newPath = new Set(parentPath).add(spaceId);
|
||||
childSpaces.forEach(childSpace => {
|
||||
|
@ -450,16 +568,17 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
});
|
||||
|
||||
this.spaceFilteredRooms.forEach((roomIds, s) => {
|
||||
// Update NotificationStates
|
||||
this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => {
|
||||
if (roomIds.has(room.roomId)) {
|
||||
if (s !== HOME_SPACE && SettingsStore.getValue("feature_spaces.space_dm_badges")) return true;
|
||||
if (this.allRoomsInHome && s === HOME_SPACE) return; // we'll be using the global notification state, skip
|
||||
|
||||
return !DMRoomMap.shared().getUserIdForRoomId(room.roomId)
|
||||
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite);
|
||||
// Update NotificationStates
|
||||
this.getNotificationState(s).setRooms(visibleRooms.filter(room => {
|
||||
if (!roomIds.has(room.roomId)) return false;
|
||||
|
||||
if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
|
||||
return s === HOME_SPACE;
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}));
|
||||
});
|
||||
}, 100, { trailing: true, leading: true });
|
||||
|
@ -552,7 +671,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
// TODO confirm this after implementing parenting behaviour
|
||||
if (room.isSpaceRoom()) {
|
||||
this.onSpaceUpdate();
|
||||
} else if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||
} else if (!this.allRoomsInHome) {
|
||||
this.onRoomUpdate(room);
|
||||
}
|
||||
this.emit(room.roomId);
|
||||
|
@ -576,7 +695,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
if (order !== lastOrder) {
|
||||
this.notifyIfOrderChanged();
|
||||
}
|
||||
} else if (ev.getType() === EventType.Tag && !SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||
} else if (ev.getType() === EventType.Tag && !this.allRoomsInHome) {
|
||||
// If the room was in favourites and now isn't or the opposite then update its position in the trees
|
||||
const oldTags = lastEv?.getContent()?.tags || {};
|
||||
const newTags = ev.getContent()?.tags || {};
|
||||
|
@ -587,7 +706,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
};
|
||||
|
||||
private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => {
|
||||
if (ev.getType() === EventType.Direct) {
|
||||
if (!this.allRoomsInHome && ev.getType() === EventType.Direct) {
|
||||
const lastContent = lastEvent.getContent();
|
||||
const content = ev.getContent();
|
||||
|
||||
|
@ -616,28 +735,29 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
protected async onNotReady() {
|
||||
if (!SettingsStore.getValue("feature_spaces")) return;
|
||||
if (!SpaceStore.spacesEnabled) return;
|
||||
if (this.matrixClient) {
|
||||
this.matrixClient.removeListener("Room", this.onRoom);
|
||||
this.matrixClient.removeListener("Room.myMembership", this.onRoom);
|
||||
this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData);
|
||||
this.matrixClient.removeListener("RoomState.events", this.onRoomState);
|
||||
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||
this.matrixClient.removeListener("accountData", this.onAccountData);
|
||||
}
|
||||
this.matrixClient.removeListener("accountData", this.onAccountData);
|
||||
}
|
||||
await this.reset();
|
||||
}
|
||||
|
||||
protected async onReady() {
|
||||
if (!SettingsStore.getValue("feature_spaces")) return;
|
||||
if (!spacesEnabled) return;
|
||||
this.matrixClient.on("Room", this.onRoom);
|
||||
this.matrixClient.on("Room.myMembership", this.onRoom);
|
||||
this.matrixClient.on("Room.accountData", this.onRoomAccountData);
|
||||
this.matrixClient.on("RoomState.events", this.onRoomState);
|
||||
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||
this.matrixClient.on("accountData", this.onAccountData);
|
||||
}
|
||||
this.matrixClient.on("accountData", this.onAccountData);
|
||||
|
||||
this.matrixClient.getCapabilities().then(capabilities => {
|
||||
this._restrictedJoinRuleSupport = capabilities
|
||||
?.["m.room_versions"]?.["org.matrix.msc3244.room_capabilities"]?.["restricted"];
|
||||
});
|
||||
|
||||
await this.onSpaceUpdate(); // trigger an initial update
|
||||
|
||||
|
@ -649,7 +769,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload) {
|
||||
if (!SettingsStore.getValue("feature_spaces")) return;
|
||||
if (!spacesEnabled) return;
|
||||
switch (payload.action) {
|
||||
case "view_room": {
|
||||
// Don't auto-switch rooms when reacting to a context-switch
|
||||
|
@ -663,7 +783,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
// as it will cause you to end up in the wrong room
|
||||
this.setActiveSpace(room, false);
|
||||
} else if (
|
||||
(!SettingsStore.getValue("feature_spaces.all_rooms") || this.activeSpace) &&
|
||||
(!this.allRoomsInHome || this.activeSpace) &&
|
||||
!this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)
|
||||
) {
|
||||
this.switchToRelatedSpace(roomId);
|
||||
|
@ -675,17 +795,33 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
window.localStorage.setItem(getSpaceContextKey(this.activeSpace), payload.room_id);
|
||||
break;
|
||||
}
|
||||
|
||||
case "after_leave_room":
|
||||
if (this._activeSpace && payload.room_id === this._activeSpace.roomId) {
|
||||
this.setActiveSpace(null, false);
|
||||
}
|
||||
break;
|
||||
|
||||
case Action.SwitchSpace:
|
||||
if (payload.num === 0) {
|
||||
this.setActiveSpace(null);
|
||||
} else if (this.spacePanelSpaces.length >= payload.num) {
|
||||
this.setActiveSpace(this.spacePanelSpaces[payload.num - 1]);
|
||||
}
|
||||
break;
|
||||
|
||||
case Action.SettingUpdated: {
|
||||
const settingUpdatedPayload = payload as SettingUpdatedPayload;
|
||||
if (settingUpdatedPayload.settingName === "Spaces.allRoomsInHome") {
|
||||
const newValue = SettingsStore.getValue("Spaces.allRoomsInHome");
|
||||
if (this.allRoomsInHome !== newValue) {
|
||||
this._allRoomsInHome = newValue;
|
||||
this.emit(UPDATE_HOME_BEHAVIOUR, this.allRoomsInHome);
|
||||
this.rebuild(); // rebuild everything
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -755,6 +891,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
export default class SpaceStore {
|
||||
public static spacesEnabled = spacesEnabled;
|
||||
|
||||
private static internalInstance = new SpaceStoreClass();
|
||||
|
||||
public static get instance(): SpaceStoreClass {
|
||||
|
|
|
@ -22,10 +22,11 @@ export interface IToast<C extends ComponentClass> {
|
|||
key: string;
|
||||
// higher priority number will be shown on top of lower priority
|
||||
priority: number;
|
||||
title: string;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
component: C;
|
||||
className?: string;
|
||||
bodyClassName?: string;
|
||||
props?: Omit<React.ComponentProps<C>, "toastKey">; // toastKey is injected by ToastContainer
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import EventEmitter from "events";
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
// XXX: resize-observer-polyfill has types that now conflict with typescript's
|
||||
// own DOM types: https://github.com/que-etc/resize-observer-polyfill/issues/80
|
||||
// Using require here rather than import is a horrenous workaround. We should
|
||||
// be able to remove the polyfill once Safari 14 is released.
|
||||
const ResizeObserverPolyfill = require('resize-observer-polyfill'); // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
import ResizeObserverEntry from 'resize-observer-polyfill/src/ResizeObserverEntry';
|
||||
|
||||
export enum UI_EVENTS {
|
||||
|
@ -43,7 +47,7 @@ export default class UIStore extends EventEmitter {
|
|||
// eslint-disable-next-line no-restricted-properties
|
||||
this.windowHeight = window.innerHeight;
|
||||
|
||||
this.resizeObserver = new ResizeObserver(this.resizeObserverCallback);
|
||||
this.resizeObserver = new ResizeObserverPolyfill(this.resizeObserverCallback);
|
||||
this.resizeObserver.observe(document.body);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
import { VoiceRecording } from "../voice/VoiceRecording";
|
||||
import { VoiceRecording } from "../audio/VoiceRecording";
|
||||
|
||||
interface IState {
|
||||
recording?: VoiceRecording;
|
||||
|
|
|
@ -137,6 +137,20 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
|
|||
if (edited && !this.roomMap.has(room.roomId)) {
|
||||
this.roomMap.set(room.roomId, roomInfo);
|
||||
}
|
||||
|
||||
// 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();
|
||||
if (persistentWidgetId) {
|
||||
if (
|
||||
ActiveWidgetStore.getRoomId(persistentWidgetId) === room.roomId &&
|
||||
!roomInfo.widgets.some(w => w.id === persistentWidgetId)
|
||||
) {
|
||||
console.log(`Persistent widget ${persistentWidgetId} removed from room ${room.roomId}: destroying.`);
|
||||
ActiveWidgetStore.destroyPersistentWidget(persistentWidgetId);
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(room.roomId);
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import { NOTIFICATION_STATE_UPDATE, NotificationState } from "./NotificationStat
|
|||
import { FetchRoomFn } from "./ListNotificationState";
|
||||
|
||||
export class SpaceNotificationState extends NotificationState {
|
||||
private rooms: Room[] = [];
|
||||
public rooms: Room[] = []; // exposed only for tests
|
||||
private states: { [spaceId: string]: RoomNotificationState } = {};
|
||||
|
||||
constructor(private spaceId: string | symbol, private getRoomFn: FetchRoomFn) {
|
||||
|
@ -53,6 +53,10 @@ export class SpaceNotificationState extends NotificationState {
|
|||
this.calculateTotalState();
|
||||
}
|
||||
|
||||
public getFirstRoomWithNotifications() {
|
||||
return this.rooms.find((room) => room.getUnreadNotificationCount() > 0).roomId;
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
for (const state of Object.values(this.states)) {
|
||||
|
|
|
@ -63,7 +63,7 @@ const PREVIEWS = {
|
|||
const MAX_EVENTS_BACKWARDS = 50;
|
||||
|
||||
// type merging ftw
|
||||
type TAG_ANY = "im.vector.any";
|
||||
type TAG_ANY = "im.vector.any"; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
const TAG_ANY: TAG_ANY = "im.vector.any";
|
||||
|
||||
interface IState {
|
||||
|
|
|
@ -35,6 +35,9 @@ import { NameFilterCondition } from "./filters/NameFilterCondition";
|
|||
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
|
||||
import { VisibilityProvider } from "./filters/VisibilityProvider";
|
||||
import { SpaceWatcher } from "./SpaceWatcher";
|
||||
import SpaceStore from "../SpaceStore";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";
|
||||
|
||||
interface IState {
|
||||
tagsEnabled?: boolean;
|
||||
|
@ -73,10 +76,11 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
|
||||
constructor() {
|
||||
super(defaultDispatcher);
|
||||
this.setMaxListeners(20); // CustomRoomTagStore + RoomList + LeftPanel + 8xRoomSubList + spares
|
||||
}
|
||||
|
||||
private setupWatchers() {
|
||||
if (SettingsStore.getValue("feature_spaces")) {
|
||||
if (SpaceStore.spacesEnabled) {
|
||||
this.spaceWatcher = new SpaceWatcher(this);
|
||||
} else {
|
||||
this.tagWatcher = new TagWatcher(this);
|
||||
|
@ -130,8 +134,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
// Update any settings here, as some may have happened before we were logically ready.
|
||||
console.log("Regenerating room lists: Startup");
|
||||
await this.readAndCacheSettingsFromStore();
|
||||
await this.regenerateAllLists({ trigger: false });
|
||||
await this.handleRVSUpdate({ trigger: false }); // fake an RVS update to adjust sticky room, if needed
|
||||
this.regenerateAllLists({ trigger: false });
|
||||
this.handleRVSUpdate({ trigger: false }); // fake an RVS update to adjust sticky room, if needed
|
||||
|
||||
this.updateFn.mark(); // we almost certainly want to trigger an update.
|
||||
this.updateFn.trigger();
|
||||
|
@ -148,7 +152,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
await this.updateState({
|
||||
tagsEnabled,
|
||||
});
|
||||
await this.updateAlgorithmInstances();
|
||||
this.updateAlgorithmInstances();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -156,23 +160,23 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
* @param trigger Set to false to prevent a list update from being sent. Should only
|
||||
* be used if the calling code will manually trigger the update.
|
||||
*/
|
||||
private async handleRVSUpdate({ trigger = true }) {
|
||||
private handleRVSUpdate({ trigger = true }) {
|
||||
if (!this.matrixClient) return; // We assume there won't be RVS updates without a client
|
||||
|
||||
const activeRoomId = RoomViewStore.getRoomId();
|
||||
if (!activeRoomId && this.algorithm.stickyRoom) {
|
||||
await this.algorithm.setStickyRoom(null);
|
||||
this.algorithm.setStickyRoom(null);
|
||||
} else if (activeRoomId) {
|
||||
const activeRoom = this.matrixClient.getRoom(activeRoomId);
|
||||
if (!activeRoom) {
|
||||
console.warn(`${activeRoomId} is current in RVS but missing from client - clearing sticky room`);
|
||||
await this.algorithm.setStickyRoom(null);
|
||||
this.algorithm.setStickyRoom(null);
|
||||
} else if (activeRoom !== this.algorithm.stickyRoom) {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`Changing sticky room to ${activeRoomId}`);
|
||||
}
|
||||
await this.algorithm.setStickyRoom(activeRoom);
|
||||
this.algorithm.setStickyRoom(activeRoom);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -211,10 +215,11 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
const logicallyReady = this.matrixClient && this.initialListsGenerated;
|
||||
if (!logicallyReady) return;
|
||||
|
||||
if (payload.action === 'setting_updated') {
|
||||
if (this.watchedSettings.includes(payload.settingName)) {
|
||||
if (payload.action === Action.SettingUpdated) {
|
||||
const settingUpdatedPayload = payload as SettingUpdatedPayload;
|
||||
if (this.watchedSettings.includes(settingUpdatedPayload.settingName)) {
|
||||
// TODO: Remove with https://github.com/vector-im/element-web/issues/14602
|
||||
if (payload.settingName === "advancedRoomListLogging") {
|
||||
if (settingUpdatedPayload.settingName === "advancedRoomListLogging") {
|
||||
// Log when the setting changes so we know when it was turned on in the rageshake
|
||||
const enabled = SettingsStore.getValue("advancedRoomListLogging");
|
||||
console.warn("Advanced room list logging is enabled? " + enabled);
|
||||
|
@ -224,7 +229,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
console.log("Regenerating room lists: Settings changed");
|
||||
await this.readAndCacheSettingsFromStore();
|
||||
|
||||
await this.regenerateAllLists({ trigger: false }); // regenerate the lists now
|
||||
this.regenerateAllLists({ trigger: false }); // regenerate the lists now
|
||||
this.updateFn.trigger();
|
||||
}
|
||||
}
|
||||
|
@ -366,7 +371,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`[RoomListDebug] Clearing sticky room due to room upgrade`);
|
||||
}
|
||||
await this.algorithm.setStickyRoom(null);
|
||||
this.algorithm.setStickyRoom(null);
|
||||
}
|
||||
|
||||
// Note: we hit the algorithm instead of our handleRoomUpdate() function to
|
||||
|
@ -375,7 +380,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`[RoomListDebug] Removing previous room from room list`);
|
||||
}
|
||||
await this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved);
|
||||
this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -431,7 +436,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
return; // don't do anything on new/moved rooms which ought not to be shown
|
||||
}
|
||||
|
||||
const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
|
||||
const shouldUpdate = this.algorithm.handleRoomUpdate(room, cause);
|
||||
if (shouldUpdate) {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
|
@ -460,13 +465,13 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
|
||||
// Reset the sticky room before resetting the known rooms so the algorithm
|
||||
// doesn't freak out.
|
||||
await this.algorithm.setStickyRoom(null);
|
||||
await this.algorithm.setKnownRooms(rooms);
|
||||
this.algorithm.setStickyRoom(null);
|
||||
this.algorithm.setKnownRooms(rooms);
|
||||
|
||||
// Set the sticky room back, if needed, now that we have updated the store.
|
||||
// This will use relative stickyness to the new room set.
|
||||
if (stickyIsStillPresent) {
|
||||
await this.algorithm.setStickyRoom(currentSticky);
|
||||
this.algorithm.setStickyRoom(currentSticky);
|
||||
}
|
||||
|
||||
// Finally, mark an update and resume updates from the algorithm
|
||||
|
@ -475,12 +480,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
public async setTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
||||
await this.setAndPersistTagSorting(tagId, sort);
|
||||
this.setAndPersistTagSorting(tagId, sort);
|
||||
this.updateFn.trigger();
|
||||
}
|
||||
|
||||
private async setAndPersistTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
||||
await this.algorithm.setTagSorting(tagId, sort);
|
||||
private setAndPersistTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
||||
this.algorithm.setTagSorting(tagId, sort);
|
||||
// TODO: Per-account? https://github.com/vector-im/element-web/issues/14114
|
||||
localStorage.setItem(`mx_tagSort_${tagId}`, sort);
|
||||
}
|
||||
|
@ -518,13 +523,13 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
return tagSort;
|
||||
}
|
||||
|
||||
public async setListOrder(tagId: TagID, order: ListAlgorithm) {
|
||||
await this.setAndPersistListOrder(tagId, order);
|
||||
public setListOrder(tagId: TagID, order: ListAlgorithm) {
|
||||
this.setAndPersistListOrder(tagId, order);
|
||||
this.updateFn.trigger();
|
||||
}
|
||||
|
||||
private async setAndPersistListOrder(tagId: TagID, order: ListAlgorithm) {
|
||||
await this.algorithm.setListOrdering(tagId, order);
|
||||
private setAndPersistListOrder(tagId: TagID, order: ListAlgorithm) {
|
||||
this.algorithm.setListOrdering(tagId, order);
|
||||
// TODO: Per-account? https://github.com/vector-im/element-web/issues/14114
|
||||
localStorage.setItem(`mx_listOrder_${tagId}`, order);
|
||||
}
|
||||
|
@ -561,7 +566,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
return listOrder;
|
||||
}
|
||||
|
||||
private async updateAlgorithmInstances() {
|
||||
private updateAlgorithmInstances() {
|
||||
// We'll require an update, so mark for one. Marking now also prevents the calls
|
||||
// to setTagSorting and setListOrder from causing triggers.
|
||||
this.updateFn.mark();
|
||||
|
@ -574,10 +579,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
const listOrder = this.calculateListOrder(tag);
|
||||
|
||||
if (tagSort !== definedSort) {
|
||||
await this.setAndPersistTagSorting(tag, tagSort);
|
||||
this.setAndPersistTagSorting(tag, tagSort);
|
||||
}
|
||||
if (listOrder !== definedOrder) {
|
||||
await this.setAndPersistListOrder(tag, listOrder);
|
||||
this.setAndPersistListOrder(tag, listOrder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -608,9 +613,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
|
||||
// if spaces are enabled only consider the prefilter conditions when there are no runtime conditions
|
||||
// for the search all spaces feature
|
||||
if (this.prefilterConditions.length > 0
|
||||
&& (!SettingsStore.getValue("feature_spaces") || !this.filterConditions.length)
|
||||
) {
|
||||
if (this.prefilterConditions.length > 0 && (!SpaceStore.spacesEnabled || !this.filterConditions.length)) {
|
||||
rooms = rooms.filter(r => {
|
||||
for (const filter of this.prefilterConditions) {
|
||||
if (!filter.isVisible(r)) {
|
||||
|
@ -632,7 +635,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
* @param trigger Set to false to prevent a list update from being sent. Should only
|
||||
* be used if the calling code will manually trigger the update.
|
||||
*/
|
||||
public async regenerateAllLists({ trigger = true }) {
|
||||
public regenerateAllLists({ trigger = true }) {
|
||||
console.warn("Regenerating all room lists");
|
||||
|
||||
const rooms = this.getPlausibleRooms();
|
||||
|
@ -656,8 +659,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
RoomListLayoutStore.instance.ensureLayoutExists(tagId);
|
||||
}
|
||||
|
||||
await this.algorithm.populateTags(sorts, orders);
|
||||
await this.algorithm.setKnownRooms(rooms);
|
||||
this.algorithm.populateTags(sorts, orders);
|
||||
this.algorithm.setKnownRooms(rooms);
|
||||
|
||||
this.initialListsGenerated = true;
|
||||
|
||||
|
@ -682,7 +685,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
} else {
|
||||
this.filterConditions.push(filter);
|
||||
// Runtime filters with spaces disable prefiltering for the search all spaces feature
|
||||
if (SettingsStore.getValue("feature_spaces")) {
|
||||
if (SpaceStore.spacesEnabled) {
|
||||
// this has to be awaited so that `setKnownRooms` is called in time for the `addFilterCondition` below
|
||||
// this way the runtime filters are only evaluated on one dataset and not both.
|
||||
await this.recalculatePrefiltering();
|
||||
|
@ -708,6 +711,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
let promise = Promise.resolve();
|
||||
let idx = this.filterConditions.indexOf(filter);
|
||||
let removed = false;
|
||||
if (idx >= 0) {
|
||||
this.filterConditions.splice(idx, 1);
|
||||
|
||||
|
@ -715,17 +719,23 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
this.algorithm.removeFilterCondition(filter);
|
||||
}
|
||||
// Runtime filters with spaces disable prefiltering for the search all spaces feature
|
||||
if (SettingsStore.getValue("feature_spaces")) {
|
||||
if (SpaceStore.spacesEnabled) {
|
||||
promise = this.recalculatePrefiltering();
|
||||
}
|
||||
removed = true;
|
||||
}
|
||||
|
||||
idx = this.prefilterConditions.indexOf(filter);
|
||||
if (idx >= 0) {
|
||||
filter.off(FILTER_CHANGED, this.onPrefilterUpdated);
|
||||
this.prefilterConditions.splice(idx, 1);
|
||||
promise = this.recalculatePrefiltering();
|
||||
removed = true;
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
promise.then(() => this.updateFn.trigger());
|
||||
}
|
||||
promise.then(() => this.updateFn.trigger());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,40 +18,47 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
|||
|
||||
import { RoomListStoreClass } from "./RoomListStore";
|
||||
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
|
||||
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import SpaceStore, { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../SpaceStore";
|
||||
|
||||
/**
|
||||
* Watches for changes in spaces to manage the filter on the provided RoomListStore
|
||||
*/
|
||||
export class SpaceWatcher {
|
||||
private filter: SpaceFilterCondition;
|
||||
private readonly filter = new SpaceFilterCondition();
|
||||
// we track these separately to the SpaceStore as we need to observe transitions
|
||||
private activeSpace: Room = SpaceStore.instance.activeSpace;
|
||||
private allRoomsInHome: boolean = SpaceStore.instance.allRoomsInHome;
|
||||
|
||||
constructor(private store: RoomListStoreClass) {
|
||||
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||
this.filter = new SpaceFilterCondition();
|
||||
if (!this.allRoomsInHome || this.activeSpace) {
|
||||
this.updateFilter();
|
||||
store.addFilter(this.filter);
|
||||
}
|
||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated);
|
||||
SpaceStore.instance.on(UPDATE_HOME_BEHAVIOUR, this.onHomeBehaviourUpdated);
|
||||
}
|
||||
|
||||
private onSelectedSpaceUpdated = (activeSpace?: Room) => {
|
||||
this.activeSpace = activeSpace;
|
||||
private onSelectedSpaceUpdated = (activeSpace?: Room, allRoomsInHome = this.allRoomsInHome) => {
|
||||
if (activeSpace === this.activeSpace && allRoomsInHome === this.allRoomsInHome) return; // nop
|
||||
|
||||
if (this.filter) {
|
||||
if (activeSpace || !SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||
this.updateFilter();
|
||||
} else {
|
||||
this.store.removeFilter(this.filter);
|
||||
this.filter = null;
|
||||
}
|
||||
} else if (activeSpace) {
|
||||
this.filter = new SpaceFilterCondition();
|
||||
const oldActiveSpace = this.activeSpace;
|
||||
const oldAllRoomsInHome = this.allRoomsInHome;
|
||||
this.activeSpace = activeSpace;
|
||||
this.allRoomsInHome = allRoomsInHome;
|
||||
|
||||
if (activeSpace || !allRoomsInHome) {
|
||||
this.updateFilter();
|
||||
this.store.addFilter(this.filter);
|
||||
}
|
||||
|
||||
if (oldAllRoomsInHome && !oldActiveSpace) {
|
||||
this.store.addFilter(this.filter);
|
||||
} else if (allRoomsInHome && !activeSpace) {
|
||||
this.store.removeFilter(this.filter);
|
||||
}
|
||||
};
|
||||
|
||||
private onHomeBehaviourUpdated = (allRoomsInHome: boolean) => {
|
||||
this.onSelectedSpaceUpdated(this.activeSpace, allRoomsInHome);
|
||||
};
|
||||
|
||||
private updateFilter = () => {
|
||||
|
|
|
@ -16,8 +16,9 @@ limitations under the License.
|
|||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { arrayDiff, arrayHasDiff } from "../../../utils/arrays";
|
||||
import { DefaultTagID, RoomUpdateCause, TagID } from "../models";
|
||||
import {
|
||||
|
@ -34,6 +35,7 @@ import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
|
|||
import { getListAlgorithmInstance } from "./list-ordering";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { VisibilityProvider } from "../filters/VisibilityProvider";
|
||||
import SpaceStore from "../../SpaceStore";
|
||||
|
||||
/**
|
||||
* Fired when the Algorithm has determined a list has been updated.
|
||||
|
@ -121,8 +123,12 @@ export class Algorithm extends EventEmitter {
|
|||
* Awaitable version of the sticky room setter.
|
||||
* @param val The new room to sticky.
|
||||
*/
|
||||
public async setStickyRoom(val: Room) {
|
||||
await this.updateStickyRoom(val);
|
||||
public setStickyRoom(val: Room) {
|
||||
try {
|
||||
this.updateStickyRoom(val);
|
||||
} catch (e) {
|
||||
console.warn("Failed to update sticky room", e);
|
||||
}
|
||||
}
|
||||
|
||||
public getTagSorting(tagId: TagID): SortAlgorithm {
|
||||
|
@ -130,13 +136,13 @@ export class Algorithm extends EventEmitter {
|
|||
return this.sortAlgorithms[tagId];
|
||||
}
|
||||
|
||||
public async setTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
||||
public setTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
||||
if (!tagId) throw new Error("Tag ID must be defined");
|
||||
if (!sort) throw new Error("Algorithm must be defined");
|
||||
this.sortAlgorithms[tagId] = sort;
|
||||
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[tagId];
|
||||
await algorithm.setSortAlgorithm(sort);
|
||||
algorithm.setSortAlgorithm(sort);
|
||||
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
||||
this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
|
||||
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
|
||||
|
@ -147,7 +153,7 @@ export class Algorithm extends EventEmitter {
|
|||
return this.listAlgorithms[tagId];
|
||||
}
|
||||
|
||||
public async setListOrdering(tagId: TagID, order: ListAlgorithm) {
|
||||
public setListOrdering(tagId: TagID, order: ListAlgorithm) {
|
||||
if (!tagId) throw new Error("Tag ID must be defined");
|
||||
if (!order) throw new Error("Algorithm must be defined");
|
||||
this.listAlgorithms[tagId] = order;
|
||||
|
@ -155,7 +161,7 @@ export class Algorithm extends EventEmitter {
|
|||
const algorithm = getListAlgorithmInstance(order, tagId, this.sortAlgorithms[tagId]);
|
||||
this.algorithms[tagId] = algorithm;
|
||||
|
||||
await algorithm.setRooms(this._cachedRooms[tagId]);
|
||||
algorithm.setRooms(this._cachedRooms[tagId]);
|
||||
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
||||
this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
|
||||
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
|
||||
|
@ -182,31 +188,25 @@ export class Algorithm extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
private async handleFilterChange() {
|
||||
await this.recalculateFilteredRooms();
|
||||
private handleFilterChange() {
|
||||
this.recalculateFilteredRooms();
|
||||
|
||||
// re-emit the update so the list store can fire an off-cycle update if needed
|
||||
if (this.updatesInhibited) return;
|
||||
this.emit(FILTER_CHANGED);
|
||||
}
|
||||
|
||||
private async updateStickyRoom(val: Room) {
|
||||
try {
|
||||
return await this.doUpdateStickyRoom(val);
|
||||
} finally {
|
||||
this._lastStickyRoom = null; // clear to indicate we're done changing
|
||||
}
|
||||
private updateStickyRoom(val: Room) {
|
||||
this.doUpdateStickyRoom(val);
|
||||
this._lastStickyRoom = null; // clear to indicate we're done changing
|
||||
}
|
||||
|
||||
private async doUpdateStickyRoom(val: Room) {
|
||||
if (SettingsStore.getValue("feature_spaces") && val?.isSpaceRoom() && val.getMyMembership() !== "invite") {
|
||||
private doUpdateStickyRoom(val: Room) {
|
||||
if (SpaceStore.spacesEnabled && val?.isSpaceRoom() && val.getMyMembership() !== "invite") {
|
||||
// no-op sticky rooms for spaces - they're effectively virtual rooms
|
||||
val = null;
|
||||
}
|
||||
|
||||
// Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
|
||||
// otherwise we risk duplicating rooms.
|
||||
|
||||
if (val && !VisibilityProvider.instance.isRoomVisible(val)) {
|
||||
val = null; // the room isn't visible - lie to the rest of this function
|
||||
}
|
||||
|
@ -222,7 +222,7 @@ export class Algorithm extends EventEmitter {
|
|||
this._stickyRoom = null; // clear before we go to update the algorithm
|
||||
|
||||
// Lie to the algorithm and re-add the room to the algorithm
|
||||
await this.handleRoomUpdate(stickyRoom, RoomUpdateCause.NewRoom);
|
||||
this.handleRoomUpdate(stickyRoom, RoomUpdateCause.NewRoom);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
|
@ -268,10 +268,10 @@ export class Algorithm extends EventEmitter {
|
|||
// referential checks as the references can differ through the lifecycle.
|
||||
if (lastStickyRoom && lastStickyRoom.room && lastStickyRoom.room.roomId !== val.roomId) {
|
||||
// Lie to the algorithm and re-add the room to the algorithm
|
||||
await this.handleRoomUpdate(lastStickyRoom.room, RoomUpdateCause.NewRoom);
|
||||
this.handleRoomUpdate(lastStickyRoom.room, RoomUpdateCause.NewRoom);
|
||||
}
|
||||
// Lie to the algorithm and remove the room from it's field of view
|
||||
await this.handleRoomUpdate(val, RoomUpdateCause.RoomRemoved);
|
||||
this.handleRoomUpdate(val, RoomUpdateCause.RoomRemoved);
|
||||
|
||||
// Check for tag & position changes while we're here. We also check the room to ensure
|
||||
// it is still the same room.
|
||||
|
@ -461,9 +461,8 @@ export class Algorithm extends EventEmitter {
|
|||
* them.
|
||||
* @param {ITagSortingMap} tagSortingMap The tags to generate.
|
||||
* @param {IListOrderingMap} listOrderingMap The ordering of those tags.
|
||||
* @returns {Promise<*>} A promise which resolves when complete.
|
||||
*/
|
||||
public async populateTags(tagSortingMap: ITagSortingMap, listOrderingMap: IListOrderingMap): Promise<any> {
|
||||
public populateTags(tagSortingMap: ITagSortingMap, listOrderingMap: IListOrderingMap): void {
|
||||
if (!tagSortingMap) throw new Error(`Sorting map cannot be null or empty`);
|
||||
if (!listOrderingMap) throw new Error(`Ordering ma cannot be null or empty`);
|
||||
if (arrayHasDiff(Object.keys(tagSortingMap), Object.keys(listOrderingMap))) {
|
||||
|
@ -512,9 +511,8 @@ export class Algorithm extends EventEmitter {
|
|||
* Seeds the Algorithm with a set of rooms. The algorithm will discard all
|
||||
* previously known information and instead use these rooms instead.
|
||||
* @param {Room[]} rooms The rooms to force the algorithm to use.
|
||||
* @returns {Promise<*>} A promise which resolves when complete.
|
||||
*/
|
||||
public async setKnownRooms(rooms: Room[]): Promise<any> {
|
||||
public setKnownRooms(rooms: Room[]): void {
|
||||
if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`);
|
||||
if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`);
|
||||
|
||||
|
@ -528,7 +526,7 @@ export class Algorithm extends EventEmitter {
|
|||
// Before we go any further we need to clear (but remember) the sticky room to
|
||||
// avoid accidentally duplicating it in the list.
|
||||
const oldStickyRoom = this._stickyRoom;
|
||||
await this.updateStickyRoom(null);
|
||||
if (oldStickyRoom) this.updateStickyRoom(null);
|
||||
|
||||
this.rooms = rooms;
|
||||
|
||||
|
@ -540,7 +538,7 @@ export class Algorithm extends EventEmitter {
|
|||
|
||||
// If we can avoid doing work, do so.
|
||||
if (!rooms.length) {
|
||||
await this.generateFreshTags(newTags); // just in case it wants to do something
|
||||
this.generateFreshTags(newTags); // just in case it wants to do something
|
||||
this.cachedRooms = newTags;
|
||||
return;
|
||||
}
|
||||
|
@ -577,7 +575,7 @@ export class Algorithm extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
await this.generateFreshTags(newTags);
|
||||
this.generateFreshTags(newTags);
|
||||
|
||||
this.cachedRooms = newTags; // this recalculates the filtered rooms for us
|
||||
this.updateTagsFromCache();
|
||||
|
@ -586,7 +584,7 @@ export class Algorithm extends EventEmitter {
|
|||
// it was. It's entirely possible that it changed lists though, so if it did then
|
||||
// we also have to update the position of it.
|
||||
if (oldStickyRoom && oldStickyRoom.room) {
|
||||
await this.updateStickyRoom(oldStickyRoom.room);
|
||||
this.updateStickyRoom(oldStickyRoom.room);
|
||||
if (this._stickyRoom && this._stickyRoom.room) { // just in case the update doesn't go according to plan
|
||||
if (this._stickyRoom.tag !== oldStickyRoom.tag) {
|
||||
// We put the sticky room at the top of the list to treat it as an obvious tag change.
|
||||
|
@ -651,16 +649,15 @@ export class Algorithm extends EventEmitter {
|
|||
* @param {ITagMap} updatedTagMap The tag map which needs populating. Each tag
|
||||
* will already have the rooms which belong to it - they just need ordering. Must
|
||||
* be mutated in place.
|
||||
* @returns {Promise<*>} A promise which resolves when complete.
|
||||
*/
|
||||
private async generateFreshTags(updatedTagMap: ITagMap): Promise<any> {
|
||||
private generateFreshTags(updatedTagMap: ITagMap): void {
|
||||
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
|
||||
|
||||
for (const tag of Object.keys(updatedTagMap)) {
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[tag];
|
||||
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
|
||||
|
||||
await algorithm.setRooms(updatedTagMap[tag]);
|
||||
algorithm.setRooms(updatedTagMap[tag]);
|
||||
updatedTagMap[tag] = algorithm.orderedRooms;
|
||||
}
|
||||
}
|
||||
|
@ -672,11 +669,10 @@ export class Algorithm extends EventEmitter {
|
|||
* may no-op this request if no changes are required.
|
||||
* @param {Room} room The room which might have affected sorting.
|
||||
* @param {RoomUpdateCause} cause The reason for the update being triggered.
|
||||
* @returns {Promise<boolean>} A promise which resolve to true or false
|
||||
* depending on whether or not getOrderedRooms() should be called after
|
||||
* processing.
|
||||
* @returns {Promise<boolean>} A boolean of whether or not getOrderedRooms()
|
||||
* should be called after processing.
|
||||
*/
|
||||
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
||||
public handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
|
||||
|
@ -684,9 +680,9 @@ export class Algorithm extends EventEmitter {
|
|||
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
|
||||
|
||||
// Note: check the isSticky against the room ID just in case the reference is wrong
|
||||
const isSticky = this._stickyRoom && this._stickyRoom.room && this._stickyRoom.room.roomId === room.roomId;
|
||||
const isSticky = this._stickyRoom?.room?.roomId === room.roomId;
|
||||
if (cause === RoomUpdateCause.NewRoom) {
|
||||
const isForLastSticky = this._lastStickyRoom && this._lastStickyRoom.room === room;
|
||||
const isForLastSticky = this._lastStickyRoom?.room === room;
|
||||
const roomTags = this.roomIdsToTags[room.roomId];
|
||||
const hasTags = roomTags && roomTags.length > 0;
|
||||
|
||||
|
@ -743,7 +739,7 @@ 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);
|
||||
algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
|
||||
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
|
||||
|
@ -755,7 +751,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);
|
||||
algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom);
|
||||
this._cachedRooms[addTag] = algorithm.orderedRooms;
|
||||
}
|
||||
|
||||
|
@ -788,7 +784,7 @@ export class Algorithm extends EventEmitter {
|
|||
};
|
||||
} else {
|
||||
// We have to clear the lock as the sticky room change will trigger updates.
|
||||
await this.setStickyRoom(room);
|
||||
this.setStickyRoom(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -851,7 +847,7 @@ export class Algorithm extends EventEmitter {
|
|||
const algorithm: OrderingAlgorithm = this.algorithms[tag];
|
||||
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
|
||||
|
||||
await algorithm.handleRoomUpdate(room, cause);
|
||||
algorithm.handleRoomUpdate(room, cause);
|
||||
this._cachedRooms[tag] = algorithm.orderedRooms;
|
||||
|
||||
// Flag that we've done something
|
||||
|
|
|
@ -94,15 +94,15 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
|||
return state.color;
|
||||
}
|
||||
|
||||
public async setRooms(rooms: Room[]): Promise<any> {
|
||||
public setRooms(rooms: Room[]): void {
|
||||
if (this.sortingAlgorithm === SortAlgorithm.Manual) {
|
||||
this.cachedOrderedRooms = await sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm);
|
||||
this.cachedOrderedRooms = sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm);
|
||||
} else {
|
||||
// Every other sorting type affects the categories, not the whole tag.
|
||||
const categorized = this.categorizeRooms(rooms);
|
||||
for (const category of Object.keys(categorized)) {
|
||||
const roomsToOrder = categorized[category];
|
||||
categorized[category] = await sortRoomsWithAlgorithm(roomsToOrder, this.tagId, this.sortingAlgorithm);
|
||||
categorized[category] = sortRoomsWithAlgorithm(roomsToOrder, this.tagId, this.sortingAlgorithm);
|
||||
}
|
||||
|
||||
const newlyOrganized: Room[] = [];
|
||||
|
@ -118,12 +118,12 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
|||
}
|
||||
}
|
||||
|
||||
private async handleSplice(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
||||
private handleSplice(room: Room, cause: RoomUpdateCause): boolean {
|
||||
if (cause === RoomUpdateCause.NewRoom) {
|
||||
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);
|
||||
this.sortCategory(category);
|
||||
} else if (cause === RoomUpdateCause.RoomRemoved) {
|
||||
const roomIdx = this.getRoomIndex(room);
|
||||
if (roomIdx === -1) {
|
||||
|
@ -141,55 +141,49 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
|||
return true;
|
||||
}
|
||||
|
||||
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
||||
try {
|
||||
await this.updateLock.acquireAsync();
|
||||
|
||||
if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) {
|
||||
return this.handleSplice(room, cause);
|
||||
}
|
||||
|
||||
if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) {
|
||||
throw new Error(`Unsupported update cause: ${cause}`);
|
||||
}
|
||||
|
||||
const category = this.getRoomCategory(room);
|
||||
if (this.sortingAlgorithm === SortAlgorithm.Manual) {
|
||||
return; // Nothing to do here.
|
||||
}
|
||||
|
||||
const roomIdx = this.getRoomIndex(room);
|
||||
if (roomIdx === -1) {
|
||||
throw new Error(`Room ${room.roomId} has no index in ${this.tagId}`);
|
||||
}
|
||||
|
||||
// Try to avoid doing array operations if we don't have to: only move rooms within
|
||||
// the categories if we're jumping categories
|
||||
const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices);
|
||||
if (oldCategory !== category) {
|
||||
// Move the room and update the indices
|
||||
this.moveRoomIndexes(1, oldCategory, category, this.indices);
|
||||
this.cachedOrderedRooms.splice(roomIdx, 1); // splice out the old index (fixed position)
|
||||
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
|
||||
// Note: if moveRoomIndexes() is called after the splice then the insert operation
|
||||
// will happen in the wrong place. Because we would have already adjusted the index
|
||||
// for the category, we don't need to determine how the room is moving in the list.
|
||||
// If we instead tried to insert before updating the indices, we'd have to determine
|
||||
// whether the room was moving later (towards IDLE) or earlier (towards RED) from its
|
||||
// current position, as it'll affect the category's start index after we remove the
|
||||
// room from the array.
|
||||
}
|
||||
|
||||
// Sort the category now that we've dumped the room in
|
||||
await this.sortCategory(category);
|
||||
|
||||
return true; // change made
|
||||
} finally {
|
||||
await this.updateLock.release();
|
||||
public handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean {
|
||||
if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) {
|
||||
return this.handleSplice(room, cause);
|
||||
}
|
||||
|
||||
if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) {
|
||||
throw new Error(`Unsupported update cause: ${cause}`);
|
||||
}
|
||||
|
||||
const category = this.getRoomCategory(room);
|
||||
if (this.sortingAlgorithm === SortAlgorithm.Manual) {
|
||||
return; // Nothing to do here.
|
||||
}
|
||||
|
||||
const roomIdx = this.getRoomIndex(room);
|
||||
if (roomIdx === -1) {
|
||||
throw new Error(`Room ${room.roomId} has no index in ${this.tagId}`);
|
||||
}
|
||||
|
||||
// Try to avoid doing array operations if we don't have to: only move rooms within
|
||||
// the categories if we're jumping categories
|
||||
const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices);
|
||||
if (oldCategory !== category) {
|
||||
// Move the room and update the indices
|
||||
this.moveRoomIndexes(1, oldCategory, category, this.indices);
|
||||
this.cachedOrderedRooms.splice(roomIdx, 1); // splice out the old index (fixed position)
|
||||
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
|
||||
// Note: if moveRoomIndexes() is called after the splice then the insert operation
|
||||
// will happen in the wrong place. Because we would have already adjusted the index
|
||||
// for the category, we don't need to determine how the room is moving in the list.
|
||||
// If we instead tried to insert before updating the indices, we'd have to determine
|
||||
// whether the room was moving later (towards IDLE) or earlier (towards RED) from its
|
||||
// current position, as it'll affect the category's start index after we remove the
|
||||
// room from the array.
|
||||
}
|
||||
|
||||
// Sort the category now that we've dumped the room in
|
||||
this.sortCategory(category);
|
||||
|
||||
return true; // change made
|
||||
}
|
||||
|
||||
private async sortCategory(category: NotificationColor) {
|
||||
private sortCategory(category: NotificationColor) {
|
||||
// This should be relatively quick because the room is usually inserted at the top of the
|
||||
// category, and most popular sorting algorithms will deal with trying to keep the active
|
||||
// room at the top/start of the category. For the few algorithms that will have to move the
|
||||
|
@ -201,7 +195,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
|||
const startIdx = this.indices[category];
|
||||
const numSort = nextCategoryStartIdx - startIdx; // splice() returns up to the max, so MAX_SAFE_INT is fine
|
||||
const unsortedSlice = this.cachedOrderedRooms.splice(startIdx, numSort);
|
||||
const sorted = await sortRoomsWithAlgorithm(unsortedSlice, this.tagId, this.sortingAlgorithm);
|
||||
const sorted = sortRoomsWithAlgorithm(unsortedSlice, this.tagId, this.sortingAlgorithm);
|
||||
this.cachedOrderedRooms.splice(startIdx, 0, ...sorted);
|
||||
}
|
||||
|
||||
|
|
|
@ -29,42 +29,32 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
|
|||
super(tagId, initialSortingAlgorithm);
|
||||
}
|
||||
|
||||
public async setRooms(rooms: Room[]): Promise<any> {
|
||||
this.cachedOrderedRooms = await sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm);
|
||||
public setRooms(rooms: Room[]): void {
|
||||
this.cachedOrderedRooms = sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm);
|
||||
}
|
||||
|
||||
public async handleRoomUpdate(room, cause): Promise<boolean> {
|
||||
try {
|
||||
await this.updateLock.acquireAsync();
|
||||
|
||||
const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved;
|
||||
const isInPlace = cause === RoomUpdateCause.Timeline || cause === RoomUpdateCause.ReadReceipt;
|
||||
if (!isSplice && !isInPlace) {
|
||||
throw new Error(`Unsupported update cause: ${cause}`);
|
||||
}
|
||||
|
||||
if (cause === RoomUpdateCause.NewRoom) {
|
||||
this.cachedOrderedRooms.push(room);
|
||||
} else if (cause === RoomUpdateCause.RoomRemoved) {
|
||||
const idx = this.getRoomIndex(room);
|
||||
if (idx >= 0) {
|
||||
this.cachedOrderedRooms.splice(idx, 1);
|
||||
} else {
|
||||
console.warn(`Tried to remove unknown room from ${this.tagId}: ${room.roomId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Optimize this to avoid useless operations: https://github.com/vector-im/element-web/issues/14457
|
||||
// For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
|
||||
this.cachedOrderedRooms = await sortRoomsWithAlgorithm(
|
||||
this.cachedOrderedRooms,
|
||||
this.tagId,
|
||||
this.sortingAlgorithm,
|
||||
);
|
||||
|
||||
return true;
|
||||
} finally {
|
||||
await this.updateLock.release();
|
||||
public handleRoomUpdate(room, cause): boolean {
|
||||
const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved;
|
||||
const isInPlace = cause === RoomUpdateCause.Timeline || cause === RoomUpdateCause.ReadReceipt;
|
||||
if (!isSplice && !isInPlace) {
|
||||
throw new Error(`Unsupported update cause: ${cause}`);
|
||||
}
|
||||
|
||||
if (cause === RoomUpdateCause.NewRoom) {
|
||||
this.cachedOrderedRooms.push(room);
|
||||
} else if (cause === RoomUpdateCause.RoomRemoved) {
|
||||
const idx = this.getRoomIndex(room);
|
||||
if (idx >= 0) {
|
||||
this.cachedOrderedRooms.splice(idx, 1);
|
||||
} else {
|
||||
console.warn(`Tried to remove unknown room from ${this.tagId}: ${room.roomId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Optimize this to avoid useless operations: https://github.com/vector-im/element-web/issues/14457
|
||||
// For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
|
||||
this.cachedOrderedRooms = sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomUpdateCause, TagID } from "../../models";
|
||||
import { SortAlgorithm } from "../models";
|
||||
import AwaitLock from "await-lock";
|
||||
|
||||
/**
|
||||
* Represents a list ordering algorithm. Subclasses should populate the
|
||||
|
@ -26,7 +25,6 @@ import AwaitLock from "await-lock";
|
|||
export abstract class OrderingAlgorithm {
|
||||
protected cachedOrderedRooms: Room[];
|
||||
protected sortingAlgorithm: SortAlgorithm;
|
||||
protected readonly updateLock = new AwaitLock();
|
||||
|
||||
protected constructor(protected tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
|
@ -45,21 +43,20 @@ export abstract class OrderingAlgorithm {
|
|||
* @param newAlgorithm The new algorithm. Must be defined.
|
||||
* @returns Resolves when complete.
|
||||
*/
|
||||
public async setSortAlgorithm(newAlgorithm: SortAlgorithm) {
|
||||
public setSortAlgorithm(newAlgorithm: SortAlgorithm) {
|
||||
if (!newAlgorithm) throw new Error("A sorting algorithm must be defined");
|
||||
this.sortingAlgorithm = newAlgorithm;
|
||||
|
||||
// Force regeneration of the rooms
|
||||
await this.setRooms(this.orderedRooms);
|
||||
this.setRooms(this.orderedRooms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the rooms the algorithm should be handling, implying a reconstruction
|
||||
* of the ordering.
|
||||
* @param rooms The rooms to use going forward.
|
||||
* @returns Resolves when complete.
|
||||
*/
|
||||
public abstract setRooms(rooms: Room[]): Promise<any>;
|
||||
public abstract setRooms(rooms: Room[]): void;
|
||||
|
||||
/**
|
||||
* Handle a room update. The Algorithm will only call this for causes which
|
||||
|
@ -69,7 +66,7 @@ export abstract class OrderingAlgorithm {
|
|||
* @param cause The cause of the update.
|
||||
* @returns True if the update requires the Algorithm to update the presentation layers.
|
||||
*/
|
||||
public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean>;
|
||||
public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean;
|
||||
|
||||
protected getRoomIndex(room: Room): number {
|
||||
let roomIdx = this.cachedOrderedRooms.indexOf(room);
|
||||
|
|
|
@ -23,7 +23,7 @@ import { compare } from "../../../../utils/strings";
|
|||
* Sorts rooms according to the browser's determination of alphabetic.
|
||||
*/
|
||||
export class AlphabeticAlgorithm implements IAlgorithm {
|
||||
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
|
||||
public sortRooms(rooms: Room[], tagId: TagID): Room[] {
|
||||
return rooms.sort((a, b) => {
|
||||
return compare(a.name, b.name);
|
||||
});
|
||||
|
|
|
@ -25,7 +25,7 @@ export interface IAlgorithm {
|
|||
* Sorts the given rooms according to the sorting rules of the algorithm.
|
||||
* @param {Room[]} rooms The rooms to sort.
|
||||
* @param {TagID} tagId The tag ID in which the rooms are being sorted.
|
||||
* @returns {Promise<Room[]>} Resolves to the sorted rooms.
|
||||
* @returns {Room[]} Returns the sorted rooms.
|
||||
*/
|
||||
sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]>;
|
||||
sortRooms(rooms: Room[], tagId: TagID): Room[];
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import { IAlgorithm } from "./IAlgorithm";
|
|||
* Sorts rooms according to the tag's `order` property on the room.
|
||||
*/
|
||||
export class ManualAlgorithm implements IAlgorithm {
|
||||
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
|
||||
public sortRooms(rooms: Room[], tagId: TagID): Room[] {
|
||||
const getOrderProp = (r: Room) => r.tags[tagId].order || 0;
|
||||
return rooms.sort((a, b) => {
|
||||
return getOrderProp(a) - getOrderProp(b);
|
||||
|
|
|
@ -97,7 +97,7 @@ export const sortRooms = (rooms: Room[]): Room[] => {
|
|||
* useful to the user.
|
||||
*/
|
||||
export class RecentAlgorithm implements IAlgorithm {
|
||||
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
|
||||
public sortRooms(rooms: Room[], tagId: TagID): Room[] {
|
||||
return sortRooms(rooms);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,8 +46,8 @@ export function getSortingAlgorithmInstance(algorithm: SortAlgorithm): IAlgorith
|
|||
* @param {Room[]} rooms The rooms to sort.
|
||||
* @param {TagID} tagId The tag in which the sorting is occurring.
|
||||
* @param {SortAlgorithm} algorithm The algorithm to use for sorting.
|
||||
* @returns {Promise<Room[]>} Resolves to the sorted rooms.
|
||||
* @returns {Room[]} Returns the sorted rooms.
|
||||
*/
|
||||
export function sortRoomsWithAlgorithm(rooms: Room[], tagId: TagID, algorithm: SortAlgorithm): Promise<Room[]> {
|
||||
export function sortRoomsWithAlgorithm(rooms: Room[], tagId: TagID, algorithm: SortAlgorithm): Room[] {
|
||||
return getSortingAlgorithmInstance(algorithm).sortRooms(rooms, tagId);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
|||
import CallHandler from "../../../CallHandler";
|
||||
import { RoomListCustomisations } from "../../../customisations/RoomList";
|
||||
import VoipUserMapper from "../../../VoipUserMapper";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import SpaceStore from "../../SpaceStore";
|
||||
|
||||
export class VisibilityProvider {
|
||||
private static internalInstance: VisibilityProvider;
|
||||
|
@ -50,7 +50,7 @@ export class VisibilityProvider {
|
|||
}
|
||||
|
||||
// hide space rooms as they'll be shown in the SpacePanel
|
||||
if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) {
|
||||
if (SpaceStore.spacesEnabled && room.isSpaceRoom()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { _t, sanitizeForTranslation } from "../../../languageHandler";
|
||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import ReplyThread from "../../../components/views/elements/ReplyThread";
|
||||
import { getHtmlText } from "../../../HtmlUtils";
|
||||
|
@ -58,6 +58,8 @@ export class MessageEventPreview implements IPreview {
|
|||
body = getHtmlText(body);
|
||||
}
|
||||
|
||||
body = sanitizeForTranslation(body);
|
||||
|
||||
if (msgtype === 'm.emote') {
|
||||
return _t("* %(senderName)s %(emote)s", { senderName: getSenderName(event), emote: body });
|
||||
}
|
||||
|
|
|
@ -51,9 +51,10 @@ import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
|
|||
import { getCustomTheme } from "../../theme";
|
||||
import CountlyAnalytics from "../../CountlyAnalytics";
|
||||
import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
|
||||
import { MatrixEvent, IEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { ELEMENT_CLIENT_ID } from "../../identifiers";
|
||||
import { getUserLanguage } from "../../languageHandler";
|
||||
import { WidgetVariableCustomisations } from "../../customisations/WidgetVariables";
|
||||
|
||||
// TODO: Destroy all of this code
|
||||
|
||||
|
@ -191,7 +192,8 @@ export class StopGapWidget extends EventEmitter {
|
|||
}
|
||||
|
||||
private runUrlTemplate(opts = { asPopout: false }): string {
|
||||
const templated = this.mockWidget.getCompleteUrl({
|
||||
const fromCustomisation = WidgetVariableCustomisations?.provideVariables?.() ?? {};
|
||||
const defaults: ITemplateParams = {
|
||||
widgetRoomId: this.roomId,
|
||||
currentUserId: MatrixClientPeg.get().getUserId(),
|
||||
userDisplayName: OwnProfileStore.instance.displayName,
|
||||
|
@ -199,7 +201,8 @@ export class StopGapWidget extends EventEmitter {
|
|||
clientId: ELEMENT_CLIENT_ID,
|
||||
clientTheme: SettingsStore.getValue("theme"),
|
||||
clientLanguage: getUserLanguage(),
|
||||
}, opts?.asPopout);
|
||||
};
|
||||
const templated = this.mockWidget.getCompleteUrl(Object.assign(defaults, fromCustomisation), opts?.asPopout);
|
||||
|
||||
const parsed = new URL(templated);
|
||||
|
||||
|
@ -363,6 +366,9 @@ export class StopGapWidget extends EventEmitter {
|
|||
}
|
||||
|
||||
public async prepare(): Promise<void> {
|
||||
// Ensure the variables are ready for us to be rendered before continuing
|
||||
await (WidgetVariableCustomisations?.isReady?.() ?? Promise.resolve());
|
||||
|
||||
if (this.scalarToken) return;
|
||||
const existingMessaging = WidgetMessagingStore.instance.getMessaging(this.mockWidget);
|
||||
if (existingMessaging) this.messaging = existingMessaging;
|
||||
|
@ -415,7 +421,7 @@ export class StopGapWidget extends EventEmitter {
|
|||
private feedEvent(ev: MatrixEvent) {
|
||||
if (!this.messaging) return;
|
||||
|
||||
const raw = ev.event as IEvent;
|
||||
const raw = ev.getEffectiveEvent();
|
||||
this.messaging.feedEvent(raw).catch(e => {
|
||||
console.error("Error sending event to widget: ", e);
|
||||
});
|
||||
|
|
|
@ -159,12 +159,12 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
|||
if (results.length >= limit) break;
|
||||
|
||||
const ev = events[i];
|
||||
if (ev.getType() !== eventType) continue;
|
||||
if (ev.getType() !== eventType || ev.isState()) continue;
|
||||
if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()['msgtype']) continue;
|
||||
results.push(ev);
|
||||
}
|
||||
|
||||
return results.map(e => e.event);
|
||||
return results.map(e => e.getEffectiveEvent());
|
||||
}
|
||||
|
||||
public async readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise<object[]> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue