Merge pull request #5736 from matrix-org/t3chguy/spaces4.5

Spaces suggested rooms support
This commit is contained in:
Michael Telatynski 2021-03-10 10:56:17 +00:00 committed by GitHub
commit 98338f1505
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 215 additions and 100 deletions

View file

@ -203,8 +203,9 @@ limitations under the License.
.mx_SpaceRoomDirectory_actions { .mx_SpaceRoomDirectory_actions {
width: 180px; width: 180px;
text-align: right; text-align: right;
height: min-content; margin-left: 28px;
margin: auto 0 auto 28px; display: inline-flex;
align-items: center;
.mx_AccessibleButton { .mx_AccessibleButton {
vertical-align: middle; vertical-align: middle;
@ -223,9 +224,5 @@ limitations under the License.
line-height: $font-15px; line-height: $font-15px;
color: $secondary-fg-color; color: $secondary-fg-color;
} }
.mx_Checkbox {
display: inline-block;
}
} }
} }

View file

@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// XXX: We shouldn't be using TemporaryTile anywhere - delete it. .mx_DecoratedRoomAvatar, .mx_ExtraTile {
.mx_DecoratedRoomAvatar, .mx_TemporaryTile {
position: relative; position: relative;
&.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar { &.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar {

View file

@ -22,7 +22,7 @@ import MultiInviter from './utils/MultiInviter';
import Modal from './Modal'; import Modal from './Modal';
import * as sdk from './'; import * as sdk from './';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import InviteDialog, {KIND_DM, KIND_INVITE, KIND_SPACE_INVITE} from "./components/views/dialogs/InviteDialog"; import InviteDialog, {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore"; import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
@ -50,11 +50,10 @@ export function showStartChatInviteDialog(initialText) {
} }
export function showRoomInviteDialog(roomId) { export function showRoomInviteDialog(roomId) {
const isSpace = MatrixClientPeg.get()?.getRoom(roomId)?.isSpaceRoom();
// This dialog handles the room creation internally - we don't need to worry about it. // This dialog handles the room creation internally - we don't need to worry about it.
Modal.createTrackedDialog( Modal.createTrackedDialog(
"Invite Users", isSpace ? "Space" : "Room", InviteDialog, { "Invite Users", "", InviteDialog, {
kind: isSpace ? KIND_SPACE_INVITE : KIND_INVITE, kind: KIND_INVITE,
roomId, roomId,
}, },
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true,

View file

@ -79,6 +79,8 @@ export default class VoipUserMapper {
} }
public async onNewInvitedRoom(invitedRoom: Room) { public async onNewInvitedRoom(invitedRoom: Room) {
if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return;
const inviterId = invitedRoom.getDMInviter(); const inviterId = invitedRoom.getDMInviter();
console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId); const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);

View file

@ -64,6 +64,7 @@ export interface ISpaceSummaryEvent {
state_key: string; state_key: string;
content: { content: {
order?: string; order?: string;
suggested?: boolean;
auto_join?: boolean; auto_join?: boolean;
via?: string; via?: string;
}; };
@ -91,7 +92,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
const name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space"); const name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space");
const evContent = event?.getContent(); const evContent = event?.getContent();
const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join); const [suggested, _setSuggested] = useState(evContent?.suggested);
const [removed, _setRemoved] = useState(!evContent?.via); const [removed, _setRemoved] = useState(!evContent?.via);
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -102,12 +103,12 @@ const SubSpace: React.FC<ISubspaceProps> = ({
let actions; let actions;
if (editing && queueAction) { if (editing && queueAction) {
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) { if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
const setAutoJoin = () => { const setSuggested = () => {
_setAutoJoin(v => { _setSuggested(v => {
queueAction({ queueAction({
event, event,
removed, removed,
autoJoin: !v, suggested: !v,
}); });
return !v; return !v;
}); });
@ -118,7 +119,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
queueAction({ queueAction({
event, event,
removed: !v, removed: !v,
autoJoin, suggested,
}); });
return !v; return !v;
}); });
@ -131,7 +132,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
} else { } else {
actions = <React.Fragment> actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} /> <FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
<StyledCheckbox checked={autoJoin} onChange={setAutoJoin} /> <StyledCheckbox checked={suggested} onChange={setSuggested} />
</React.Fragment>; </React.Fragment>;
} }
} else { } else {
@ -182,8 +183,8 @@ const SubSpace: React.FC<ISubspaceProps> = ({
interface IAction { interface IAction {
event: MatrixEvent; event: MatrixEvent;
suggested: boolean;
removed: boolean; removed: boolean;
autoJoin: boolean;
} }
interface IRoomTileProps { interface IRoomTileProps {
@ -199,7 +200,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Unnamed Room"); const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Unnamed Room");
const evContent = event?.getContent(); const evContent = event?.getContent();
const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join); const [suggested, _setSuggested] = useState(evContent?.suggested);
const [removed, _setRemoved] = useState(!evContent?.via); const [removed, _setRemoved] = useState(!evContent?.via);
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -209,12 +210,12 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
let actions; let actions;
if (editing && queueAction) { if (editing && queueAction) {
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) { if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
const setAutoJoin = () => { const setSuggested = () => {
_setAutoJoin(v => { _setSuggested(v => {
queueAction({ queueAction({
event, event,
removed, removed,
autoJoin: !v, suggested: !v,
}); });
return !v; return !v;
}); });
@ -225,7 +226,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
queueAction({ queueAction({
event, event,
removed: !v, removed: !v,
autoJoin, suggested,
}); });
return !v; return !v;
}); });
@ -238,7 +239,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
} else { } else {
actions = <React.Fragment> actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} /> <FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
<StyledCheckbox checked={autoJoin} onChange={setAutoJoin} /> <StyledCheckbox checked={suggested} onChange={setSuggested} />
</React.Fragment>; </React.Fragment>;
} }
} else { } else {
@ -445,10 +446,10 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
const onSaveButtonClicked = () => { const onSaveButtonClicked = () => {
// TODO setBusy // TODO setBusy
pendingActions.current.forEach(({event, autoJoin, removed}) => { pendingActions.current.forEach(({event, suggested, removed}) => {
const content = { const content = {
...event.getContent(), ...event.getContent(),
auto_join: autoJoin, suggested,
}; };
if (removed) { if (removed) {
@ -463,7 +464,7 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
if (isEditing) { if (isEditing) {
adminButton = <React.Fragment> adminButton = <React.Fragment>
<FormButton label={_t("Save changes")} onClick={onSaveButtonClicked} /> <FormButton label={_t("Save changes")} onClick={onSaveButtonClicked} />
<span>{ _t("All users join by default") }</span> <span>{ _t("Promoted to users") }</span>
</React.Fragment>; </React.Fragment>;
} else { } else {
adminButton = <FormButton label={_t("Manage rooms")} onClick={onManageButtonClicked} />; adminButton = <FormButton label={_t("Manage rooms")} onClick={onManageButtonClicked} />;

View file

@ -557,7 +557,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
case Phase.PublicCreateRooms: case Phase.PublicCreateRooms:
return <SpaceSetupFirstRooms return <SpaceSetupFirstRooms
space={this.props.space} space={this.props.space}
title={_t("What discussions do you want to have?")} title={_t("What are some things you want to discuss?")}
description={_t("We'll create rooms for each topic.")} description={_t("We'll create rooms for each topic.")}
onFinished={() => this.setState({ phase: Phase.PublicShare })} onFinished={() => this.setState({ phase: Phase.PublicShare })}
/>; />;

View file

@ -27,7 +27,7 @@ export default class InfoDialog extends React.Component {
className: PropTypes.string, className: PropTypes.string,
title: PropTypes.string, title: PropTypes.string,
description: PropTypes.node, description: PropTypes.node,
button: PropTypes.oneOfType(PropTypes.string, PropTypes.bool), button: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
onFinished: PropTypes.func, onFinished: PropTypes.func,
hasCloseButton: PropTypes.bool, hasCloseButton: PropTypes.bool,
onKeyDown: PropTypes.func, onKeyDown: PropTypes.func,

View file

@ -48,7 +48,6 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
export const KIND_DM = "dm"; export const KIND_DM = "dm";
export const KIND_INVITE = "invite"; export const KIND_INVITE = "invite";
export const KIND_SPACE_INVITE = "space_invite";
export const KIND_CALL_TRANSFER = "call_transfer"; export const KIND_CALL_TRANSFER = "call_transfer";
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
@ -310,7 +309,7 @@ interface IInviteDialogProps {
// not provided. // not provided.
kind: string, kind: string,
// The room ID this dialog is for. Only required for KIND_INVITE and KIND_SPACE_INVITE. // The room ID this dialog is for. Only required for KIND_INVITE.
roomId: string, roomId: string,
// The call to transfer. Only required for KIND_CALL_TRANSFER. // The call to transfer. Only required for KIND_CALL_TRANSFER.
@ -349,8 +348,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
constructor(props) { constructor(props) {
super(props); super(props);
if ((props.kind === KIND_INVITE || props.kind === KIND_SPACE_INVITE) && !props.roomId) { if ((props.kind === KIND_INVITE) && !props.roomId) {
throw new Error("When using KIND_INVITE or KIND_SPACE_INVITE a roomId is required for an InviteDialog"); throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
} else if (props.kind === KIND_CALL_TRANSFER && !props.call) { } else if (props.kind === KIND_CALL_TRANSFER && !props.call) {
throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog"); throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog");
} }
@ -1027,7 +1026,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
sectionSubname = _t("May include members not in %(communityName)s", {communityName}); sectionSubname = _t("May include members not in %(communityName)s", {communityName});
} }
if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) { if (this.props.kind === KIND_INVITE) {
sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions"); sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
} }
@ -1248,25 +1247,31 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
} }
buttonText = _t("Go"); buttonText = _t("Go");
goButtonFn = this._startDm; goButtonFn = this._startDm;
} else if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) { } else if (this.props.kind === KIND_INVITE) {
title = this.props.kind === KIND_INVITE ? _t("Invite to this room") : _t("Invite to this space"); const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
const isSpace = room?.isSpaceRoom();
title = isSpace
? _t("Invite to %(spaceName)s", {
spaceName: room.name || _t("Unnamed Space"),
})
: _t("Invite to this room");
let helpTextUntranslated; let helpTextUntranslated;
if (this.props.kind === KIND_INVITE) { if (isSpace) {
if (identityServersEnabled) { if (identityServersEnabled) {
helpTextUntranslated = _td("Invite someone using their name, email address, username " + helpTextUntranslated = _td("Invite someone using their name, email address, username " +
"(like <userId/>) or <a>share this room</a>."); "(like <userId/>) or <a>share this space</a>.");
} else { } else {
helpTextUntranslated = _td("Invite someone using their name, username " + helpTextUntranslated = _td("Invite someone using their name, username " +
"(like <userId/>) or <a>share this room</a>."); "(like <userId/>) or <a>share this space</a>.");
} }
} else { // KIND_SPACE_INVITE } else {
if (identityServersEnabled) { if (identityServersEnabled) {
helpTextUntranslated = _td("Invite someone using their name, email address, username " + helpTextUntranslated = _td("Invite someone using their name, email address, username " +
"(like <userId/>) or <a>share this space</a>."); "(like <userId/>) or <a>share this room</a>.");
} else { } else {
helpTextUntranslated = _td("Invite someone using their name, username " + helpTextUntranslated = _td("Invite someone using their name, username " +
"(like <userId/>) or <a>share this space</a>."); "(like <userId/>) or <a>share this room</a>.");
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -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;
} }
@ -36,8 +36,7 @@ interface IState {
hover: boolean; hover: boolean;
} }
// TODO: Remove with community invites in the room list: https://github.com/vector-im/element-web/issues/14456 export default class ExtraTile extends React.Component<IProps, IState> {
export default class TemporaryTile extends React.Component<IProps, IState> {
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
@ -57,18 +56,21 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
public render(): React.ReactElement { public render(): React.ReactElement {
// XXX: We copy classes because it's easier // XXX: We copy classes because it's easier
const classes = classNames({ const classes = classNames({
'mx_ExtraTile': true,
'mx_RoomTile': true, 'mx_RoomTile': true,
'mx_TemporaryTile': true,
'mx_RoomTile_selected': this.props.isSelected, 'mx_RoomTile_selected': this.props.isSelected,
'mx_RoomTile_minimized': this.props.isMinimized, 'mx_RoomTile_minimized': this.props.isMinimized,
}); });
const badge = ( let badge;
if (this.props.notificationState) {
badge = (
<NotificationBadge <NotificationBadge
notification={this.props.notificationState} notification={this.props.notificationState}
forceCount={false} forceCount={false}
/> />
); );
}
let name = this.props.displayName; let name = this.props.displayName;
if (typeof name !== 'string') name = ''; if (typeof name !== 'string') name = '';
@ -76,7 +78,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 = (

View file

@ -16,9 +16,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import * as React from "react"; import React, { ReactComponentElement } 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";
@ -33,7 +34,7 @@ import RoomSublist from "./RoomSublist";
import { ActionPayload } from "../../../dispatcher/payloads"; import { ActionPayload } from "../../../dispatcher/payloads";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import GroupAvatar from "../avatars/GroupAvatar"; import GroupAvatar from "../avatars/GroupAvatar";
import TemporaryTile from "./TemporaryTile"; import ExtraTile from "./ExtraTile";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import { NotificationColor } from "../../../stores/notifications/NotificationColor"; import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
@ -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,7 +422,44 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
dis.dispatch({ action: Action.ViewRoomDirectory, initialText }); dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
}; };
private renderCommunityInvites(): TemporaryTile[] { private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
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,
oobData: {
avatarUrl: room.avatar_url,
name,
},
});
};
return (
<ExtraTile
isMinimized={this.props.isMinimized}
isSelected={this.state.currentRoomId === room.room_id}
displayName={name}
avatar={avatar}
onClick={viewRoom}
key={`suggestedRoomTile_${room.room_id}`}
/>
);
});
}
private renderCommunityInvites(): ReactComponentElement<typeof ExtraTile>[] {
// 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
return MatrixClientPeg.get().getGroups().filter(g => { return MatrixClientPeg.get().getGroups().filter(g => {
@ -415,7 +480,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
}); });
}; };
return ( return (
<TemporaryTile <ExtraTile
isMinimized={this.props.isMinimized} isMinimized={this.props.isMinimized}
isSelected={false} isSelected={false}
displayName={g.name} displayName={g.name}
@ -447,7 +512,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
@ -470,7 +542,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
isMinimized={this.props.isMinimized} isMinimized={this.props.isMinimized}
onResize={this.props.onResize} onResize={this.props.onResize}
showSkeleton={showSkeleton} showSkeleton={showSkeleton}
extraBadTilesThatShouldntExist={extraTiles} extraTiles={extraTiles}
/>); />);
} }

View file

@ -17,7 +17,7 @@ limitations under the License.
*/ */
import * as React from "react"; import * as React from "react";
import {createRef} from "react"; import { createRef, ReactComponentElement } from "react";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import classNames from 'classnames'; import classNames from 'classnames';
import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
@ -48,7 +48,7 @@ import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNo
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore"; import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays"; import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
import { objectExcluding, objectHasDiff } from "../../../utils/objects"; import { objectExcluding, objectHasDiff } from "../../../utils/objects";
import TemporaryTile from "./TemporaryTile"; import ExtraTile from "./ExtraTile";
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState"; import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
import IconizedContextMenu from "../context_menus/IconizedContextMenu"; import IconizedContextMenu from "../context_menus/IconizedContextMenu";
@ -73,9 +73,7 @@ interface IProps {
onResize: () => void; onResize: () => void;
showSkeleton?: boolean; showSkeleton?: boolean;
// TODO: Don't use this. It's for community invites, and community invites shouldn't be here. extraTiles?: ReactComponentElement<typeof ExtraTile>[];
// You should feel bad if you use this.
extraBadTilesThatShouldntExist?: TemporaryTile[];
// TODO: Account for https://github.com/vector-im/element-web/issues/14179 // TODO: Account for https://github.com/vector-im/element-web/issues/14179
} }
@ -95,7 +93,7 @@ interface IState {
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
height: number; height: number;
rooms: Room[]; rooms: Room[];
filteredExtraTiles?: TemporaryTile[]; filteredExtraTiles?: ReactComponentElement<typeof ExtraTile>[];
} }
export default class RoomSublist extends React.Component<IProps, IState> { export default class RoomSublist extends React.Component<IProps, IState> {
@ -153,12 +151,12 @@ export default class RoomSublist extends React.Component<IProps, IState> {
return padding; return padding;
} }
private get extraTiles(): TemporaryTile[] | null { private get extraTiles(): ReactComponentElement<typeof ExtraTile>[] | null {
if (this.state.filteredExtraTiles) { if (this.state.filteredExtraTiles) {
return this.state.filteredExtraTiles; return this.state.filteredExtraTiles;
} }
if (this.props.extraBadTilesThatShouldntExist) { if (this.props.extraTiles) {
return this.props.extraBadTilesThatShouldntExist; return this.props.extraTiles;
} }
return null; return null;
} }
@ -177,7 +175,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
} }
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) { public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) {
const prevExtraTiles = prevState.filteredExtraTiles || prevProps.extraBadTilesThatShouldntExist; const prevExtraTiles = prevState.filteredExtraTiles || prevProps.extraTiles;
// as the rooms can come in one by one we need to reevaluate // as the rooms can come in one by one we need to reevaluate
// the amount of available rooms to cap the amount of requested visible rooms by the layout // the amount of available rooms to cap the amount of requested visible rooms by the layout
if (RoomSublist.calcNumTiles(prevState.rooms, prevExtraTiles) !== this.numTiles) { if (RoomSublist.calcNumTiles(prevState.rooms, prevExtraTiles) !== this.numTiles) {
@ -200,8 +198,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
// If we're supposed to handle extra tiles, take the performance hit and re-render all the // If we're supposed to handle extra tiles, take the performance hit and re-render all the
// time so we don't have to consider them as part of the visible room optimization. // time so we don't have to consider them as part of the visible room optimization.
const prevExtraTiles = this.props.extraBadTilesThatShouldntExist || []; const prevExtraTiles = this.props.extraTiles || [];
const nextExtraTiles = (nextState.filteredExtraTiles || nextProps.extraBadTilesThatShouldntExist) || []; const nextExtraTiles = (nextState.filteredExtraTiles || nextProps.extraTiles) || [];
if (prevExtraTiles.length > 0 || nextExtraTiles.length > 0) { if (prevExtraTiles.length > 0 || nextExtraTiles.length > 0) {
return true; return true;
} }
@ -249,10 +247,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
private onListsUpdated = () => { private onListsUpdated = () => {
const stateUpdates: IState & any = {}; // &any is to avoid a cast on the initializer const stateUpdates: IState & any = {}; // &any is to avoid a cast on the initializer
if (this.props.extraBadTilesThatShouldntExist) { if (this.props.extraTiles) {
const nameCondition = RoomListStore.instance.getFirstNameFilterCondition(); const nameCondition = RoomListStore.instance.getFirstNameFilterCondition();
if (nameCondition) { if (nameCondition) {
stateUpdates.filteredExtraTiles = this.props.extraBadTilesThatShouldntExist stateUpdates.filteredExtraTiles = this.props.extraTiles
.filter(t => nameCondition.matches(t.props.displayName || "")); .filter(t => nameCondition.matches(t.props.displayName || ""));
} else if (this.state.filteredExtraTiles) { } else if (this.state.filteredExtraTiles) {
stateUpdates.filteredExtraTiles = null; stateUpdates.filteredExtraTiles = null;

View file

@ -107,7 +107,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
if (visibility === null) { if (visibility === null) {
body = <React.Fragment> body = <React.Fragment>
<h2>{ _t("Create a space") }</h2> <h2>{ _t("Create a space") }</h2>
<p>{ _t("Organise rooms into spaces, for just you or anyone") }</p> <p>{ _t("Spaces are new ways to group rooms and people. " +
"To join an existing space youll need an invite") }</p>
<SpaceCreateMenuType <SpaceCreateMenuType
title={_t("Public")} title={_t("Public")}
@ -117,12 +118,12 @@ const SpaceCreateMenu = ({ onFinished }) => {
/> />
<SpaceCreateMenuType <SpaceCreateMenuType
title={_t("Private")} title={_t("Private")}
description={_t("Invite only space, best for yourself or teams")} description={_t("Invite only, best for yourself or teams")}
className="mx_SpaceCreateMenuType_private" className="mx_SpaceCreateMenuType_private"
onClick={() => setVisibility(Visibility.Private)} onClick={() => setVisibility(Visibility.Private)}
/> />
{/*<p>{ _t("Looking to join an existing space?") }</p>*/} <p>{ _t("You can change this later") }</p>
</React.Fragment>; </React.Fragment>;
} else { } else {
body = <React.Fragment> body = <React.Fragment>
@ -134,9 +135,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
<h2> <h2>
{ {
visibility === Visibility.Public visibility === Visibility.Public ? _t("Your public space") : _t("Your private space")
? _t("Personalise your public space")
: _t("Personalise your private space")
} }
</h2> </h2>
<p> <p>

View file

@ -988,14 +988,15 @@
"Name": "Name", "Name": "Name",
"Description": "Description", "Description": "Description",
"Create a space": "Create a space", "Create a space": "Create a space",
"Organise rooms into spaces, for just you or anyone": "Organise rooms into spaces, for just you or anyone", "Spaces are new ways to group rooms and people. To join an existing space youll need an invite": "Spaces are new ways to group rooms and people. To join an existing space youll need an invite",
"Public": "Public", "Public": "Public",
"Open space for anyone, best for communities": "Open space for anyone, best for communities", "Open space for anyone, best for communities": "Open space for anyone, best for communities",
"Private": "Private", "Private": "Private",
"Invite only space, best for yourself or teams": "Invite only space, best for yourself or teams", "Invite only, best for yourself or teams": "Invite only, best for yourself or teams",
"You can change this later": "You can change this later",
"Go back": "Go back", "Go back": "Go back",
"Personalise your public space": "Personalise your public space", "Your public space": "Your public space",
"Personalise your private space": "Personalise your private space", "Your private space": "Your private space",
"Give it a photo, name and description to help you identify it.": "Give it a photo, name and description to help you identify it.", "Give it a photo, name and description to help you identify it.": "Give it a photo, name and description to help you identify it.",
"You can change these at any point.": "You can change these at any point.", "You can change these at any point.": "You can change these at any point.",
"Creating...": "Creating...", "Creating...": "Creating...",
@ -1532,7 +1533,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 youre looking for?": "Can't see what youre looking for?", "Can't see what youre looking for?": "Can't see what youre 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",
@ -2194,10 +2197,12 @@
"Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).", "Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).",
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>", "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>",
"Go": "Go", "Go": "Go",
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.", "Invite to %(spaceName)s": "Invite to %(spaceName)s",
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.", "Unnamed Space": "Unnamed Space",
"Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.", "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",
"Invite someone using their name, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.", "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.",
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.",
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
"Transfer": "Transfer", "Transfer": "Transfer",
"a new master key signature": "a new master key signature", "a new master key signature": "a new master key signature",
"a new cross-signing key signature": "a new cross-signing key signature", "a new cross-signing key signature": "a new cross-signing key signature",
@ -2595,14 +2600,13 @@
"Drop file here to upload": "Drop file here to upload", "Drop file here to upload": "Drop file here to upload",
"You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
"You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
"Unnamed Space": "Unnamed Space",
"Undo": "Undo", "Undo": "Undo",
"Remove from Space": "Remove from Space", "Remove from Space": "Remove from Space",
"No permissions": "No permissions", "No permissions": "No permissions",
"You're in this space": "You're in this space", "You're in this space": "You're in this space",
"You're in this room": "You're in this room", "You're in this room": "You're in this room",
"Save changes": "Save changes", "Save changes": "Save changes",
"All users join by default": "All users join by default", "Promoted to users": "Promoted to users",
"Manage rooms": "Manage rooms", "Manage rooms": "Manage rooms",
"Find a room...": "Find a room...", "Find a room...": "Find a room...",
"Accept Invite": "Accept Invite", "Accept Invite": "Accept Invite",
@ -2634,7 +2638,7 @@
"Invite your teammates": "Invite your teammates", "Invite your teammates": "Invite your teammates",
"Invite by username": "Invite by username", "Invite by username": "Invite by username",
"Inviting...": "Inviting...", "Inviting...": "Inviting...",
"What discussions do you want to have?": "What discussions do you want to have?", "What are some things you want to discuss?": "What are some things you want to discuss?",
"We'll create rooms for each topic.": "We'll create rooms for each topic.", "We'll create rooms for each topic.": "We'll create rooms for each topic.",
"What projects are you working on?": "What projects are you working on?", "What projects are you working on?": "What projects are you working on?",
"We'll create rooms for each of them. You can add existing rooms after setup.": "We'll create rooms for each of them. You can add existing rooms after setup.", "We'll create rooms for each of them. You can add existing rooms after setup.": "We'll create rooms for each of them. You can add existing rooms after setup.",

View file

@ -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,11 +116,29 @@ 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[], autoJoin = false) { public addRoomToSpace(space: Room, roomId: string, via: string[], suggested = false, autoJoin = false) {
return this.matrixClient.sendStateEvent(space.roomId, EventType.SpaceChild, { return this.matrixClient.sendStateEvent(space.roomId, EventType.SpaceChild, {
via, via,
suggested,
auto_join: autoJoin, auto_join: autoJoin,
}, roomId); }, roomId);
} }
@ -327,6 +355,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
// this.onRoomUpdate(room); // this.onRoomUpdate(room);
this.onRoomsUpdate(); this.onRoomsUpdate();
} }
const numSuggestedRooms = this._suggestedRooms.length;
this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId);
if (numSuggestedRooms !== this._suggestedRooms.length) {
this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
}
}; };
private onRoomState = (ev: MatrixEvent) => { private onRoomState = (ev: MatrixEvent) => {
@ -408,6 +442,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
} }
protected async onAction(payload: ActionPayload) { protected async onAction(payload: ActionPayload) {
if (!SettingsStore.getValue("feature_spaces")) return;
switch (payload.action) { switch (payload.action) {
case "view_room": { case "view_room": {
const room = this.matrixClient?.getRoom(payload.room_id); const room = this.matrixClient?.getRoom(payload.room_id);

View file

@ -409,7 +409,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
} }
private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<any> { private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<any> {
if (cause === RoomUpdateCause.NewRoom) { if (cause === RoomUpdateCause.NewRoom && room.getMyMembership() === "invite") {
// Let the visibility provider know that there is a new invited room. It would be nice // Let the visibility provider know that there is a new invited room. It would be nice
// if this could just be an event that things listen for but the point of this is that // if this could just be an event that things listen for but the point of this is that
// we delay doing anything about this room until the VoipUserMapper had had a chance // we delay doing anything about this room until the VoipUserMapper had had a chance

View file

@ -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,
]; ];