diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index d3e7d7efee..ffe67ce6ab 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -63,7 +63,7 @@ $activeBorderColor: $secondary-fg-color; } .mx_AutoHideScrollbar { - padding: 16px 0; + padding: 8px 0 16px; } .mx_SpaceButton_toggleCollapse { @@ -99,7 +99,6 @@ $activeBorderColor: $secondary-fg-color; .mx_SpaceButton { border-radius: 8px; - margin-bottom: 2px; display: flex; align-items: center; padding: 4px 4px 4px 0; diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 60abe36c29..a7ce630b96 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -16,6 +16,51 @@ limitations under the License. $SpaceRoomViewInnerWidth: 428px; +@define-mixin SpacePillButton { + position: relative; + padding: 16px 32px 16px 72px; + width: 432px; + box-sizing: border-box; + border-radius: 8px; + border: 1px solid $input-darker-bg-color; + font-size: $font-15px; + margin: 20px 0; + + > h3 { + font-weight: $font-semi-bold; + margin: 0 0 4px; + } + + > span { + color: $secondary-fg-color; + } + + &::before { + position: absolute; + content: ''; + width: 32px; + height: 32px; + top: 24px; + left: 20px; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 24px; + background-color: $tertiary-fg-color; + } + + &:hover { + border-color: $accent-color; + + &::before { + background-color: $accent-color; + } + + > span { + color: $primary-fg-color; + } + } +} + .mx_SpaceRoomView { .mx_MainSplit > div:first-child { padding: 80px 60px; @@ -331,64 +376,8 @@ $SpaceRoomViewInnerWidth: 428px; } .mx_SpaceRoomView_privateScope { - .mx_RadioButton { - width: $SpaceRoomViewInnerWidth; - border-radius: 8px; - border: 1px solid $space-button-outline-color; - padding: 16px 16px 16px 72px; - margin-top: 36px; - cursor: pointer; - box-sizing: border-box; - position: relative; - - > div:first-of-type { - // hide radio dot - display: none; - } - - .mx_RadioButton_content { - margin: 0; - - > h3 { - margin: 0 0 4px; - font-size: $font-15px; - font-weight: $font-semi-bold; - line-height: $font-18px; - } - - > div { - color: $secondary-fg-color; - font-size: $font-15px; - line-height: $font-24px; - } - } - - &::before { - content: ""; - position: absolute; - height: 32px; - width: 32px; - top: 24px; - left: 20px; - background-color: $secondary-fg-color; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - } - } - - .mx_RadioButton_checked { - border-color: $accent-color; - - .mx_RadioButton_content { - > div { - color: $primary-fg-color; - } - } - - &::before { - background-color: $accent-color; - } + .mx_AccessibleButton { + @mixin SpacePillButton; } .mx_SpaceRoomView_privateScope_justMeButton::before { diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss index 2a11ec9f23..bea39e2389 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.scss +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -14,10 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// TODO: the space panel currently does not have a fixed width, -// just the headers at each level have a max-width of 150px -// so this will look slightly off for now. We should probably use css grid for the whole main layout... -$spacePanelWidth: 200px; +$spacePanelWidth: 71px; .mx_SpaceCreateMenu_wrapper { // background blur everything except SpacePanel @@ -48,53 +45,11 @@ $spacePanelWidth: 200px; } .mx_SpaceCreateMenuType { - position: relative; - padding: 16px 32px 16px 72px; - width: 432px; - box-sizing: border-box; - border-radius: 8px; - border: 1px solid $input-darker-bg-color; - font-size: $font-15px; - margin: 20px 0; - - > h3 { - font-weight: $font-semi-bold; - margin: 0 0 4px; - } - - > span { - color: $secondary-fg-color; - } - - &::before { - position: absolute; - content: ''; - width: 32px; - height: 32px; - top: 24px; - left: 20px; - mask-position: center; - mask-repeat: no-repeat; - mask-size: 32px; - background-color: $tertiary-fg-color; - } - - &:hover { - border-color: $accent-color; - - &::before { - background-color: $accent-color; - } - - > span { - color: $primary-fg-color; - } - } + @mixin SpacePillButton; } .mx_SpaceCreateMenuType_public::before { mask-image: url('$(res)/img/globe.svg'); - mask-size: 26px; } .mx_SpaceCreateMenuType_private::before { mask-image: url('$(res)/img/element-icons/lock.svg'); diff --git a/res/css/views/spaces/_SpacePublicShare.scss b/res/css/views/spaces/_SpacePublicShare.scss index 9ba0549ae3..373fa94e00 100644 --- a/res/css/views/spaces/_SpacePublicShare.scss +++ b/res/css/views/spaces/_SpacePublicShare.scss @@ -16,38 +16,7 @@ limitations under the License. .mx_SpacePublicShare { .mx_AccessibleButton { - border: 1px solid $space-button-outline-color; - box-sizing: border-box; - border-radius: 8px; - padding: 12px 24px 12px 52px; - margin-top: 16px; - width: $SpaceRoomViewInnerWidth; - font-size: $font-15px; - line-height: $font-24px; - position: relative; - display: flex; - - > span { - color: #368bd6; - margin-left: auto; - } - - &:hover { - background-color: rgba(141, 151, 165, 0.1); - } - - &::before { - content: ""; - position: absolute; - width: 30px; - height: 30px; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - background: $muted-fg-color; - left: 12px; - top: 9px; - } + @mixin SpacePillButton; &.mx_SpacePublicShare_shareButton::before { mask-image: url('$(res)/img/element-icons/link.svg'); diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 0b0f2a2ac9..f0789e1e21 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -17,6 +17,7 @@ limitations under the License. import React, {RefObject, useContext, useRef, useState} from "react"; import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {Room} from "matrix-js-sdk/src/models/room"; +import {EventSubscription} from "fbemitter"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import RoomAvatar from "../views/avatars/RoomAvatar"; @@ -31,7 +32,6 @@ import {useRoomMembers} from "../../hooks/useRoomMembers"; import createRoom, {IOpts, Preset} from "../../createRoom"; import Field from "../views/elements/Field"; import {useEventEmitter} from "../../hooks/useEventEmitter"; -import StyledRadioGroup from "../views/elements/StyledRadioGroup"; import withValidation from "../views/elements/Validation"; import * as Email from "../../email"; import defaultDispatcher from "../../dispatcher/dispatcher"; @@ -42,7 +42,6 @@ import ErrorBoundary from "../views/elements/ErrorBoundary"; import {ActionPayload} from "../../dispatcher/payloads"; import RightPanel from "./RightPanel"; import RightPanelStore from "../../stores/RightPanelStore"; -import {EventSubscription} from "fbemitter"; import {RightPanelPhases} from "../../stores/RightPanelStorePhases"; import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload"; import {useStateArray} from "../../hooks/useStateArray"; @@ -54,6 +53,7 @@ import {EnhancedMap} from "../../utils/maps"; import AutoHideScrollbar from "./AutoHideScrollbar"; import MemberAvatar from "../views/avatars/MemberAvatar"; import {useStateToggle} from "../../hooks/useStateToggle"; +import SpaceStore from "../../stores/SpaceStore"; interface IProps { space: Room; @@ -66,6 +66,7 @@ interface IProps { interface IState { phase: Phase; showRightPanel: boolean; + myMembership: string; } enum Phase { @@ -98,6 +99,8 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => const cli = useContext(MatrixClientContext); const myMembership = useMyRoomMembership(space); + const [busy, setBusy] = useState(false); + let inviterSection; let joinButtons; if (myMembership === "invite") { @@ -121,11 +124,35 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => } joinButtons = <> - - + { + setBusy(true); + onRejectButtonClicked(); + }} /> + { + setBusy(true); + onJoinButtonClicked(); + }} + /> ; } else { - joinButtons = + joinButtons = ( + { + setBusy(true); + onJoinButtonClicked(); + }} + /> + ) + } + + if (busy) { + joinButtons = ; } let visibilitySection; @@ -337,6 +364,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { placeholder={placeholders[i]} value={roomNames[i]} onChange={ev => setRoomName(i, ev.target.value)} + autoFocus={i === 2} />; }); @@ -369,7 +397,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { let buttonLabel = _t("Skip for now"); if (roomNames.some(name => name.trim())) { onClick = onNextClick; - buttonLabel = busy ? _t("Creating rooms...") : _t("Next") + buttonLabel = busy ? _t("Creating rooms...") : _t("Continue") } return
@@ -391,50 +419,40 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { const SpaceSetupPublicShare = ({ space, onFinished }) => { return
-

{ _t("Share your public space") }

-
{ _t("At the moment only you can see it.") }
+

{ _t("Share %(name)s", { name: space.name }) }

+
+ { _t("It's just you at the moment, it will be even better with others.") } +
- +
; }; -const SpaceSetupPrivateScope = ({ onFinished }) => { - const [option, setOption] = useState(null); - +const SpaceSetupPrivateScope = ({ space, onFinished }) => { return

{ _t("Who are you working with?") }

-
{ _t("Ensure the right people have access to the space.") }
- - -

{ _t("Just Me") }

-
{ _t("A private space just for you") }
- , - }, { - value: "meAndMyTeammates", - className: "mx_SpaceRoomView_privateScope_meAndMyTeammatesButton", - label: -

{ _t("Me and my teammates") }

-
{ _t("A private space for you and your teammates") }
-
, - }, - ]} - /> - -
- onFinished(option !== "justMe")} /> +
+ { _t("Make sure the right people have access to %(name)s", { name: space.name }) }
+ + { onFinished(false) }} + > +

{ _t("Just me") }

+
{ _t("A private space to organise your rooms") }
+
+ { onFinished(true) }} + > +

{ _t("Me and my teammates") }

+
{ _t("A private space for you and your teammates") }
+
; }; @@ -464,6 +482,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => { onChange={ev => setEmailAddress(i, ev.target.value)} ref={fieldRefs[i]} onValidate={validateEmailRules} + autoFocus={i === 0} />; }); @@ -501,9 +520,18 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => { setBusy(false); }; + let onClick = onFinished; + let buttonLabel = _t("Skip for now"); + if (emailAddresses.some(name => name.trim())) { + onClick = onNextClick; + buttonLabel = busy ? _t("Inviting...") : _t("Continue") + } + return

{ _t("Invite your teammates") }

-
{ _t("Ensure the right people have access to the space.") }
+
+ { _t("Make sure the right people have access. You can invite more later.") } +
{ error &&
{ error }
} { fields } @@ -518,8 +546,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
- {_t("Skip for now")} - +
; }; @@ -547,17 +574,26 @@ export default class SpaceRoomView extends React.PureComponent { this.state = { phase, showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, + myMembership: this.props.space.getMyMembership(), }; this.dispatcherRef = defaultDispatcher.register(this.onAction); this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate); + this.context.on("Room.myMembership", this.onMyMembership); } componentWillUnmount() { defaultDispatcher.unregister(this.dispatcherRef); this.rightPanelStoreToken.remove(); + this.context.off("Room.myMembership", this.onMyMembership); } + private onMyMembership = (room: Room, myMembership: string) => { + if (room.roomId === this.props.space.roomId) { + this.setState({ myMembership }); + } + }; + private onRightPanelStoreUpdate = () => { this.setState({ showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, @@ -594,10 +630,43 @@ export default class SpaceRoomView extends React.PureComponent { } }; + private goToFirstRoom = async () => { + const childRooms = SpaceStore.instance.getChildRooms(this.props.space.roomId); + if (childRooms.length) { + const room = childRooms[0]; + defaultDispatcher.dispatch({ + action: "view_room", + room_id: room.roomId, + }); + return; + } + + let suggestedRooms = SpaceStore.instance.suggestedRooms; + if (SpaceStore.instance.activeSpace !== this.props.space) { + // the space store has the suggested rooms loaded for a different space, fetch the right ones + suggestedRooms = (await SpaceStore.instance.fetchSuggestedRooms(this.props.space, 1)).rooms; + } + + if (suggestedRooms.length) { + const room = suggestedRooms[0]; + defaultDispatcher.dispatch({ + action: "view_room", + room_id: room.room_id, + oobData: { + avatarUrl: room.avatar_url, + name: room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"), + }, + }); + return; + } + + this.setState({ phase: Phase.Landing }); + }; + private renderBody() { switch (this.state.phase) { case Phase.Landing: - if (this.props.space.getMyMembership() === "join") { + if (this.state.myMembership === "join") { return ; } else { return { return this.setState({ phase: Phase.PublicShare })} />; case Phase.PublicShare: - return this.setState({ phase: Phase.Landing })} - />; + return ; case Phase.PrivateScope: return { this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateCreateRooms }); }} @@ -634,7 +702,8 @@ export default class SpaceRoomView extends React.PureComponent { return this.setState({ phase: Phase.Landing })} />; } diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 66efaefd9d..500637244a 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -69,6 +69,7 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, const existingRoomsSet = new Set(existingRooms); const rooms = cli.getVisibleRooms().filter(room => { return !existingRoomsSet.has(room) // not already in space + && !room.isSpaceRoom() // not a space itself && room.name.toLowerCase().includes(lcQuery) // contains query && !DMRoomMap.shared().getUserIdForRoomId(room.roomId); // not a DM }); diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 88098d1b66..879cf929e0 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -108,7 +108,7 @@ const SpaceCreateMenu = ({ onFinished }) => { body =

{ _t("Create a space") }

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

+ "To join an existing space you'll need an invite.") }

{

{ - _t("Give it a photo, name and description to help you identify it.") + _t("Add some details to help people recognise it.") } { - _t("You can change these at any point.") + _t("You can change these anytime.") }

diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 48e2c86b2c..bacf1bd929 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -220,13 +220,19 @@ const SpacePanel = () => { { + openMenu(); + if (!isPanelCollapsed) setPanelCollapsed(true); + }} isNarrow={isPanelCollapsed} /> setPanelCollapsed(!isPanelCollapsed)} + onClick={() => { + setPanelCollapsed(!isPanelCollapsed); + if (menuDisplayed) closeMenu(); + }} title={expandCollapseButtonTitle} /> { contextMenu } diff --git a/src/components/views/spaces/SpacePublicShare.tsx b/src/components/views/spaces/SpacePublicShare.tsx index 3930c1db16..b2d3b7ce29 100644 --- a/src/components/views/spaces/SpacePublicShare.tsx +++ b/src/components/views/spaces/SpacePublicShare.tsx @@ -41,13 +41,13 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => { const success = await copyPlaintext(permalinkCreator.forRoom()); const text = success ? _t("Copied!") : _t("Failed to copy"); setCopiedText(text); - await sleep(10); + await sleep(5000); if (copiedText === text) { // if the text hasn't changed by another click then clear it after some time setCopiedText(_t("Click to copy")); } }} > - { _t("Share invite link") } +

{ _t("Share invite link") }

{ copiedText } { onFinished(); }} > - { _t("Invite by email or username") } +

{ _t("Invite people") }

+ { _t("Invite with email or username") }
; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 63b19831bb..9739523d96 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -989,7 +989,7 @@ "Name": "Name", "Description": "Description", "Create a space": "Create a space", - "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", + "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", @@ -998,8 +998,8 @@ "Go back": "Go back", "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.", + "Add some details to help people recognise it.": "Add some details to help people recognise it.", + "You can change these anytime.": "You can change these anytime.", "Creating...": "Creating...", "Create": "Create", "Expand space panel": "Expand space panel", @@ -1009,10 +1009,10 @@ "Copied!": "Copied!", "Failed to copy": "Failed to copy", "Share invite link": "Share invite link", - "Invite by email or username": "Invite by email or username", + "Invite people": "Invite people", + "Invite with email or username": "Invite with email or username", "Invite members": "Invite members", "Share your public space": "Share your public space", - "Invite people": "Invite people", "Settings": "Settings", "Leave space": "Leave space", "New room": "New room", @@ -2633,22 +2633,24 @@ "Failed to create initial space rooms": "Failed to create initial space rooms", "Skip for now": "Skip for now", "Creating rooms...": "Creating rooms...", - "At the moment only you can see it.": "At the moment only you can see it.", - "Finish": "Finish", + "Share %(name)s": "Share %(name)s", + "It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.", + "Go to my first room": "Go to my first room", "Who are you working with?": "Who are you working with?", - "Ensure the right people have access to the space.": "Ensure the right people have access to the space.", - "Just Me": "Just Me", - "A private space just for you": "A private space just for you", + "Make sure the right people have access to %(name)s": "Make sure the right people have access to %(name)s", + "Just me": "Just me", + "A private space to organise your rooms": "A private space to organise your rooms", "Me and my teammates": "Me and my teammates", "A private space for you and your teammates": "A private space for you and your teammates", "Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s", - "Invite your teammates": "Invite your teammates", - "Invite by username": "Invite by username", "Inviting...": "Inviting...", + "Invite your teammates": "Invite your teammates", + "Make sure the right people have access. You can invite more later.": "Make sure the right people have access. You can invite more later.", + "Invite by username": "Invite by username", "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.", + "Let's create a room for each of them. You can add more later too, including already existing ones.": "Let's create a room for each of them. You can add more later too, including already existing ones.", "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 more later too, including already existing ones.": "We'll create rooms for each of them. You can add more later too, including already existing ones.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Failed to load timeline position": "Failed to load timeline position", diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index d1abc68f4e..b82acfd0ed 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -118,23 +118,32 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } 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); + const data = await this.fetchSuggestedRooms(space); + 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); } } } + public fetchSuggestedRooms = async (space: Room, limit = MAX_SUGGESTED_ROOMS) => { + try { + const data: { + rooms: ISpaceSummaryRoom[]; + events: ISpaceSummaryEvent[]; + } = await this.matrixClient.getSpaceSummary(space.roomId, 0, true, false, limit); + return data; + } catch (e) { + console.error(e); + } + return { + rooms: [], + events: [], + }; + }; + public addRoomToSpace(space: Room, roomId: string, via: string[], suggested = false, autoJoin = false) { return this.matrixClient.sendStateEvent(space.roomId, EventType.SpaceChild, { via, @@ -385,7 +394,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent: MatrixEvent) => { if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) { // If the room was in favourites and now isn't or the opposite then update its position in the trees - if (!!ev.getContent()[DefaultTagID.Favourite] !== !!lastEvent.getContent()[DefaultTagID.Favourite]) { + const oldTags = lastEvent.getContent()?.tags; + const newTags = ev.getContent()?.tags; + if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) { this.onRoomUpdate(room); } }