Show suggested rooms from the selected space
This commit is contained in:
parent
ab4220b20d
commit
6a5efad142
5 changed files with 116 additions and 14 deletions
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Dispatcher } from "flux";
|
import { Dispatcher } from "flux";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import * as fbEmitter from "fbemitter";
|
||||||
|
|
||||||
import { _t, _td } from "../../../languageHandler";
|
import { _t, _td } from "../../../languageHandler";
|
||||||
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
|
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
|
||||||
|
@ -47,9 +48,11 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||||
import CallHandler from "../../../CallHandler";
|
import CallHandler from "../../../CallHandler";
|
||||||
import SpaceStore from "../../../stores/SpaceStore";
|
import SpaceStore, { SUGGESTED_ROOMS } from "../../../stores/SpaceStore";
|
||||||
import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space";
|
import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space";
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory";
|
||||||
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||||
|
@ -63,6 +66,8 @@ interface IProps {
|
||||||
interface IState {
|
interface IState {
|
||||||
sublists: ITagMap;
|
sublists: ITagMap;
|
||||||
isNameFiltering: boolean;
|
isNameFiltering: boolean;
|
||||||
|
currentRoomId?: string;
|
||||||
|
suggestedRooms: ISpaceSummaryRoom[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const TAG_ORDER: TagID[] = [
|
const TAG_ORDER: TagID[] = [
|
||||||
|
@ -75,6 +80,7 @@ const TAG_ORDER: TagID[] = [
|
||||||
|
|
||||||
DefaultTagID.LowPriority,
|
DefaultTagID.LowPriority,
|
||||||
DefaultTagID.ServerNotice,
|
DefaultTagID.ServerNotice,
|
||||||
|
DefaultTagID.Suggested,
|
||||||
DefaultTagID.Archived,
|
DefaultTagID.Archived,
|
||||||
];
|
];
|
||||||
const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority;
|
const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority;
|
||||||
|
@ -242,6 +248,12 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
|
||||||
isInvite: false,
|
isInvite: false,
|
||||||
defaultHidden: true,
|
defaultHidden: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[DefaultTagID.Suggested]: {
|
||||||
|
sectionLabel: _td("Suggested Rooms"),
|
||||||
|
isInvite: false,
|
||||||
|
defaultHidden: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function customTagAesthetics(tagId: TagID): ITagAesthetics {
|
function customTagAesthetics(tagId: TagID): ITagAesthetics {
|
||||||
|
@ -260,6 +272,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
private dispatcherRef;
|
private dispatcherRef;
|
||||||
private customTagStoreRef;
|
private customTagStoreRef;
|
||||||
private tagAesthetics: ITagAestheticsMap;
|
private tagAesthetics: ITagAestheticsMap;
|
||||||
|
private roomStoreToken: fbEmitter.EventSubscription;
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -267,6 +280,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
this.state = {
|
this.state = {
|
||||||
sublists: {},
|
sublists: {},
|
||||||
isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(),
|
isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(),
|
||||||
|
suggestedRooms: SpaceStore.instance.suggestedRooms,
|
||||||
};
|
};
|
||||||
|
|
||||||
// shallow-copy from the template as we need to make modifications to it
|
// shallow-copy from the template as we need to make modifications to it
|
||||||
|
@ -274,20 +288,30 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
this.updateDmAddRoomAction();
|
this.updateDmAddRoomAction();
|
||||||
|
|
||||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
|
SpaceStore.instance.on(SUGGESTED_ROOMS, this.updateSuggestedRooms);
|
||||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
|
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
|
||||||
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
|
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
|
||||||
this.updateLists(); // trigger the first update
|
this.updateLists(); // trigger the first update
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
SpaceStore.instance.off(SUGGESTED_ROOMS, this.updateSuggestedRooms);
|
||||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
|
||||||
defaultDispatcher.unregister(this.dispatcherRef);
|
defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
if (this.customTagStoreRef) this.customTagStoreRef.remove();
|
if (this.customTagStoreRef) this.customTagStoreRef.remove();
|
||||||
|
if (this.roomStoreToken) this.roomStoreToken.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onRoomViewStoreUpdate = () => {
|
||||||
|
this.setState({
|
||||||
|
currentRoomId: RoomViewStore.getRoomId(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private updateDmAddRoomAction() {
|
private updateDmAddRoomAction() {
|
||||||
const dmTagAesthetics = objectShallowClone(TAG_AESTHETICS[DefaultTagID.DM]);
|
const dmTagAesthetics = objectShallowClone(TAG_AESTHETICS[DefaultTagID.DM]);
|
||||||
if (CallHandler.sharedInstance().getSupportsPstnProtocol()) {
|
if (CallHandler.sharedInstance().getSupportsPstnProtocol()) {
|
||||||
|
@ -319,7 +343,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
private getRoomDelta = (roomId: string, delta: number, unread = false) => {
|
private getRoomDelta = (roomId: string, delta: number, unread = false) => {
|
||||||
const lists = RoomListStore.instance.orderedLists;
|
const lists = RoomListStore.instance.orderedLists;
|
||||||
const rooms: Room = [];
|
const rooms: Room[] = [];
|
||||||
TAG_ORDER.forEach(t => {
|
TAG_ORDER.forEach(t => {
|
||||||
let listRooms = lists[t];
|
let listRooms = lists[t];
|
||||||
|
|
||||||
|
@ -340,6 +364,10 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
return room;
|
return room;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private updateSuggestedRooms = (suggestedRooms: ISpaceSummaryRoom[]) => {
|
||||||
|
this.setState({ suggestedRooms });
|
||||||
|
};
|
||||||
|
|
||||||
private updateLists = () => {
|
private updateLists = () => {
|
||||||
const newLists = RoomListStore.instance.orderedLists;
|
const newLists = RoomListStore.instance.orderedLists;
|
||||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||||
|
@ -394,6 +422,39 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
|
dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private renderSuggestedRooms(): JSX.Element[] {
|
||||||
|
return this.state.suggestedRooms.map(room => {
|
||||||
|
const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room");
|
||||||
|
const avatar = (
|
||||||
|
<RoomAvatar
|
||||||
|
oobData={{
|
||||||
|
name,
|
||||||
|
avatarUrl: room.avatar_url,
|
||||||
|
}}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
resizeMethod="crop"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const viewRoom = () => {
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: "view_room",
|
||||||
|
room_id: room.room_id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<TemporaryTile
|
||||||
|
isMinimized={this.props.isMinimized}
|
||||||
|
isSelected={this.state.currentRoomId === room.room_id}
|
||||||
|
displayName={name}
|
||||||
|
avatar={avatar}
|
||||||
|
onClick={viewRoom}
|
||||||
|
key={`suggestedRoomTile_${room.room_id}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private renderCommunityInvites(): TemporaryTile[] {
|
private renderCommunityInvites(): TemporaryTile[] {
|
||||||
// TODO: Put community invites in a more sensible place (not in the room list)
|
// TODO: Put community invites in a more sensible place (not in the room list)
|
||||||
// See https://github.com/vector-im/element-web/issues/14456
|
// See https://github.com/vector-im/element-web/issues/14456
|
||||||
|
@ -447,7 +508,14 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
for (const orderedTagId of tagOrder) {
|
for (const orderedTagId of tagOrder) {
|
||||||
const orderedRooms = this.state.sublists[orderedTagId] || [];
|
const orderedRooms = this.state.sublists[orderedTagId] || [];
|
||||||
const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
|
|
||||||
|
let extraTiles = null;
|
||||||
|
if (orderedTagId === DefaultTagID.Invite) {
|
||||||
|
extraTiles = this.renderCommunityInvites();
|
||||||
|
} else if (orderedTagId === DefaultTagID.Suggested) {
|
||||||
|
extraTiles = this.renderSuggestedRooms();
|
||||||
|
}
|
||||||
|
|
||||||
const totalTiles = orderedRooms.length + (extraTiles ? extraTiles.length : 0);
|
const totalTiles = orderedRooms.length + (extraTiles ? extraTiles.length : 0);
|
||||||
if (totalTiles === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
|
if (totalTiles === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
|
||||||
continue; // skip tag - not needed
|
continue; // skip tag - not needed
|
||||||
|
|
|
@ -28,7 +28,7 @@ interface IProps {
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
avatar: React.ReactElement;
|
avatar: React.ReactElement;
|
||||||
notificationState: NotificationState;
|
notificationState?: NotificationState;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,12 +63,15 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
|
||||||
'mx_RoomTile_minimized': this.props.isMinimized,
|
'mx_RoomTile_minimized': this.props.isMinimized,
|
||||||
});
|
});
|
||||||
|
|
||||||
const badge = (
|
let badge;
|
||||||
<NotificationBadge
|
if (this.props.notificationState) {
|
||||||
notification={this.props.notificationState}
|
badge = (
|
||||||
forceCount={false}
|
<NotificationBadge
|
||||||
/>
|
notification={this.props.notificationState}
|
||||||
);
|
forceCount={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let name = this.props.displayName;
|
let name = this.props.displayName;
|
||||||
if (typeof name !== 'string') name = '';
|
if (typeof name !== 'string') name = '';
|
||||||
|
@ -76,7 +79,7 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const nameClasses = classNames({
|
const nameClasses = classNames({
|
||||||
"mx_RoomTile_name": true,
|
"mx_RoomTile_name": true,
|
||||||
"mx_RoomTile_nameHasUnreadEvents": this.props.notificationState.isUnread,
|
"mx_RoomTile_nameHasUnreadEvents": this.props.notificationState?.isUnread,
|
||||||
});
|
});
|
||||||
|
|
||||||
let nameContainer = (
|
let nameContainer = (
|
||||||
|
|
|
@ -1529,7 +1529,9 @@
|
||||||
"Low priority": "Low priority",
|
"Low priority": "Low priority",
|
||||||
"System Alerts": "System Alerts",
|
"System Alerts": "System Alerts",
|
||||||
"Historical": "Historical",
|
"Historical": "Historical",
|
||||||
|
"Suggested Rooms": "Suggested Rooms",
|
||||||
"Custom Tag": "Custom Tag",
|
"Custom Tag": "Custom Tag",
|
||||||
|
"Empty room": "Empty room",
|
||||||
"Can't see what you’re looking for?": "Can't see what you’re looking for?",
|
"Can't see what you’re looking for?": "Can't see what you’re looking for?",
|
||||||
"Start a new chat": "Start a new chat",
|
"Start a new chat": "Start a new chat",
|
||||||
"Explore all public rooms": "Explore all public rooms",
|
"Explore all public rooms": "Explore all public rooms",
|
||||||
|
|
|
@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {throttle, sortBy} from "lodash";
|
import {sortBy, throttle} from "lodash";
|
||||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ import {EnhancedMap, mapDiff} from "../utils/maps";
|
||||||
import {setHasDiff} from "../utils/sets";
|
import {setHasDiff} from "../utils/sets";
|
||||||
import {objectDiff} from "../utils/objects";
|
import {objectDiff} from "../utils/objects";
|
||||||
import {arrayHasDiff} from "../utils/arrays";
|
import {arrayHasDiff} from "../utils/arrays";
|
||||||
|
import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory";
|
||||||
|
|
||||||
type SpaceKey = string | symbol;
|
type SpaceKey = string | symbol;
|
||||||
|
|
||||||
|
@ -41,11 +42,14 @@ interface IState {}
|
||||||
const ACTIVE_SPACE_LS_KEY = "mx_active_space";
|
const ACTIVE_SPACE_LS_KEY = "mx_active_space";
|
||||||
|
|
||||||
export const HOME_SPACE = Symbol("home-space");
|
export const HOME_SPACE = Symbol("home-space");
|
||||||
|
export const SUGGESTED_ROOMS = Symbol("suggested-rooms");
|
||||||
|
|
||||||
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
|
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
|
||||||
export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
|
export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
|
||||||
// Space Room ID/HOME_SPACE will be emitted when a Space's children change
|
// Space Room ID/HOME_SPACE will be emitted when a Space's children change
|
||||||
|
|
||||||
|
const MAX_SUGGESTED_ROOMS = 20;
|
||||||
|
|
||||||
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
|
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
|
||||||
return arr.reduce((result, room: Room) => {
|
return arr.reduce((result, room: Room) => {
|
||||||
result[room.isSpaceRoom() ? 0 : 1].push(room);
|
result[room.isSpaceRoom() ? 0 : 1].push(room);
|
||||||
|
@ -85,6 +89,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
private spaceFilteredRooms = new Map<string | symbol, Set<string>>();
|
private spaceFilteredRooms = new Map<string | symbol, Set<string>>();
|
||||||
// The space currently selected in the Space Panel - if null then `Home` is selected
|
// The space currently selected in the Space Panel - if null then `Home` is selected
|
||||||
private _activeSpace?: Room = null;
|
private _activeSpace?: Room = null;
|
||||||
|
private _suggestedRooms: ISpaceSummaryRoom[] = [];
|
||||||
|
|
||||||
public get spacePanelSpaces(): Room[] {
|
public get spacePanelSpaces(): Room[] {
|
||||||
return this.rootSpaces;
|
return this.rootSpaces;
|
||||||
|
@ -94,11 +99,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
return this._activeSpace || null;
|
return this._activeSpace || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setActiveSpace(space: Room | null) {
|
public get suggestedRooms(): ISpaceSummaryRoom[] {
|
||||||
|
return this._suggestedRooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setActiveSpace(space: Room | null) {
|
||||||
if (space === this.activeSpace) return;
|
if (space === this.activeSpace) return;
|
||||||
|
|
||||||
this._activeSpace = space;
|
this._activeSpace = space;
|
||||||
this.emit(UPDATE_SELECTED_SPACE, this.activeSpace);
|
this.emit(UPDATE_SELECTED_SPACE, this.activeSpace);
|
||||||
|
this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []);
|
||||||
|
|
||||||
// persist space selected
|
// persist space selected
|
||||||
if (space) {
|
if (space) {
|
||||||
|
@ -106,6 +116,23 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
} else {
|
} else {
|
||||||
window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY);
|
window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (space) {
|
||||||
|
try {
|
||||||
|
const data: {
|
||||||
|
rooms: ISpaceSummaryRoom[];
|
||||||
|
events: ISpaceSummaryEvent[];
|
||||||
|
} = await this.matrixClient.getSpaceSummary(space.roomId, 0, true, false, MAX_SUGGESTED_ROOMS);
|
||||||
|
if (this._activeSpace === space) {
|
||||||
|
this._suggestedRooms = data.rooms.filter(roomInfo => {
|
||||||
|
return roomInfo.room_type !== RoomType.Space && !this.matrixClient.getRoom(roomInfo.room_id);
|
||||||
|
});
|
||||||
|
this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public addRoomToSpace(space: Room, roomId: string, via: string[], suggested = false, autoJoin = false) {
|
public addRoomToSpace(space: Room, roomId: string, via: string[], suggested = false, autoJoin = false) {
|
||||||
|
|
|
@ -24,6 +24,7 @@ export enum DefaultTagID {
|
||||||
Favourite = "m.favourite",
|
Favourite = "m.favourite",
|
||||||
DM = "im.vector.fake.direct",
|
DM = "im.vector.fake.direct",
|
||||||
ServerNotice = "m.server_notice",
|
ServerNotice = "m.server_notice",
|
||||||
|
Suggested = "im.vector.fake.suggested",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OrderedDefaultTagIDs = [
|
export const OrderedDefaultTagIDs = [
|
||||||
|
@ -33,6 +34,7 @@ export const OrderedDefaultTagIDs = [
|
||||||
DefaultTagID.Untagged,
|
DefaultTagID.Untagged,
|
||||||
DefaultTagID.LowPriority,
|
DefaultTagID.LowPriority,
|
||||||
DefaultTagID.ServerNotice,
|
DefaultTagID.ServerNotice,
|
||||||
|
DefaultTagID.Suggested,
|
||||||
DefaultTagID.Archived,
|
DefaultTagID.Archived,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue