diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss index 5cb91820cf..c96398594f 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -203,8 +203,9 @@ limitations under the License. .mx_SpaceRoomDirectory_actions { width: 180px; text-align: right; - height: min-content; - margin: auto 0 auto 28px; + margin-left: 28px; + display: inline-flex; + align-items: center; .mx_AccessibleButton { vertical-align: middle; @@ -223,9 +224,5 @@ limitations under the License. line-height: $font-15px; color: $secondary-fg-color; } - - .mx_Checkbox { - display: inline-block; - } } } diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.scss b/res/css/views/avatars/_DecoratedRoomAvatar.scss index e0afd9de66..2631cbfb40 100644 --- a/res/css/views/avatars/_DecoratedRoomAvatar.scss +++ b/res/css/views/avatars/_DecoratedRoomAvatar.scss @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// XXX: We shouldn't be using TemporaryTile anywhere - delete it. -.mx_DecoratedRoomAvatar, .mx_TemporaryTile { +.mx_DecoratedRoomAvatar, .mx_ExtraTile { position: relative; &.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar { diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 503411d2b3..9ae41b851a 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -22,7 +22,7 @@ import MultiInviter from './utils/MultiInviter'; import Modal from './Modal'; import * as sdk from './'; 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 {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore"; @@ -50,11 +50,10 @@ export function showStartChatInviteDialog(initialText) { } 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. Modal.createTrackedDialog( - "Invite Users", isSpace ? "Space" : "Room", InviteDialog, { - kind: isSpace ? KIND_SPACE_INVITE : KIND_INVITE, + "Invite Users", "", InviteDialog, { + kind: KIND_INVITE, roomId, }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index d919615349..4f5613b4a8 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -37,7 +37,7 @@ export default class VoipUserMapper { return results[0].userid; } - public async getOrCreateVirtualRoomForRoom(roomId: string):Promise { + public async getOrCreateVirtualRoomForRoom(roomId: string): Promise { const userId = DMRoomMap.shared().getUserIdForRoomId(roomId); if (!userId) return null; @@ -52,7 +52,7 @@ export default class VoipUserMapper { return virtualRoomId; } - public nativeRoomForVirtualRoom(roomId: string):string { + public nativeRoomForVirtualRoom(roomId: string): string { const virtualRoom = MatrixClientPeg.get().getRoom(roomId); if (!virtualRoom) return null; const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE); @@ -60,7 +60,7 @@ export default class VoipUserMapper { return virtualRoomEvent.getContent()['native_room'] || null; } - public isVirtualRoom(room: Room):boolean { + public isVirtualRoom(room: Room): boolean { if (this.nativeRoomForVirtualRoom(room.roomId)) return true; if (this.virtualRoomIdCache.has(room.roomId)) return true; @@ -79,6 +79,8 @@ export default class VoipUserMapper { } public async onNewInvitedRoom(invitedRoom: Room) { + if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return; + const inviterId = invitedRoom.getDMInviter(); console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId); diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 06df6a528e..72e52678b6 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -64,6 +64,7 @@ export interface ISpaceSummaryEvent { state_key: string; content: { order?: string; + suggested?: boolean; auto_join?: boolean; via?: string; }; @@ -91,7 +92,7 @@ const SubSpace: React.FC = ({ const name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space"); const evContent = event?.getContent(); - const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join); + const [suggested, _setSuggested] = useState(evContent?.suggested); const [removed, _setRemoved] = useState(!evContent?.via); const cli = MatrixClientPeg.get(); @@ -102,12 +103,12 @@ const SubSpace: React.FC = ({ let actions; if (editing && queueAction) { if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) { - const setAutoJoin = () => { - _setAutoJoin(v => { + const setSuggested = () => { + _setSuggested(v => { queueAction({ event, removed, - autoJoin: !v, + suggested: !v, }); return !v; }); @@ -118,7 +119,7 @@ const SubSpace: React.FC = ({ queueAction({ event, removed: !v, - autoJoin, + suggested, }); return !v; }); @@ -131,7 +132,7 @@ const SubSpace: React.FC = ({ } else { actions = - + ; } } else { @@ -182,8 +183,8 @@ const SubSpace: React.FC = ({ interface IAction { event: MatrixEvent; + suggested: boolean; removed: boolean; - autoJoin: boolean; } 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 evContent = event?.getContent(); - const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join); + const [suggested, _setSuggested] = useState(evContent?.suggested); const [removed, _setRemoved] = useState(!evContent?.via); const cli = MatrixClientPeg.get(); @@ -209,12 +210,12 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli let actions; if (editing && queueAction) { if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) { - const setAutoJoin = () => { - _setAutoJoin(v => { + const setSuggested = () => { + _setSuggested(v => { queueAction({ event, removed, - autoJoin: !v, + suggested: !v, }); return !v; }); @@ -225,7 +226,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli queueAction({ event, removed: !v, - autoJoin, + suggested, }); return !v; }); @@ -238,7 +239,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli } else { actions = - + ; } } else { @@ -445,10 +446,10 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis const onSaveButtonClicked = () => { // TODO setBusy - pendingActions.current.forEach(({event, autoJoin, removed}) => { + pendingActions.current.forEach(({event, suggested, removed}) => { const content = { ...event.getContent(), - auto_join: autoJoin, + suggested, }; if (removed) { @@ -463,7 +464,7 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis if (isEditing) { adminButton = - { _t("All users join by default") } + { _t("Promoted to users") } ; } else { adminButton = ; diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 5c91efc1c0..9bacdd975d 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -557,7 +557,7 @@ export default class SpaceRoomView extends React.PureComponent { case Phase.PublicCreateRooms: return this.setState({ phase: Phase.PublicShare })} />; diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.js index 6dc9fc01b0..8207d334d3 100644 --- a/src/components/views/dialogs/InfoDialog.js +++ b/src/components/views/dialogs/InfoDialog.js @@ -27,7 +27,7 @@ export default class InfoDialog extends React.Component { className: PropTypes.string, title: PropTypes.string, description: PropTypes.node, - button: PropTypes.oneOfType(PropTypes.string, PropTypes.bool), + button: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), onFinished: PropTypes.func, hasCloseButton: PropTypes.bool, onKeyDown: PropTypes.func, diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 9bc5b6476f..6d5cb8786e 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -48,7 +48,6 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; export const KIND_DM = "dm"; export const KIND_INVITE = "invite"; -export const KIND_SPACE_INVITE = "space_invite"; export const KIND_CALL_TRANSFER = "call_transfer"; const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first @@ -310,7 +309,7 @@ interface IInviteDialogProps { // not provided. 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, // The call to transfer. Only required for KIND_CALL_TRANSFER. @@ -349,8 +348,8 @@ export default class InviteDialog extends React.PureComponent) or share this room."); + "(like ) or share this space."); } else { helpTextUntranslated = _td("Invite someone using their name, username " + - "(like ) or share this room."); + "(like ) or share this space."); } - } else { // KIND_SPACE_INVITE + } else { if (identityServersEnabled) { helpTextUntranslated = _td("Invite someone using their name, email address, username " + - "(like ) or share this space."); + "(like ) or share this room."); } else { helpTextUntranslated = _td("Invite someone using their name, username " + - "(like ) or share this space."); + "(like ) or share this room."); } } diff --git a/src/components/views/rooms/TemporaryTile.tsx b/src/components/views/rooms/ExtraTile.tsx similarity index 86% rename from src/components/views/rooms/TemporaryTile.tsx rename to src/components/views/rooms/ExtraTile.tsx index eec3105880..20d12955d5 100644 --- a/src/components/views/rooms/TemporaryTile.tsx +++ b/src/components/views/rooms/ExtraTile.tsx @@ -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"); you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ interface IProps { isSelected: boolean; displayName: string; avatar: React.ReactElement; - notificationState: NotificationState; + notificationState?: NotificationState; onClick: () => void; } @@ -36,8 +36,7 @@ interface IState { hover: boolean; } -// TODO: Remove with community invites in the room list: https://github.com/vector-im/element-web/issues/14456 -export default class TemporaryTile extends React.Component { +export default class ExtraTile extends React.Component { constructor(props: IProps) { super(props); @@ -57,18 +56,21 @@ export default class TemporaryTile extends React.Component { public render(): React.ReactElement { // XXX: We copy classes because it's easier const classes = classNames({ + 'mx_ExtraTile': true, 'mx_RoomTile': true, - 'mx_TemporaryTile': true, 'mx_RoomTile_selected': this.props.isSelected, 'mx_RoomTile_minimized': this.props.isMinimized, }); - const badge = ( - - ); + let badge; + if (this.props.notificationState) { + badge = ( + + ); + } let name = this.props.displayName; if (typeof name !== 'string') name = ''; @@ -76,7 +78,7 @@ export default class TemporaryTile extends React.Component { const nameClasses = classNames({ "mx_RoomTile_name": true, - "mx_RoomTile_nameHasUnreadEvents": this.props.notificationState.isUnread, + "mx_RoomTile_nameHasUnreadEvents": this.props.notificationState?.isUnread, }); let nameContainer = ( diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index f7da6571da..1573945a17 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -16,9 +16,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as React from "react"; +import React, { ReactComponentElement } from "react"; import { Dispatcher } from "flux"; import { Room } from "matrix-js-sdk/src/models/room"; +import * as fbEmitter from "fbemitter"; import { _t, _td } from "../../../languageHandler"; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; @@ -33,7 +34,7 @@ import RoomSublist from "./RoomSublist"; import { ActionPayload } from "../../../dispatcher/payloads"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import GroupAvatar from "../avatars/GroupAvatar"; -import TemporaryTile from "./TemporaryTile"; +import ExtraTile from "./ExtraTile"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { NotificationColor } from "../../../stores/notifications/NotificationColor"; import { Action } from "../../../dispatcher/actions"; @@ -47,9 +48,11 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con import AccessibleButton from "../elements/AccessibleButton"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import CallHandler from "../../../CallHandler"; -import SpaceStore from "../../../stores/SpaceStore"; +import SpaceStore, { SUGGESTED_ROOMS } from "../../../stores/SpaceStore"; import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space"; import { EventType } from "matrix-js-sdk/src/@types/event"; +import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory"; +import RoomAvatar from "../avatars/RoomAvatar"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; @@ -63,6 +66,8 @@ interface IProps { interface IState { sublists: ITagMap; isNameFiltering: boolean; + currentRoomId?: string; + suggestedRooms: ISpaceSummaryRoom[]; } const TAG_ORDER: TagID[] = [ @@ -75,6 +80,7 @@ const TAG_ORDER: TagID[] = [ DefaultTagID.LowPriority, DefaultTagID.ServerNotice, + DefaultTagID.Suggested, DefaultTagID.Archived, ]; const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority; @@ -242,6 +248,12 @@ const TAG_AESTHETICS: ITagAestheticsMap = { isInvite: false, defaultHidden: true, }, + + [DefaultTagID.Suggested]: { + sectionLabel: _td("Suggested Rooms"), + isInvite: false, + defaultHidden: false, + }, }; function customTagAesthetics(tagId: TagID): ITagAesthetics { @@ -260,6 +272,7 @@ export default class RoomList extends React.PureComponent { private dispatcherRef; private customTagStoreRef; private tagAesthetics: ITagAestheticsMap; + private roomStoreToken: fbEmitter.EventSubscription; constructor(props: IProps) { super(props); @@ -267,6 +280,7 @@ export default class RoomList extends React.PureComponent { this.state = { sublists: {}, isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(), + suggestedRooms: SpaceStore.instance.suggestedRooms, }; // shallow-copy from the template as we need to make modifications to it @@ -274,20 +288,30 @@ export default class RoomList extends React.PureComponent { this.updateDmAddRoomAction(); this.dispatcherRef = defaultDispatcher.register(this.onAction); + this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); } public componentDidMount(): void { + SpaceStore.instance.on(SUGGESTED_ROOMS, this.updateSuggestedRooms); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists); this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists); this.updateLists(); // trigger the first update } public componentWillUnmount() { + SpaceStore.instance.off(SUGGESTED_ROOMS, this.updateSuggestedRooms); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists); defaultDispatcher.unregister(this.dispatcherRef); if (this.customTagStoreRef) this.customTagStoreRef.remove(); + if (this.roomStoreToken) this.roomStoreToken.remove(); } + private onRoomViewStoreUpdate = () => { + this.setState({ + currentRoomId: RoomViewStore.getRoomId(), + }); + }; + private updateDmAddRoomAction() { const dmTagAesthetics = objectShallowClone(TAG_AESTHETICS[DefaultTagID.DM]); if (CallHandler.sharedInstance().getSupportsPstnProtocol()) { @@ -319,7 +343,7 @@ export default class RoomList extends React.PureComponent { private getRoomDelta = (roomId: string, delta: number, unread = false) => { const lists = RoomListStore.instance.orderedLists; - const rooms: Room = []; + const rooms: Room[] = []; TAG_ORDER.forEach(t => { let listRooms = lists[t]; @@ -340,6 +364,10 @@ export default class RoomList extends React.PureComponent { return room; }; + private updateSuggestedRooms = (suggestedRooms: ISpaceSummaryRoom[]) => { + this.setState({ suggestedRooms }); + }; + private updateLists = () => { const newLists = RoomListStore.instance.orderedLists; if (SettingsStore.getValue("advancedRoomListLogging")) { @@ -394,7 +422,44 @@ export default class RoomList extends React.PureComponent { dis.dispatch({ action: Action.ViewRoomDirectory, initialText }); }; - private renderCommunityInvites(): TemporaryTile[] { + private renderSuggestedRooms(): ReactComponentElement[] { + return this.state.suggestedRooms.map(room => { + const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"); + const avatar = ( + + ); + const viewRoom = () => { + defaultDispatcher.dispatch({ + action: "view_room", + room_id: room.room_id, + oobData: { + avatarUrl: room.avatar_url, + name, + }, + }); + }; + return ( + + ); + }); + } + + private renderCommunityInvites(): ReactComponentElement[] { // TODO: Put community invites in a more sensible place (not in the room list) // See https://github.com/vector-im/element-web/issues/14456 return MatrixClientPeg.get().getGroups().filter(g => { @@ -415,7 +480,7 @@ export default class RoomList extends React.PureComponent { }); }; return ( - { for (const orderedTagId of tagOrder) { 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); if (totalTiles === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) { continue; // skip tag - not needed @@ -470,7 +542,7 @@ export default class RoomList extends React.PureComponent { isMinimized={this.props.isMinimized} onResize={this.props.onResize} showSkeleton={showSkeleton} - extraBadTilesThatShouldntExist={extraTiles} + extraTiles={extraTiles} />); } diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index a2574bf60c..1a9ff182bc 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -17,7 +17,7 @@ limitations under the License. */ import * as React from "react"; -import {createRef} from "react"; +import { createRef, ReactComponentElement } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import classNames from 'classnames'; import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; @@ -48,7 +48,7 @@ import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNo import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore"; import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays"; import { objectExcluding, objectHasDiff } from "../../../utils/objects"; -import TemporaryTile from "./TemporaryTile"; +import ExtraTile from "./ExtraTile"; import { ListNotificationState } from "../../../stores/notifications/ListNotificationState"; import IconizedContextMenu from "../context_menus/IconizedContextMenu"; @@ -73,9 +73,7 @@ interface IProps { onResize: () => void; showSkeleton?: boolean; - // TODO: Don't use this. It's for community invites, and community invites shouldn't be here. - // You should feel bad if you use this. - extraBadTilesThatShouldntExist?: TemporaryTile[]; + extraTiles?: ReactComponentElement[]; // 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 height: number; rooms: Room[]; - filteredExtraTiles?: TemporaryTile[]; + filteredExtraTiles?: ReactComponentElement[]; } export default class RoomSublist extends React.Component { @@ -153,12 +151,12 @@ export default class RoomSublist extends React.Component { return padding; } - private get extraTiles(): TemporaryTile[] | null { + private get extraTiles(): ReactComponentElement[] | null { if (this.state.filteredExtraTiles) { return this.state.filteredExtraTiles; } - if (this.props.extraBadTilesThatShouldntExist) { - return this.props.extraBadTilesThatShouldntExist; + if (this.props.extraTiles) { + return this.props.extraTiles; } return null; } @@ -177,7 +175,7 @@ export default class RoomSublist extends React.Component { } public componentDidUpdate(prevProps: Readonly, prevState: Readonly) { - 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 // the amount of available rooms to cap the amount of requested visible rooms by the layout if (RoomSublist.calcNumTiles(prevState.rooms, prevExtraTiles) !== this.numTiles) { @@ -200,8 +198,8 @@ export default class RoomSublist extends React.Component { // 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. - const prevExtraTiles = this.props.extraBadTilesThatShouldntExist || []; - const nextExtraTiles = (nextState.filteredExtraTiles || nextProps.extraBadTilesThatShouldntExist) || []; + const prevExtraTiles = this.props.extraTiles || []; + const nextExtraTiles = (nextState.filteredExtraTiles || nextProps.extraTiles) || []; if (prevExtraTiles.length > 0 || nextExtraTiles.length > 0) { return true; } @@ -249,10 +247,10 @@ export default class RoomSublist extends React.Component { private onListsUpdated = () => { 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(); if (nameCondition) { - stateUpdates.filteredExtraTiles = this.props.extraBadTilesThatShouldntExist + stateUpdates.filteredExtraTiles = this.props.extraTiles .filter(t => nameCondition.matches(t.props.displayName || "")); } else if (this.state.filteredExtraTiles) { stateUpdates.filteredExtraTiles = null; diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 9d0543a6c5..88098d1b66 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -107,7 +107,8 @@ const SpaceCreateMenu = ({ onFinished }) => { if (visibility === null) { body =

{ _t("Create a space") }

-

{ _t("Organise rooms into spaces, for just you or anyone") }

+

{ _t("Spaces are new ways to group rooms and people. " + + "To join an existing space you’ll need an invite") }

{ /> setVisibility(Visibility.Private)} /> - {/*

{ _t("Looking to join an existing space?") }

*/} +

{ _t("You can change this later") }

; } else { body = @@ -134,9 +135,7 @@ const SpaceCreateMenu = ({ onFinished }) => {

{ - visibility === Visibility.Public - ? _t("Personalise your public space") - : _t("Personalise your private space") + visibility === Visibility.Public ? _t("Your public space") : _t("Your private space") }

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9ed75bde37..59cb6f67d8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -988,14 +988,15 @@ "Name": "Name", "Description": "Description", "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 you’ll need an invite": "Spaces are new ways to group rooms and people. To join an existing space you’ll need an invite", "Public": "Public", "Open space for anyone, best for communities": "Open space for anyone, best for communities", "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", - "Personalise your public space": "Personalise your public space", - "Personalise your private space": "Personalise your private space", + "Your public space": "Your public 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.", "You can change these at any point.": "You can change these at any point.", "Creating...": "Creating...", @@ -1532,7 +1533,9 @@ "Low priority": "Low priority", "System Alerts": "System Alerts", "Historical": "Historical", + "Suggested Rooms": "Suggested Rooms", "Custom Tag": "Custom Tag", + "Empty room": "Empty room", "Can't see what you’re looking for?": "Can't see what you’re looking for?", "Start a new chat": "Start a new chat", "Explore all public rooms": "Explore all public rooms", @@ -2194,10 +2197,12 @@ "Start a conversation with someone using their name or username (like ).": "Start a conversation with someone using their name or username (like ).", "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here", "Go": "Go", - "Invite someone using their name, email address, username (like ) or share this room.": "Invite someone using their name, email address, username (like ) or share this room.", - "Invite someone using their name, username (like ) or share this room.": "Invite someone using their name, username (like ) or share this room.", + "Invite to %(spaceName)s": "Invite to %(spaceName)s", + "Unnamed Space": "Unnamed Space", "Invite someone using their name, email address, username (like ) or share this space.": "Invite someone using their name, email address, username (like ) or share this space.", "Invite someone using their name, username (like ) or share this space.": "Invite someone using their name, username (like ) or share this space.", + "Invite someone using their name, email address, username (like ) or share this room.": "Invite someone using their name, email address, username (like ) or share this room.", + "Invite someone using their name, username (like ) or share this room.": "Invite someone using their name, username (like ) or share this room.", "Transfer": "Transfer", "a new master key signature": "a new master 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", "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.", - "Unnamed Space": "Unnamed Space", "Undo": "Undo", "Remove from Space": "Remove from Space", "No permissions": "No permissions", "You're in this space": "You're in this space", "You're in this room": "You're in this room", "Save changes": "Save changes", - "All users join by default": "All users join by default", + "Promoted to users": "Promoted to users", "Manage rooms": "Manage rooms", "Find a room...": "Find a room...", "Accept Invite": "Accept Invite", @@ -2634,7 +2638,7 @@ "Invite your teammates": "Invite your teammates", "Invite by username": "Invite by username", "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.", "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.", diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index d675879138..d1abc68f4e 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {throttle, sortBy} from "lodash"; -import {EventType} from "matrix-js-sdk/src/@types/event"; +import {sortBy, throttle} from "lodash"; +import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixEvent} from "matrix-js-sdk/src/models/event"; @@ -33,6 +33,7 @@ import {EnhancedMap, mapDiff} from "../utils/maps"; import {setHasDiff} from "../utils/sets"; import {objectDiff} from "../utils/objects"; import {arrayHasDiff} from "../utils/arrays"; +import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; type SpaceKey = string | symbol; @@ -41,11 +42,14 @@ interface IState {} const ACTIVE_SPACE_LS_KEY = "mx_active_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_SELECTED_SPACE = Symbol("selected-space"); // 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] return arr.reduce((result, room: Room) => { result[room.isSpaceRoom() ? 0 : 1].push(room); @@ -85,6 +89,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private spaceFilteredRooms = new Map>(); // The space currently selected in the Space Panel - if null then `Home` is selected private _activeSpace?: Room = null; + private _suggestedRooms: ISpaceSummaryRoom[] = []; public get spacePanelSpaces(): Room[] { return this.rootSpaces; @@ -94,11 +99,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient { 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; this._activeSpace = space; this.emit(UPDATE_SELECTED_SPACE, this.activeSpace); + this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []); // persist space selected if (space) { @@ -106,11 +116,29 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } else { 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, { via, + suggested, auto_join: autoJoin, }, roomId); } @@ -327,6 +355,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // this.onRoomUpdate(room); 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) => { @@ -408,6 +442,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } protected async onAction(payload: ActionPayload) { + if (!SettingsStore.getValue("feature_spaces")) return; switch (payload.action) { case "view_room": { const room = this.matrixClient?.getRoom(payload.room_id); diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 60a960261c..5775e685fd 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -409,7 +409,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { - 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 // 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 diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts index 7d3902f552..54d49ea18a 100644 --- a/src/stores/room-list/models.ts +++ b/src/stores/room-list/models.ts @@ -24,6 +24,7 @@ export enum DefaultTagID { Favourite = "m.favourite", DM = "im.vector.fake.direct", ServerNotice = "m.server_notice", + Suggested = "im.vector.fake.suggested", } export const OrderedDefaultTagIDs = [ @@ -33,6 +34,7 @@ export const OrderedDefaultTagIDs = [ DefaultTagID.Untagged, DefaultTagID.LowPriority, DefaultTagID.ServerNotice, + DefaultTagID.Suggested, DefaultTagID.Archived, ];