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:
parent
03c80707c9
commit
fce36ec826
171 changed files with 317 additions and 12160 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue