Delete groups (legacy communities system) (#8027)

* Remove deprecated feature_communities_v2_prototypes

* Update _components

* i18n

* delint

* Cut out a bit more dead code

* Carve into legacy components

* Carve into mostly the room list code

* Carve into instances of "groupId"

* Carve out more of what comes up with "groups"

* Carve out some settings

* ignore related groups state

* Remove instances of spacesEnabled

* Fix some obvious issues

* Remove now-unused css

* Fix variable naming for legacy components

* Update i18n

* Misc cleanup from manual review

* Update snapshot for changed flag

* Appease linters

* rethemedex

* Remove now-unused AddressPickerDialog

* Make ConfirmUserActionDialog's member a required prop

* Remove useless override from RightPanelStore

* Remove extraneous CSS

* Update i18n

* Demo: "Communities are now Spaces" landing page

* Restore linkify for group IDs

* Demo: Dialog on click for communities->spaces notice

* i18n for demos

* i18n post-merge

* Update copy

* Appease the linter

* Post-merge cleanup

* Re-add spaces_learn_more_url to the new SdkConfig place

* Round 1 of post-merge fixes

* i18n

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Travis Ralston 2022-03-22 17:07:37 -06:00 committed by GitHub
parent 03c80707c9
commit fce36ec826
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
171 changed files with 317 additions and 12160 deletions

View file

@ -1,192 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Room } from "matrix-js-sdk/src/models/room";
import * as utils from "matrix-js-sdk/src/utils";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { logger } from "matrix-js-sdk/src/logger";
import { Method } from "matrix-js-sdk/src/http-api";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import { ActionPayload } from "../dispatcher/payloads";
import { Action } from "../dispatcher/actions";
import { EffectiveMembership, getEffectiveMembership } from "../utils/membership";
import SettingsStore from "../settings/SettingsStore";
import { UPDATE_EVENT } from "./AsyncStore";
import FlairStore from "./FlairStore";
import GroupFilterOrderStore from "./GroupFilterOrderStore";
import GroupStore from "./GroupStore";
import dis from "../dispatcher/dispatcher";
import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
interface IState {
// nothing of value - we use account data
}
export interface IRoomProfile {
displayName: string;
avatarMxc: string;
}
export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
private static internalInstance = new CommunityPrototypeStore();
private constructor() {
super(defaultDispatcher, {});
}
public static get instance(): CommunityPrototypeStore {
return CommunityPrototypeStore.internalInstance;
}
public static getUpdateEventName(roomId: string): string {
return `${UPDATE_EVENT}:${roomId}`;
}
public getSelectedCommunityId(): string {
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
return GroupFilterOrderStore.getSelectedTags()[0];
}
return null; // no selection as far as this function is concerned
}
public getSelectedCommunityName(): string {
return CommunityPrototypeStore.instance.getCommunityName(this.getSelectedCommunityId());
}
public getSelectedCommunityGeneralChat(): Room {
const communityId = this.getSelectedCommunityId();
if (communityId) {
return this.getGeneralChat(communityId);
}
}
public getCommunityName(communityId: string): string {
const profile = FlairStore.getGroupProfileCachedFast(this.matrixClient, communityId);
return profile?.name || communityId;
}
public getCommunityProfile(communityId: string): { name?: string, avatarUrl?: string } {
return FlairStore.getGroupProfileCachedFast(this.matrixClient, communityId);
}
public getGeneralChat(communityId: string): Room {
const rooms = GroupStore.getGroupRooms(communityId)
.map(r => this.matrixClient.getRoom(r.roomId))
.filter(r => !!r);
let chat = rooms.find(r => {
const idState = r.currentState.getStateEvents("im.vector.general_chat", "");
if (!idState || idState.getContent()['groupId'] !== communityId) return false;
return true;
});
if (!chat) chat = rooms[0];
return chat; // can be null
}
public isAdminOf(communityId: string): boolean {
const members = GroupStore.getGroupMembers(communityId);
const myMember = members.find(m => m.userId === this.matrixClient.getUserId());
return myMember?.isPrivileged;
}
public canInviteTo(communityId: string): boolean {
const generalChat = this.getGeneralChat(communityId);
if (!generalChat) return this.isAdminOf(communityId);
const myMember = generalChat.getMember(this.matrixClient.getUserId());
if (!myMember) return this.isAdminOf(communityId);
const pl = generalChat.currentState.getStateEvents("m.room.power_levels", "");
if (!pl) return this.isAdminOf(communityId);
const plContent = pl.getContent();
const invitePl = isNullOrUndefined(plContent.invite) ? 50 : Number(plContent.invite);
return invitePl <= myMember.powerLevel;
}
protected async onAction(payload: ActionPayload): Promise<any> {
if (!this.matrixClient || !SettingsStore.getValue("feature_communities_v2_prototypes")) {
return;
}
if (payload.action === "MatrixActions.Room.myMembership") {
const room: Room = payload.room;
const membership = getEffectiveMembership(payload.membership);
const oldMembership = getEffectiveMembership(payload.oldMembership);
if (membership === oldMembership) return;
if (membership === EffectiveMembership.Invite) {
try {
const path = utils.encodeUri("/rooms/$roomId/group_info", { $roomId: room.roomId });
const profile = await this.matrixClient.http.authedRequest(
undefined, Method.Get, path,
undefined, undefined,
{ prefix: "/_matrix/client/unstable/im.vector.custom" });
// we use global account data because per-room account data on invites is unreliable
await this.matrixClient.setAccountData("im.vector.group_info." + room.roomId, profile);
} catch (e) {
logger.warn("Non-fatal error getting group information for invite:", e);
}
}
} else if (payload.action === "MatrixActions.accountData") {
if (payload.event_type.startsWith("im.vector.group_info.")) {
const roomId = payload.event_type.substring("im.vector.group_info.".length);
this.emit(CommunityPrototypeStore.getUpdateEventName(roomId), roomId);
}
} else if (payload.action === "select_tag") {
// Automatically select the general chat when switching communities
const chat = this.getGeneralChat(payload.tag);
if (chat) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: chat.roomId,
metricsTrigger: undefined, // Deprecated groups
});
}
}
}
public getInviteProfile(roomId: string): IRoomProfile {
if (!this.matrixClient) return { displayName: null, avatarMxc: null };
const room = this.matrixClient.getRoom(roomId);
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
const data = this.matrixClient.getAccountData("im.vector.group_info." + roomId);
if (data && data.getContent()) {
return {
displayName: data.getContent().name,
avatarMxc: data.getContent().avatar_url,
};
}
}
return {
displayName: room.name,
avatarMxc: room.getMxcAvatarUrl(),
};
}
protected async onReady(): Promise<any> {
for (const room of this.matrixClient.getRooms()) {
const myMember = room.currentState.getMembers().find(m => m.userId === this.matrixClient.getUserId());
if (!myMember) continue;
if (getEffectiveMembership(myMember.membership) === EffectiveMembership.Invite) {
// Fake an update for anything that might have started listening before the invite
// data was available (eg: RoomPreviewBar after a refresh)
this.emit(CommunityPrototypeStore.getUpdateEventName(room.roomId), room.roomId);
}
}
}
}

View file

@ -1,157 +0,0 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import EventEmitter from 'events';
import { throttle } from "lodash";
import dis from '../dispatcher/dispatcher';
import SettingsStore from "../settings/SettingsStore";
import RoomListStore, { LISTS_UPDATE_EVENT } from "./room-list/RoomListStore";
import { RoomNotificationStateStore } from "./notifications/RoomNotificationStateStore";
import { isCustomTag } from "./room-list/models";
import { objectHasDiff } from "../utils/objects";
function commonPrefix(a, b) {
const len = Math.min(a.length, b.length);
let prefix;
for (let i = 0; i < len; ++i) {
if (a.charAt(i) !== b.charAt(i)) {
prefix = a.substr(0, i);
break;
}
}
if (prefix === undefined) {
prefix = a.substr(0, len);
}
const spaceIdx = prefix.indexOf(' ');
if (spaceIdx !== -1) {
prefix = prefix.substr(0, spaceIdx + 1);
}
if (prefix.length >= 2) {
return prefix;
}
return "";
}
/**
* A class for storing application state for ordering tags in the GroupFilterPanel.
*/
class CustomRoomTagStore extends EventEmitter {
constructor() {
super();
// Initialise state
this._state = { tags: {} };
// as RoomListStore gets updated by every timeline event
// throttle this to only run every 500ms
this._getUpdatedTags = throttle(
this._getUpdatedTags, 500, {
leading: true,
trailing: true,
},
);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this._onListsUpdated);
dis.register(payload => this._onDispatch(payload));
}
getTags() {
return this._state.tags;
}
_setState(newState) {
this._state = Object.assign(this._state, newState);
this.emit("change");
}
addListener(callback) {
this.on("change", callback);
return {
remove: () => {
this.removeListener("change", callback);
},
};
}
getSortedTags() {
const tagNames = Object.keys(this._state.tags).sort();
const prefixes = tagNames.map((name, i) => {
const isFirst = i === 0;
const isLast = i === tagNames.length - 1;
const backwardsPrefix = !isFirst ? commonPrefix(name, tagNames[i - 1]) : "";
const forwardsPrefix = !isLast ? commonPrefix(name, tagNames[i + 1]) : "";
const longestPrefix = backwardsPrefix.length > forwardsPrefix.length ?
backwardsPrefix : forwardsPrefix;
return longestPrefix;
});
return tagNames.map((name, i) => {
const notifs = RoomNotificationStateStore.instance.getListState(name);
let badgeNotifState;
if (notifs.hasUnreadCount) {
badgeNotifState = notifs;
}
const avatarLetter = name.substr(prefixes[i].length, 1);
const selected = this._state.tags[name];
return { name, avatarLetter, badgeNotifState, selected };
});
}
_onListsUpdated = () => {
const newTags = this._getUpdatedTags();
if (!this._state.tags || objectHasDiff(this._state.tags, newTags)) {
this._setState({ tags: newTags });
}
};
_onDispatch(payload) {
switch (payload.action) {
case 'select_custom_room_tag': {
const oldTags = this._state.tags;
if (oldTags.hasOwnProperty(payload.tag)) {
const tag = {};
tag[payload.tag] = !oldTags[payload.tag];
const tags = Object.assign({}, oldTags, tag);
this._setState({ tags });
}
break;
}
case 'on_client_not_viable':
case 'on_logged_out': {
// we assume to always have a tags object in the state
this._state = { tags: {} };
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this._onListsUpdated);
break;
}
}
}
_getUpdatedTags() {
if (!SettingsStore.getValue("feature_custom_tags")) {
return {}; // none
}
const newTagNames = Object.keys(RoomListStore.instance.orderedLists).filter(t => isCustomTag(t)).sort();
const prevTags = this._state && this._state.tags;
return newTagNames.reduce((c, tagName) => {
c[tagName] = (prevTags && prevTags[tagName]) || false;
return c;
}, {});
}
}
if (global.singletonCustomRoomTagStore === undefined) {
global.singletonCustomRoomTagStore = new CustomRoomTagStore();
}
export default global.singletonCustomRoomTagStore;

View file

@ -1,234 +0,0 @@
/*
Copyright 2017 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 { logger } from "matrix-js-sdk/src/logger";
const BULK_REQUEST_DEBOUNCE_MS = 200;
// Does the server support groups? Assume yes until we receive M_UNRECOGNIZED.
// If true, flair can function and we should keep sending requests for groups and avatars.
let groupSupport = true;
const USER_GROUPS_CACHE_BUST_MS = 1800000; // 30 mins
const GROUP_PROFILES_CACHE_BUST_MS = 1800000; // 30 mins
/**
* Stores data used by <Flair/>
*/
class FlairStore extends EventEmitter {
constructor(matrixClient) {
super();
this._matrixClient = matrixClient;
this._userGroups = {
// $userId: ['+group1:domain', '+group2:domain', ...]
};
this._groupProfiles = {
// $groupId: {
// avatar_url: 'mxc://...'
// }
};
this._groupProfilesPromise = {
// $groupId: Promise
};
this._usersPending = {
// $userId: {
// prom: Promise
// resolve: () => {}
// reject: () => {}
// }
};
this._usersInFlight = {
// This has the same schema as _usersPending
};
this._debounceTimeoutID = null;
}
groupSupport() {
return groupSupport;
}
invalidatePublicisedGroups(userId) {
delete this._userGroups[userId];
}
cachedPublicisedGroups(userId) {
return this._userGroups[userId];
}
getPublicisedGroupsCached(matrixClient, userId) {
if (this._userGroups[userId]) {
return Promise.resolve(this._userGroups[userId]);
}
// Bulk lookup ongoing, return promise to resolve/reject
if (this._usersPending[userId]) {
return this._usersPending[userId].prom;
}
// User has been moved from pending to in-flight
if (this._usersInFlight[userId]) {
return this._usersInFlight[userId].prom;
}
this._usersPending[userId] = {};
this._usersPending[userId].prom = new Promise((resolve, reject) => {
this._usersPending[userId].resolve = resolve;
this._usersPending[userId].reject = reject;
}).then((groups) => {
this._userGroups[userId] = groups;
setTimeout(() => {
delete this._userGroups[userId];
}, USER_GROUPS_CACHE_BUST_MS);
return this._userGroups[userId];
}).catch((err) => {
// Indicate whether the homeserver supports groups
if (err.errcode === 'M_UNRECOGNIZED') {
logger.warn('Cannot display flair, server does not support groups');
groupSupport = false;
// Return silently to avoid spamming for non-supporting servers
return;
}
logger.error('Could not get groups for user', userId, err);
throw err;
}).finally(() => {
delete this._usersInFlight[userId];
});
// This debounce will allow consecutive requests for the public groups of users that
// are sent in intervals of < BULK_REQUEST_DEBOUNCE_MS to be batched and only requested
// when no more requests are received within the next BULK_REQUEST_DEBOUNCE_MS. The naive
// implementation would do a request that only requested the groups for `userId`, leading
// to a worst and best case of 1 user per request. This implementation's worst is still
// 1 user per request but only if the requests are > BULK_REQUEST_DEBOUNCE_MS apart and the
// best case is N users per request.
//
// This is to reduce the number of requests made whilst trading off latency when viewing
// a Flair component.
if (this._debounceTimeoutID) clearTimeout(this._debounceTimeoutID);
this._debounceTimeoutID = setTimeout(() => {
this._batchedGetPublicGroups(matrixClient);
}, BULK_REQUEST_DEBOUNCE_MS);
return this._usersPending[userId].prom;
}
async _batchedGetPublicGroups(matrixClient) {
// Move users pending to users in flight
this._usersInFlight = this._usersPending;
this._usersPending = {};
let resp = {
users: [],
};
try {
resp = await matrixClient.getPublicisedGroups(Object.keys(this._usersInFlight));
} catch (err) {
// Propagate the same error to all usersInFlight
Object.keys(this._usersInFlight).forEach((userId) => {
// The promise should always exist for userId, but do a null-check anyway
if (!this._usersInFlight[userId]) return;
this._usersInFlight[userId].reject(err);
});
return;
}
const updatedUserGroups = resp.users;
Object.keys(this._usersInFlight).forEach((userId) => {
// The promise should always exist for userId, but do a null-check anyway
if (!this._usersInFlight[userId]) return;
this._usersInFlight[userId].resolve(updatedUserGroups[userId] || []);
});
}
/**
* Gets the profile for the given group if known, otherwise returns null.
* This triggers `getGroupProfileCached` if needed, though the result of the
* call will not be returned by this function.
* @param {MatrixClient} matrixClient The matrix client to use to fetch the profile, if needed.
* @param {string} groupId The group ID to get the profile for.
* @returns {*} The profile if known, otherwise null.
*/
getGroupProfileCachedFast(matrixClient, groupId) {
if (!matrixClient || !groupId) return null;
if (this._groupProfiles[groupId]) {
return this._groupProfiles[groupId];
}
this.getGroupProfileCached(matrixClient, groupId);
return null;
}
async getGroupProfileCached(matrixClient, groupId) {
if (this._groupProfiles[groupId]) {
return this._groupProfiles[groupId];
}
// A request is ongoing, wait for it to complete and return the group profile.
if (this._groupProfilesPromise[groupId]) {
try {
await this._groupProfilesPromise[groupId];
} catch (e) {
// Don't log the error; this is done below
return null;
}
return this._groupProfiles[groupId];
}
// No request yet, start one
logger.log('FlairStore: Request group profile of ' + groupId);
this._groupProfilesPromise[groupId] = matrixClient.getGroupProfile(groupId);
let profile;
try {
profile = await this._groupProfilesPromise[groupId];
} catch (e) {
logger.log('FlairStore: Failed to get group profile for ' + groupId, e);
// Don't retry, but allow a retry when the profile is next requested
delete this._groupProfilesPromise[groupId];
return null;
}
this._groupProfiles[groupId] = {
groupId,
avatarUrl: profile.avatar_url,
name: profile.name,
shortDescription: profile.short_description,
};
delete this._groupProfilesPromise[groupId];
/// XXX: This is verging on recreating a third "Flux"-looking Store. We really
/// should replace FlairStore with a Flux store and some async actions.
logger.log('FlairStore: Emit updateGroupProfile for ' + groupId);
this.emit('updateGroupProfile');
setTimeout(() => {
this.refreshGroupProfile(matrixClient, groupId);
}, GROUP_PROFILES_CACHE_BUST_MS);
return this._groupProfiles[groupId];
}
refreshGroupProfile(matrixClient, groupId) {
// Invalidate the cache
delete this._groupProfiles[groupId];
// Fetch new profile data, and cache it
return this.getGroupProfileCached(matrixClient, groupId);
}
}
if (global.singletonFlairStore === undefined) {
global.singletonFlairStore = new FlairStore();
}
export default global.singletonFlairStore;

View file

@ -1,281 +0,0 @@
/*
Copyright 2017 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 { Store } from 'flux/utils';
import { EventType } from "matrix-js-sdk/src/@types/event";
import dis from '../dispatcher/dispatcher';
import { Action } from '../dispatcher/actions';
import GroupStore from './GroupStore';
import Analytics from '../Analytics';
import * as RoomNotifs from "../RoomNotifs";
import { MatrixClientPeg } from '../MatrixClientPeg';
import SettingsStore from "../settings/SettingsStore";
import { CreateEventField } from "../@types/groups";
const INITIAL_STATE = {
orderedTags: null,
orderedTagsAccountData: null,
hasSynced: false,
joinedGroupIds: null,
selectedTags: [],
// Last selected tag when shift was not being pressed
anchorTag: null,
};
/**
* A class for storing application state for ordering tags in the GroupFilterPanel.
*/
class GroupFilterOrderStore extends Store {
constructor() {
super(dis);
// Initialise state
this._state = Object.assign({}, INITIAL_STATE);
SettingsStore.monitorSetting("TagPanel.enableTagPanel", null);
}
_setState(newState) {
this._state = Object.assign(this._state, newState);
this.__emitChange();
}
__onDispatch(payload) { // eslint-disable-line @typescript-eslint/naming-convention
switch (payload.action) {
// Initialise state after initial sync
case Action.ViewRoom: {
const relatedGroupIds = GroupStore.getGroupIdsForRoomId(payload.room_id);
this._updateBadges(relatedGroupIds);
break;
}
case 'MatrixActions.sync': {
if (payload.state === 'SYNCING' || payload.state === 'PREPARED') {
this._updateBadges();
}
if (!(payload.prevState !== 'PREPARED' && payload.state === 'PREPARED')) {
break;
}
const tagOrderingEvent = payload.matrixClient.getAccountData('im.vector.web.tag_ordering');
const tagOrderingEventContent = tagOrderingEvent ? tagOrderingEvent.getContent() : {};
this._setState({
orderedTagsAccountData: tagOrderingEventContent.tags || null,
removedTagsAccountData: tagOrderingEventContent.removedTags || null,
hasSynced: true,
});
this._updateOrderedTags();
break;
}
// Get ordering from account data
case 'MatrixActions.accountData': {
if (payload.event_type !== 'im.vector.web.tag_ordering') break;
// Ignore remote echos caused by this store so as to avoid setting
// state back to old state.
if (payload.event_content._storeId === this.getStoreId()) break;
this._setState({
orderedTagsAccountData: payload.event_content ? payload.event_content.tags : null,
removedTagsAccountData: payload.event_content ? payload.event_content.removedTags : null,
});
this._updateOrderedTags();
break;
}
// Initialise the state such that if account data is unset, default to joined groups
case 'GroupActions.fetchJoinedGroups.success': {
this._setState({
joinedGroupIds: payload.result.groups.sort(), // Sort lexically
hasFetchedJoinedGroups: true,
});
this._updateOrderedTags();
break;
}
case 'TagOrderActions.moveTag.pending': {
// Optimistic update of a moved tag
this._setState({
orderedTags: payload.request.tags,
removedTagsAccountData: payload.request.removedTags,
});
break;
}
case 'TagOrderActions.removeTag.pending': {
// Optimistic update of a removed tag
this._setState({
removedTagsAccountData: payload.request.removedTags,
});
this._updateOrderedTags();
break;
}
case 'select_tag': {
const allowMultiple = !SettingsStore.getValue("feature_communities_v2_prototypes");
let newTags = [];
// Shift-click semantics
if (payload.shiftKey && allowMultiple) {
// Select range of tags
let start = this._state.orderedTags.indexOf(this._state.anchorTag);
let end = this._state.orderedTags.indexOf(payload.tag);
if (start === -1) {
start = end;
}
if (start > end) {
const temp = start;
start = end;
end = temp;
}
newTags = payload.ctrlOrCmdKey ? this._state.selectedTags : [];
newTags = [...new Set(
this._state.orderedTags.slice(start, end + 1).concat(newTags),
)];
} else {
if (payload.ctrlOrCmdKey && allowMultiple) {
// Toggle individual tag
if (this._state.selectedTags.includes(payload.tag)) {
newTags = this._state.selectedTags.filter((t) => t !== payload.tag);
} else {
newTags = [...this._state.selectedTags, payload.tag];
}
} else {
if (this._state.selectedTags.length === 1 && this._state.selectedTags.includes(payload.tag)) {
// Existing (only) selected tag is being normally clicked again, clear tags
newTags = [];
} else {
// Select individual tag
newTags = [payload.tag];
}
}
// Only set the anchor tag if the tag was previously unselected, otherwise
// the next range starts with an unselected tag.
if (!this._state.selectedTags.includes(payload.tag)) {
this._setState({
anchorTag: payload.tag,
});
}
}
this._setState({
selectedTags: newTags,
});
Analytics.trackEvent('FilterStore', 'select_tag');
}
break;
case 'deselect_tags':
if (payload.tag) {
// if a tag is passed, only deselect that tag
this._setState({
selectedTags: this._state.selectedTags.filter(tag => tag !== payload.tag),
});
} else {
this._setState({
selectedTags: [],
});
}
Analytics.trackEvent('FilterStore', 'deselect_tags');
break;
case 'on_client_not_viable':
case 'on_logged_out': {
// Reset state without pushing an update to the view, which generally assumes that
// the matrix client isn't `null` and so causing a re-render will cause NPEs.
this._state = Object.assign({}, INITIAL_STATE);
break;
}
case 'setting_updated':
if (payload.settingName === 'TagPanel.enableTagPanel' && !payload.newValue) {
this._setState({
selectedTags: [],
});
Analytics.trackEvent('FilterStore', 'disable_tags');
}
break;
}
}
_updateBadges(groupIds = this._state.joinedGroupIds) {
if (groupIds && groupIds.length) {
const client = MatrixClientPeg.get();
const changedBadges = {};
groupIds.forEach(groupId => {
const rooms =
GroupStore.getGroupRooms(groupId)
.map(r => client.getRoom(r.roomId)) // to Room objects
.filter(r => r !== null && r !== undefined); // filter out rooms we haven't joined from the group
const badge = rooms && RoomNotifs.aggregateNotificationCount(rooms);
changedBadges[groupId] = (badge && badge.count !== 0) ? badge : undefined;
});
const newBadges = Object.assign({}, this._state.badges, changedBadges);
this._setState({ badges: newBadges });
}
}
_updateOrderedTags() {
this._setState({
orderedTags:
this._state.hasSynced &&
this._state.hasFetchedJoinedGroups ?
this._mergeGroupsAndTags() : null,
});
}
_mergeGroupsAndTags() {
const groupIds = this._state.joinedGroupIds || [];
const tags = this._state.orderedTagsAccountData || [];
const removedTags = new Set(this._state.removedTagsAccountData || []);
const tagsToKeep = tags.filter(
(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) && !migratedCommunities.has(groupId),
);
return tagsToKeep.concat(groupIdsToAdd);
}
getGroupBadge(groupId) {
const badges = this._state.badges;
return badges && badges[groupId];
}
getOrderedTags() {
return this._state.orderedTags;
}
getRemovedTagsAccountData() {
return this._state.removedTagsAccountData;
}
getStoreId() {
// Generate a random ID to prevent this store from clobbering its
// state with redundant remote echos.
if (!this._id) this._id = Math.random().toString(16).slice(2, 10);
return this._id;
}
getSelectedTags() {
return this._state.selectedTags;
}
}
if (global.singletonGroupFilterOrderStore === undefined) {
global.singletonGroupFilterOrderStore = new GroupFilterOrderStore();
}
export default global.singletonGroupFilterOrderStore;

View file

@ -1,351 +0,0 @@
/*
Copyright 2017 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 { logger } from "matrix-js-sdk/src/logger";
import { groupMemberFromApiObject, groupRoomFromApiObject } from '../groups';
import FlairStore from './FlairStore';
import { MatrixClientPeg } from '../MatrixClientPeg';
import dis from '../dispatcher/dispatcher';
export function parseMembersResponse(response) {
return response.chunk.map((apiMember) => groupMemberFromApiObject(apiMember));
}
export function parseRoomsResponse(response) {
return response.chunk.map((apiRoom) => groupRoomFromApiObject(apiRoom));
}
// The number of ongoing group requests
let ongoingRequestCount = 0;
// This has arbitrarily been set to a small number to lower the priority
// of doing group-related requests because we care about other important
// requests like hitting /sync.
const LIMIT = 3; // Maximum number of ongoing group requests
// FIFO queue of functions to call in the backlog
const backlogQueue = [
// () => {...}
];
// Pull from the FIFO queue
function checkBacklog() {
const item = backlogQueue.shift();
if (typeof item === 'function') item();
}
// Limit the maximum number of ongoing promises returned by fn to LIMIT and
// use a FIFO queue to handle the backlog.
async function limitConcurrency(fn) {
if (ongoingRequestCount >= LIMIT) {
// Enqueue this request for later execution
await new Promise((resolve, reject) => {
backlogQueue.push(resolve);
});
}
ongoingRequestCount++;
try {
return await fn();
} catch (err) {
// We explicitly do not handle the error here, but let it propogate.
throw err;
} finally {
ongoingRequestCount--;
checkBacklog();
}
}
/**
* Global store for tracking group summary, members, invited members and rooms.
*/
class GroupStore extends EventEmitter {
STATE_KEY = {
GroupMembers: 'GroupMembers',
GroupInvitedMembers: 'GroupInvitedMembers',
Summary: 'Summary',
GroupRooms: 'GroupRooms',
};
constructor() {
super();
this._state = {};
this._state[this.STATE_KEY.Summary] = {};
this._state[this.STATE_KEY.GroupRooms] = {};
this._state[this.STATE_KEY.GroupMembers] = {};
this._state[this.STATE_KEY.GroupInvitedMembers] = {};
this._ready = {};
this._ready[this.STATE_KEY.Summary] = {};
this._ready[this.STATE_KEY.GroupRooms] = {};
this._ready[this.STATE_KEY.GroupMembers] = {};
this._ready[this.STATE_KEY.GroupInvitedMembers] = {};
this._fetchResourcePromise = {
[this.STATE_KEY.Summary]: {},
[this.STATE_KEY.GroupRooms]: {},
[this.STATE_KEY.GroupMembers]: {},
[this.STATE_KEY.GroupInvitedMembers]: {},
};
this._resourceFetcher = {
[this.STATE_KEY.Summary]: (groupId) => {
return limitConcurrency(
() => MatrixClientPeg.get().getGroupSummary(groupId),
);
},
[this.STATE_KEY.GroupRooms]: (groupId) => {
return limitConcurrency(
() => MatrixClientPeg.get().getGroupRooms(groupId).then(parseRoomsResponse),
);
},
[this.STATE_KEY.GroupMembers]: (groupId) => {
return limitConcurrency(
() => MatrixClientPeg.get().getGroupUsers(groupId).then(parseMembersResponse),
);
},
[this.STATE_KEY.GroupInvitedMembers]: (groupId) => {
return limitConcurrency(
() => MatrixClientPeg.get().getGroupInvitedUsers(groupId).then(parseMembersResponse),
);
},
};
}
_fetchResource(stateKey, groupId) {
// Ongoing request, ignore
if (this._fetchResourcePromise[stateKey][groupId]) return;
const clientPromise = this._resourceFetcher[stateKey](groupId);
// Indicate ongoing request
this._fetchResourcePromise[stateKey][groupId] = clientPromise;
clientPromise.then((result) => {
this._state[stateKey][groupId] = result;
this._ready[stateKey][groupId] = true;
this._notifyListeners();
}).catch((err) => {
// Invited users not visible to non-members
if (stateKey === this.STATE_KEY.GroupInvitedMembers && err.httpStatus === 403) {
return;
}
logger.error(`Failed to get resource ${stateKey} for ${groupId}`, err);
this.emit('error', err, groupId, stateKey);
}).finally(() => {
// Indicate finished request, allow for future fetches
delete this._fetchResourcePromise[stateKey][groupId];
});
return clientPromise;
}
_notifyListeners() {
this.emit('update');
}
/**
* Register a listener to recieve updates from the store. This also
* immediately triggers an update to send the current state of the
* store (which could be the initial state).
*
* If a group ID is specified, this also causes a fetch of all data
* of the specified group, which might cause 4 separate HTTP
* requests, but only if said requests aren't already ongoing.
*
* @param {string?} groupId the ID of the group to fetch data for.
* Optional.
* @param {function} fn the function to call when the store updates.
* @return {Object} tok a registration "token" with a single
* property `unregister`, a function that can
* be called to unregister the listener such
* that it won't be called any more.
*/
registerListener(groupId, fn) {
this.on('update', fn);
// Call to set initial state (before fetching starts)
this.emit('update');
if (groupId) {
this._fetchResource(this.STATE_KEY.Summary, groupId);
this._fetchResource(this.STATE_KEY.GroupRooms, groupId);
this._fetchResource(this.STATE_KEY.GroupMembers, groupId);
this._fetchResource(this.STATE_KEY.GroupInvitedMembers, groupId);
}
// Similar to the Store of flux/utils, we return a "token" that
// can be used to unregister the listener.
return {
unregister: () => {
this.unregisterListener(fn);
},
};
}
unregisterListener(fn) {
this.removeListener('update', fn);
}
isStateReady(groupId, id) {
return this._ready[id][groupId];
}
getGroupIdsForRoomId(roomId) {
const groupIds = Object.keys(this._state[this.STATE_KEY.GroupRooms]);
return groupIds.filter(groupId => {
const rooms = this._state[this.STATE_KEY.GroupRooms][groupId] || [];
return rooms.some(room => room.roomId === roomId);
});
}
getSummary(groupId) {
return this._state[this.STATE_KEY.Summary][groupId] || {};
}
getGroupRooms(groupId) {
return this._state[this.STATE_KEY.GroupRooms][groupId] || [];
}
getGroupMembers(groupId) {
return this._state[this.STATE_KEY.GroupMembers][groupId] || [];
}
getGroupInvitedMembers(groupId) {
return this._state[this.STATE_KEY.GroupInvitedMembers][groupId] || [];
}
getGroupPublicity(groupId) {
return (this._state[this.STATE_KEY.Summary][groupId] || {}).user ?
(this._state[this.STATE_KEY.Summary][groupId] || {}).user.is_publicised : null;
}
isUserPrivileged(groupId) {
return (this._state[this.STATE_KEY.Summary][groupId] || {}).user ?
(this._state[this.STATE_KEY.Summary][groupId] || {}).user.is_privileged : null;
}
refreshGroupRooms(groupId) {
return this._fetchResource(this.STATE_KEY.GroupRooms, groupId);
}
refreshGroupMembers(groupId) {
return this._fetchResource(this.STATE_KEY.GroupMembers, groupId);
}
addRoomToGroup(groupId, roomId, isPublic) {
return MatrixClientPeg.get()
.addRoomToGroup(groupId, roomId, isPublic)
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId));
}
updateGroupRoomVisibility(groupId, roomId, isPublic) {
return MatrixClientPeg.get()
.updateGroupRoomVisibility(groupId, roomId, isPublic)
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId));
}
removeRoomFromGroup(groupId, roomId) {
return MatrixClientPeg.get()
.removeRoomFromGroup(groupId, roomId)
// Room might be in the summary, refresh just in case
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId));
}
inviteUserToGroup(groupId, userId) {
return MatrixClientPeg.get().inviteUserToGroup(groupId, userId)
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupInvitedMembers, groupId));
}
acceptGroupInvite(groupId) {
return MatrixClientPeg.get().acceptGroupInvite(groupId)
// The user should now be able to access (personal) group settings
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
// The user might be able to see more rooms now
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId))
// The user should now appear as a member
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupMembers, groupId))
// The user should now not appear as an invited member
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupInvitedMembers, groupId));
}
joinGroup(groupId) {
return MatrixClientPeg.get().joinGroup(groupId)
// The user should now be able to access (personal) group settings
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
// The user might be able to see more rooms now
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId))
// The user should now appear as a member
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupMembers, groupId))
// The user should now not appear as an invited member
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupInvitedMembers, groupId));
}
leaveGroup(groupId) {
// ensure the tag panel filter is cleared if the group was selected
dis.dispatch({
action: "deselect_tags",
tag: groupId,
});
return MatrixClientPeg.get().leaveGroup(groupId)
// The user should now not be able to access group settings
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
// The user might only be able to see a subset of rooms now
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId))
// The user should now not appear as a member
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupMembers, groupId));
}
addRoomToGroupSummary(groupId, roomId, categoryId) {
return MatrixClientPeg.get()
.addRoomToGroupSummary(groupId, roomId, categoryId)
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
}
addUserToGroupSummary(groupId, userId, roleId) {
return MatrixClientPeg.get()
.addUserToGroupSummary(groupId, userId, roleId)
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
}
removeRoomFromGroupSummary(groupId, roomId) {
return MatrixClientPeg.get()
.removeRoomFromGroupSummary(groupId, roomId)
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
}
removeUserFromGroupSummary(groupId, userId) {
return MatrixClientPeg.get()
.removeUserFromGroupSummary(groupId, userId)
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
}
setGroupPublicity(groupId, isPublished) {
return MatrixClientPeg.get()
.setGroupPublicity(groupId, isPublished)
.then(() => { FlairStore.invalidatePublicisedGroups(MatrixClientPeg.get().credentials.userId); })
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
}
}
let singletonGroupStore = null;
if (!singletonGroupStore) {
singletonGroupStore = new GroupStore();
}
export default singletonGroupStore;

View file

@ -123,11 +123,8 @@ class RoomViewStore extends Store<ActionPayload> {
this.viewRoom(payload);
break;
// for these events blank out the roomId as we are no longer in the RoomView
case 'view_create_group':
case 'view_welcome_page':
case Action.ViewHomePage:
case 'view_my_groups':
case 'view_group':
this.setState({
roomId: null,
roomAlias: null,

View file

@ -22,7 +22,6 @@ import defaultDispatcher from '../../dispatcher/dispatcher';
import { pendingVerificationRequestForUser } from '../../verification';
import SettingsStore from "../../settings/SettingsStore";
import { RightPanelPhases } from "./RightPanelStorePhases";
import { ActionPayload } from "../../dispatcher/payloads";
import { SettingLevel } from "../../settings/SettingLevel";
import { UPDATE_EVENT } from '../AsyncStore';
import { ReadyWatchingStore } from '../ReadyWatchingStore';
@ -34,27 +33,15 @@ import {
} from './RightPanelStoreIPanelState';
import RoomViewStore from '../RoomViewStore';
const GROUP_PHASES = [
RightPanelPhases.GroupMemberList,
RightPanelPhases.GroupRoomList,
RightPanelPhases.GroupRoomInfo,
RightPanelPhases.GroupMemberInfo,
];
/**
* A class for tracking the state of the right panel between layouts and
* sessions. This state includes a history for each room. Each history element
* contains the phase (e.g. RightPanelPhase.RoomMemberInfo) and the state (e.g.
* the member) associated with it.
* Groups are treated the same as rooms (they are also stored in the byRoom
* object). This is possible since the store keeps track of the opened
* room/group -> the store will provide the correct history for that group/room.
*/
export default class RightPanelStore extends ReadyWatchingStore {
private static internalInstance: RightPanelStore;
private readonly dispatcherRefRightPanelStore: string;
private viewedRoomId: string;
private isReady = false;
private global?: IRightPanelForRoom = null;
private byRoom: {
@ -65,26 +52,17 @@ export default class RightPanelStore extends ReadyWatchingStore {
private constructor() {
super(defaultDispatcher);
this.dispatcherRefRightPanelStore = defaultDispatcher.register(this.onDispatch);
}
protected async onReady(): Promise<any> {
this.isReady = true;
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
this.matrixClient.on(CryptoEvent.VerificationRequest, this.onVerificationRequestUpdate);
this.viewedRoomId = RoomViewStore.getRoomId();
this.loadCacheFromSettings();
this.emitAndUpdateSettings();
}
public destroy() {
if (this.dispatcherRefRightPanelStore) {
defaultDispatcher.unregister(this.dispatcherRefRightPanelStore);
}
super.destroy();
}
protected async onNotReady(): Promise<any> {
this.isReady = false;
this.matrixClient.off(CryptoEvent.VerificationRequest, this.onVerificationRequestUpdate);
this.roomStoreToken.remove();
}
@ -138,12 +116,6 @@ export default class RightPanelStore extends ReadyWatchingStore {
return { state: {}, phase: null };
}
// The Group associated getters are just for backwards compatibility. Can be removed when deprecating groups.
public get isOpenForGroup(): boolean { return this.isOpen; }
public get groupPhaseHistory(): Array<IRightPanelCard> { return this.roomPhaseHistory; }
public get currentGroup(): IRightPanelCard { return this.currentCard; }
public get previousGroup(): IRightPanelCard { return this.previousCard; }
// Setters
public setCard(card: IRightPanelCard, allowClose = true, roomId?: string) {
const rId = roomId ?? this.viewedRoomId;
@ -251,8 +223,7 @@ export default class RightPanelStore extends ReadyWatchingStore {
this.byRoom[this.viewedRoomId] = this.byRoom[this.viewedRoomId] ??
convertToStatePanel(SettingsStore.getValue("RightPanel.phases", this.viewedRoomId), room);
} else {
console.warn("Could not restore the right panel after load because there was no associated room object. " +
"The right panel can only be restored for rooms and spaces but not for groups.");
console.warn("Could not restore the right panel after load because there was no associated room object.");
}
}
@ -296,7 +267,6 @@ export default class RightPanelStore extends ReadyWatchingStore {
case RightPanelPhases.RoomMemberInfo:
case RightPanelPhases.SpaceMemberInfo:
case RightPanelPhases.EncryptionPanel:
case RightPanelPhases.GroupMemberInfo:
if (!card.state.member) {
console.warn("removed card from right panel because of missing member in card state");
}
@ -307,11 +277,6 @@ export default class RightPanelStore extends ReadyWatchingStore {
console.warn("removed card from right panel because of missing memberInfoEvent in card state");
}
return !!card.state.memberInfoEvent;
case RightPanelPhases.GroupRoomInfo:
if (!card.state.groupRoomId) {
console.warn("removed card from right panel because of missing groupRoomId in card state");
}
return !!card.state.groupRoomId;
case RightPanelPhases.Widget:
if (!card.state.widgetId) {
console.warn("removed card from right panel because of missing widgetId in card state");
@ -350,13 +315,7 @@ export default class RightPanelStore extends ReadyWatchingStore {
logger.warn(`Tried to switch right panel to unknown phase: ${targetPhase}`);
return false;
}
if (GROUP_PHASES.includes(targetPhase) && isViewingRoom) {
logger.warn(
`Tried to switch right panel to a group phase: ${targetPhase}, ` +
`but we are currently not viewing a group`,
);
return false;
} else if (!GROUP_PHASES.includes(targetPhase) && !isViewingRoom) {
if (!isViewingRoom) {
logger.warn(
`Tried to switch right panel to a room phase: ${targetPhase}, ` +
`but we are currently not viewing a room`,
@ -378,7 +337,6 @@ export default class RightPanelStore extends ReadyWatchingStore {
};
private onRoomViewStoreUpdate = () => {
// TODO: only use this function instead of the onDispatch (the whole onDispatch can get removed!) as soon groups are removed
const oldRoomId = this.viewedRoomId;
this.viewedRoomId = RoomViewStore.getRoomId();
// load values from byRoomCache with the viewedRoomId.
@ -422,37 +380,6 @@ export default class RightPanelStore extends ReadyWatchingStore {
return !!this.viewedRoomId;
}
private onDispatch = (payload: ActionPayload) => {
switch (payload.action) {
case 'view_group': {
// Put group in the same/similar view to what was open from the previously viewed room
// Is contradictory to the new "per room" philosophy but it is the legacy behavior for groups.
if (
this.currentCard?.phase === RightPanelPhases.GroupMemberInfo
) {
// switch from room to group
this.setRightPanelCache({ phase: RightPanelPhases.GroupMemberList, state: {} });
}
// The right panel store always will return the state for the current room.
this.viewedRoomId = null; // a group is not a room
// load values from byRoomCache with the viewedRoomId.
if (this.isReady) {
// we need the client to be ready to get the events form the ids of the settings
// the loading will be done in the onReady function (to catch up with the changes done here before it was ready)
// all the logic in this case is not necessary anymore as soon as groups are dropped and we use: onRoomViewStoreUpdate
this.loadCacheFromSettings();
// DO NOT EMIT. Emitting breaks iframe refs by triggering a render
// for the room view and calling the iframe ref changed function
// this.emitAndUpdateSettings();
}
break;
}
}
};
public static get instance(): RightPanelStore {
if (!RightPanelStore.internalInstance) {
RightPanelStore.internalInstance = new RightPanelStore();

View file

@ -20,16 +20,12 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { GroupMember } from "../../components/views/right_panel/UserInfo";
import { RightPanelPhases } from "./RightPanelStorePhases";
export interface IRightPanelCardState {
member?: RoomMember | User | GroupMember;
member?: RoomMember | User;
verificationRequest?: VerificationRequest;
verificationRequestPromise?: Promise<VerificationRequest>;
// group
groupId?: string;
groupRoomId?: string;
widgetId?: string;
spaceId?: string;
// Room3pidMemberInfo, Space3pidMemberInfo,
@ -43,9 +39,6 @@ export interface IRightPanelCardState {
export interface IRightPanelCardStateStored {
memberId?: string;
// we do not store the things associated with verification
// group
groupId?: string;
groupRoomId?: string;
widgetId?: string;
spaceId?: string;
// 3pidMemberInfo
@ -91,8 +84,6 @@ export function convertToStatePanel(storeRoom: IRightPanelForRoomStored, room: R
export function convertCardToStore(panelState: IRightPanelCard): IRightPanelCardStored {
const state = panelState.state ?? {};
const stateStored: IRightPanelCardStateStored = {
groupId: state.groupId,
groupRoomId: state.groupRoomId,
widgetId: state.widgetId,
spaceId: state.spaceId,
isInitialEventHighlighted: state.isInitialEventHighlighted,
@ -112,8 +103,6 @@ export function convertCardToStore(panelState: IRightPanelCard): IRightPanelCard
function convertStoreToCard(panelStateStore: IRightPanelCardStored, room: Room): IRightPanelCard {
const stateStored = panelStateStore.state ?? {};
const state: IRightPanelCardState = {
groupId: stateStored.groupId,
groupRoomId: stateStored.groupRoomId,
widgetId: stateStored.widgetId,
spaceId: stateStored.spaceId,
isInitialEventHighlighted: stateStored.isInitialEventHighlighted,

View file

@ -30,11 +30,6 @@ export enum RightPanelPhases {
Timeline = "Timeline",
Room3pidMemberInfo = 'Room3pidMemberInfo',
// Group stuff
GroupMemberList = 'GroupMemberList',
GroupRoomList = 'GroupRoomList',
GroupRoomInfo = 'GroupRoomInfo',
GroupMemberInfo = 'GroupMemberInfo',
// Space stuff
SpaceMemberList = "SpaceMemberList",

View file

@ -21,13 +21,12 @@ import { logger } from "matrix-js-sdk/src/logger";
import { EventType } from "matrix-js-sdk/src/@types/event";
import SettingsStore from "../../settings/SettingsStore";
import { DefaultTagID, isCustomTag, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
import { ActionPayload } from "../../dispatcher/payloads";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./filters/IFilterCondition";
import { TagWatcher } from "./TagWatcher";
import RoomViewStore from "../RoomViewStore";
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm";
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
@ -38,13 +37,10 @@ import { NameFilterCondition } from "./filters/NameFilterCondition";
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
import { VisibilityProvider } from "./filters/VisibilityProvider";
import { SpaceWatcher } from "./SpaceWatcher";
import SpaceStore from "../spaces/SpaceStore";
import { Action } from "../../dispatcher/actions";
import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";
import { IRoomTimelineActionPayload } from "../../actions/MatrixActionCreators";
interface IState {
tagsEnabled?: boolean;
// state is tracked in underlying classes
}
/**
@ -71,22 +67,14 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
this.emit(LISTS_UPDATE_EVENT);
});
private readonly watchedSettings = [
'feature_custom_tags',
];
constructor() {
super(defaultDispatcher);
this.setMaxListeners(20); // CustomRoomTagStore + RoomList + LeftPanel + 8xRoomSubList + spares
this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares
}
private setupWatchers() {
// TODO: Maybe destroy these if this class supports destruction
if (SpaceStore.spacesEnabled) {
new SpaceWatcher(this);
} else {
new TagWatcher(this);
}
// TODO: Maybe destroy this if this class supports destruction
new SpaceWatcher(this);
}
public get unfilteredLists(): ITagMap {
@ -123,7 +111,6 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
this.readyStore.useUnitTestClient(forcedClient);
}
for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null);
RoomViewStore.addListener(() => this.handleRVSUpdate({}));
this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
this.algorithm.on(FILTER_CHANGED, this.onAlgorithmFilterUpdated);
@ -131,7 +118,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
// Update any settings here, as some may have happened before we were logically ready.
logger.log("Regenerating room lists: Startup");
await this.readAndCacheSettingsFromStore();
this.updateAlgorithmInstances();
this.regenerateAllLists({ trigger: false });
this.handleRVSUpdate({ trigger: false }); // fake an RVS update to adjust sticky room, if needed
@ -139,14 +126,6 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
this.updateFn.trigger();
}
private async readAndCacheSettingsFromStore() {
const tagsEnabled = SettingsStore.getValue("feature_custom_tags");
await this.updateState({
tagsEnabled,
});
this.updateAlgorithmInstances();
}
/**
* Handles suspected RoomViewStore changes.
* @param trigger Set to false to prevent a list update from being sent. Should only
@ -203,17 +182,6 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
const logicallyReady = this.matrixClient && this.initialListsGenerated;
if (!logicallyReady) return;
if (payload.action === Action.SettingUpdated) {
const settingUpdatedPayload = payload as SettingUpdatedPayload;
if (this.watchedSettings.includes(settingUpdatedPayload.settingName)) {
logger.log("Regenerating room lists: Settings changed");
await this.readAndCacheSettingsFromStore();
this.regenerateAllLists({ trigger: false }); // regenerate the lists now
this.updateFn.trigger();
}
}
if (!this.algorithm) {
// This shouldn't happen because `initialListsGenerated` implies we have an algorithm.
throw new Error("Room list store has no algorithm to process dispatcher update with");
@ -533,7 +501,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 && (!SpaceStore.spacesEnabled || !this.filterConditions.length)) {
if (this.prefilterConditions.length > 0 && !this.filterConditions.length) {
rooms = rooms.filter(r => {
for (const filter of this.prefilterConditions) {
if (!filter.isVisible(r)) {
@ -560,18 +528,9 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
const rooms = this.getPlausibleRooms();
const customTags = new Set<TagID>();
if (this.state.tagsEnabled) {
for (const room of rooms) {
if (!room.tags) continue;
const tags = Object.keys(room.tags).filter(t => isCustomTag(t));
tags.forEach(t => customTags.add(t));
}
}
const sorts: ITagSortingMap = {};
const orders: IListOrderingMap = {};
const allTags = [...OrderedDefaultTagIDs, ...Array.from(customTags)];
const allTags = [...OrderedDefaultTagIDs];
for (const tagId of allTags) {
sorts[tagId] = this.calculateTagSorting(tagId);
orders[tagId] = this.calculateListOrder(tagId);
@ -600,12 +559,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
promise = this.recalculatePrefiltering();
} else {
this.filterConditions.push(filter);
// Runtime filters with spaces disable prefiltering for the search all spaces feature
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();
}
// Runtime filters with spaces disable prefiltering for the search all spaces feature.
// 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();
if (this.algorithm) {
this.algorithm.addFilterCondition(filter);
}
@ -631,9 +588,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
this.algorithm.removeFilterCondition(filter);
}
// Runtime filters with spaces disable prefiltering for the search all spaces feature
if (SpaceStore.spacesEnabled) {
promise = this.recalculatePrefiltering();
}
promise = this.recalculatePrefiltering();
removed = true;
}

View file

@ -1,83 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from "matrix-js-sdk/src/logger";
import { RoomListStoreClass } from "./RoomListStore";
import GroupFilterOrderStore from "../GroupFilterOrderStore";
import { CommunityFilterCondition } from "./filters/CommunityFilterCondition";
import { arrayDiff, arrayHasDiff } from "../../utils/arrays";
/**
* Watches for changes in groups to manage filters on the provided RoomListStore
*/
export class TagWatcher {
private filters = new Map<string, CommunityFilterCondition>();
constructor(private store: RoomListStoreClass) {
GroupFilterOrderStore.addListener(this.onTagsUpdated);
}
private onTagsUpdated = () => {
const lastTags = Array.from(this.filters.keys());
const newTags = GroupFilterOrderStore.getSelectedTags();
if (arrayHasDiff(lastTags, newTags)) {
// Selected tags changed, do some filtering
if (!this.store.matrixClient) {
logger.warn("Tag update without an associated matrix client - ignoring");
return;
}
const newFilters = new Map<string, CommunityFilterCondition>();
const filterableTags = newTags.filter(t => t.startsWith("+"));
for (const tag of filterableTags) {
const group = this.store.matrixClient.getGroup(tag);
if (!group) {
logger.warn(`Group selected with no group object available: ${tag}`);
continue;
}
let filter = this.filters.get(tag);
if (!filter) {
filter = new CommunityFilterCondition(group);
}
newFilters.set(tag, filter);
}
// Update the room list store's filters
const diff = arrayDiff(lastTags, newTags);
for (const tag of diff.added) {
const filter = newFilters.get(tag);
if (!filter) continue;
this.store.addFilter(filter);
}
for (const tag of diff.removed) {
// TODO: Remove this check when custom tags are supported (as we shouldn't be losing filters)
const filter = this.filters.get(tag);
if (!filter) continue;
this.store.removeFilter(filter);
filter.destroy();
}
this.filters = newFilters;
}
};
}

View file

@ -35,7 +35,6 @@ import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } f
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
import { getListAlgorithmInstance } from "./list-ordering";
import { VisibilityProvider } from "../filters/VisibilityProvider";
import SpaceStore from "../../spaces/SpaceStore";
/**
* Fired when the Algorithm has determined a list has been updated.
@ -202,7 +201,7 @@ export class Algorithm extends EventEmitter {
}
private doUpdateStickyRoom(val: Room) {
if (SpaceStore.spacesEnabled && val?.isSpaceRoom() && val.getMyMembership() !== "invite") {
if (val?.isSpaceRoom() && val.getMyMembership() !== "invite") {
// no-op sticky rooms for spaces - they're effectively virtual rooms
val = null;
}

View file

@ -1,67 +0,0 @@
/*
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Room } from "matrix-js-sdk/src/models/room";
import { Group } from "matrix-js-sdk/src/models/group";
import { EventEmitter } from "events";
import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
import GroupStore from "../../GroupStore";
import { IDestroyable } from "../../../utils/IDestroyable";
import DMRoomMap from "../../../utils/DMRoomMap";
import { setHasDiff } from "../../../utils/sets";
/**
* A filter condition for the room list which reveals rooms which
* are a member of a given community.
*/
export class CommunityFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable {
private roomIds = new Set<string>();
private userIds = new Set<string>();
constructor(private community: Group) {
super();
GroupStore.on("update", this.onStoreUpdate);
// noinspection JSIgnoredPromiseFromCall
this.onStoreUpdate(); // trigger a false update to seed the store
}
public get kind(): FilterKind {
return FilterKind.Prefilter;
}
public isVisible(room: Room): boolean {
return this.roomIds.has(room.roomId) || this.userIds.has(DMRoomMap.shared().getUserIdForRoomId(room.roomId));
}
private onStoreUpdate = async (): Promise<any> => {
// We don't actually know if the room list changed for the community, so just check it again.
const beforeRoomIds = this.roomIds;
this.roomIds = new Set((await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId));
const beforeUserIds = this.userIds;
this.userIds = new Set((await GroupStore.getGroupMembers(this.community.groupId)).map(u => u.userId));
if (setHasDiff(beforeRoomIds, this.roomIds) || setHasDiff(beforeUserIds, this.userIds)) {
this.emit(FILTER_CHANGED);
}
};
public destroy(): void {
GroupStore.off("update", this.onStoreUpdate);
}
}

View file

@ -19,7 +19,6 @@ import { Room } from "matrix-js-sdk/src/models/room";
import CallHandler from "../../../CallHandler";
import { RoomListCustomisations } from "../../../customisations/RoomList";
import VoipUserMapper from "../../../VoipUserMapper";
import SpaceStore from "../../spaces/SpaceStore";
export class VisibilityProvider {
private static internalInstance: VisibilityProvider;
@ -51,7 +50,7 @@ export class VisibilityProvider {
}
// hide space rooms as they'll be shown in the SpacePanel
if (SpaceStore.spacesEnabled && room.isSpaceRoom()) {
if (room.isSpaceRoom()) {
return false;
}

View file

@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { isEnumValue } from "../../utils/enums";
export enum DefaultTagID {
Invite = "im.vector.fake.invite",
Untagged = "im.vector.fake.recent", // legacy: used to just be 'recent rooms' but now it's all untagged rooms
@ -40,10 +38,6 @@ export const OrderedDefaultTagIDs = [
export type TagID = string | DefaultTagID;
export function isCustomTag(tagId: TagID): boolean {
return !isEnumValue(DefaultTagID, tagId);
}
export enum RoomUpdateCause {
Timeline = "TIMELINE",
PossibleTagChange = "POSSIBLE_TAG_CHANGE",

View file

@ -73,9 +73,6 @@ const metaSpaceOrder: MetaSpace[] = [MetaSpace.Home, MetaSpace.Favourites, MetaS
const MAX_SUGGESTED_ROOMS = 20;
// This setting causes the page to reload and can be costly if read frequently, so read it here only
const spacesEnabled = !SettingsStore.getValue("showCommunitiesInsteadOfSpaces");
const getSpaceContextKey = (space: SpaceKey) => `mx_space_context_${space}`;
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
@ -1054,7 +1051,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}
protected async onNotReady() {
if (!SpaceStore.spacesEnabled) return;
if (this.matrixClient) {
this.matrixClient.removeListener(ClientEvent.Room, this.onRoom);
this.matrixClient.removeListener(RoomEvent.MyMembership, this.onRoom);
@ -1067,7 +1063,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}
protected async onReady() {
if (!spacesEnabled) return;
this.matrixClient.on(ClientEvent.Room, this.onRoom);
this.matrixClient.on(RoomEvent.MyMembership, this.onRoom);
this.matrixClient.on(RoomEvent.AccountData, this.onRoomAccountData);
@ -1115,7 +1110,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}
protected async onAction(payload: SpaceStoreActions) {
if (!spacesEnabled || !this.matrixClient) return;
if (!this.matrixClient) return;
switch (payload.action) {
case Action.ViewRoom: {
@ -1297,8 +1292,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}
export default class SpaceStore {
public static spacesEnabled = spacesEnabled;
private static internalInstance = new SpaceStoreClass();
public static get instance(): SpaceStoreClass {