From 60ef657f64fda8cb4a20c8331cef1eff4b9e335d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 09:41:07 +0100 Subject: [PATCH 1/6] Properly hide spaces from the room list --- src/stores/room-list/RoomListStore.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 88df05b5d0..caab46a0c2 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -599,11 +599,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { private getPlausibleRooms(): Room[] { if (!this.matrixClient) return []; - let rooms = [ - ...this.matrixClient.getVisibleRooms(), - // also show space invites in the room list - ...this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "invite"), - ].filter(r => VisibilityProvider.instance.isRoomVisible(r)); + let rooms = this.matrixClient.getVisibleRooms().filter(r => VisibilityProvider.instance.isRoomVisible(r)); if (this.prefilterConditions.length > 0) { rooms = rooms.filter(r => { From 7efd4a43a5d1a9477379f215d47e0845ed7dde7a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 11:01:49 +0100 Subject: [PATCH 2/6] Show space invites at the top of the space panel --- src/components/views/spaces/SpacePanel.tsx | 22 +++++++-- .../views/spaces/SpaceTreeLevel.tsx | 6 ++- src/stores/SpaceStore.tsx | 47 +++++++++++++------ 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index bacf1bd929..36ab423885 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -25,7 +25,12 @@ import SpaceCreateMenu from "./SpaceCreateMenu"; import {SpaceItem} from "./SpaceTreeLevel"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; -import SpaceStore, {HOME_SPACE, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../../stores/SpaceStore"; +import SpaceStore, { + HOME_SPACE, + UPDATE_INVITED_SPACES, + UPDATE_SELECTED_SPACE, + UPDATE_TOP_LEVEL_SPACES, +} from "../../../stores/SpaceStore"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState"; import NotificationBadge from "../rooms/NotificationBadge"; @@ -105,19 +110,21 @@ const SpaceButton: React.FC = ({ ; } -const useSpaces = (): [Room[], Room | null] => { +const useSpaces = (): [Room[], Room[], Room | null] => { + const [invites, setInvites] = useState(SpaceStore.instance.invitedSpaces); + useEventEmitter(SpaceStore.instance, UPDATE_INVITED_SPACES, setInvites); const [spaces, setSpaces] = useState(SpaceStore.instance.spacePanelSpaces); useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces); const [activeSpace, setActiveSpace] = useState(SpaceStore.instance.activeSpace); useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace); - return [spaces, activeSpace]; + return [invites, spaces, activeSpace]; }; const SpacePanel = () => { // We don't need the handle as we position the menu in a constant location // eslint-disable-next-line @typescript-eslint/no-unused-vars const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); - const [spaces, activeSpace] = useSpaces(); + const [invites, spaces, activeSpace] = useSpaces(); const [isPanelCollapsed, setPanelCollapsed] = useState(true); const newClasses = classNames("mx_SpaceButton_new", { @@ -209,6 +216,13 @@ const SpacePanel = () => { notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)} isNarrow={isPanelCollapsed} /> + { invites.map(s => setPanelCollapsed(false)} + />) } { spaces.map(s => { mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition, mx_SpaceButton_narrow: isNarrow, }); - const notificationState = SpaceStore.instance.getNotificationState(space.roomId); + const notificationState = space.getMyMembership() === "invite" + ? StaticNotificationState.forSymbol("!", NotificationColor.Red) + : SpaceStore.instance.getNotificationState(space.roomId); let childItems; if (childSpaces && !collapsed) { diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 80722ad3ac..c28e24a460 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -46,6 +46,7 @@ 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_INVITED_SPACES = Symbol("invited-spaces"); export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); // Space Room ID/HOME_SPACE will be emitted when a Space's children change @@ -93,6 +94,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // The space currently selected in the Space Panel - if null then `Home` is selected private _activeSpace?: Room = null; private _suggestedRooms: ISpaceSummaryRoom[] = []; + private _invitedSpaces = new Set(); + + public get invitedSpaces(): Room[] { + return Array.from(this._invitedSpaces); + } public get spacePanelSpaces(): Room[] { return this.rootSpaces; @@ -214,25 +220,27 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return sortBy(parents, r => r.roomId)?.[0] || null; } - public getSpaces = () => { - return this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "join"); - }; - public getSpaceFilteredRoomIds = (space: Room | null): Set => { return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set(); }; private rebuild = throttle(() => { - // get all most-upgraded rooms & spaces except spaces which have been left (historical) - const visibleRooms = this.matrixClient.getVisibleRooms().filter(r => { - return !r.isSpaceRoom() || r.getMyMembership() === "join"; - }); + const [visibleSpaces, visibleRooms] = partitionSpacesAndRooms(this.matrixClient.getVisibleRooms()); + const [joinedSpaces, invitedSpaces] = visibleSpaces.reduce((arr, s) => { + if (s.getMyMembership() === "join") { + arr[0].push(s); + } else if (s.getMyMembership() === "invite") { + arr[1].push(s); + } + return arr; + }, [[], []]); - const unseenChildren = new Set(visibleRooms); + // exclude invited spaces from unseenChildren as they will be forcibly shown at the top level of the treeview + const unseenChildren = new Set([...visibleRooms, ...joinedSpaces]); const backrefs = new EnhancedMap>(); // Sort spaces by room ID to force the cycle breaking to be deterministic - const spaces = sortBy(visibleRooms.filter(r => r.isSpaceRoom()), space => space.roomId); + const spaces = sortBy(joinedSpaces, space => space.roomId); // TODO handle cleaning up links when a Space is removed spaces.forEach(space => { @@ -296,6 +304,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.onRoomsUpdate(); // TODO only do this if a change has happened this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); + + // build initial state of invited spaces as we would have missed the emitted events about the room at launch + this._invitedSpaces = new Set(invitedSpaces); + this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); }, 100, {trailing: true, leading: true}); onSpaceUpdate = () => { @@ -303,6 +315,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } private showInHomeSpace = (room: Room) => { + if (room.isSpaceRoom()) return false; return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites @@ -333,8 +346,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const oldFilteredRooms = this.spaceFilteredRooms; this.spaceFilteredRooms = new Map(); - // put all invites (rooms & spaces) in the Home Space - const invites = this.matrixClient.getRooms().filter(r => r.getMyMembership() === "invite"); + // put all room invites in the Home Space + const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite"); this.spaceFilteredRooms.set(HOME_SPACE, new Set(invites.map(room => room.roomId))); visibleRooms.forEach(room => { @@ -392,8 +405,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }); }, 100, {trailing: true, leading: true}); - private onRoom = (room: Room) => { - if (room?.isSpaceRoom()) { + private onRoom = (room: Room, membership?: string, oldMembership?: string) => { + if ((membership || room.getMyMembership()) === "invite") { + this._invitedSpaces.add(room); + this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); + } else if (oldMembership === "invite") { + this._invitedSpaces.delete(room); + this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); + } else if (room?.isSpaceRoom()) { this.onSpaceUpdate(); this.emit(room.roomId); } else { From a51aeaa04d10291fccb46c8b3f4099f3eeae3843 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 11:24:52 +0100 Subject: [PATCH 3/6] Disable context menu on space invite tiles as no options sensibly work --- src/components/views/spaces/SpaceTreeLevel.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 71ef4c562c..2e2901ce64 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -85,6 +85,7 @@ export class SpaceItem extends React.PureComponent { } private onContextMenu = (ev: React.MouseEvent) => { + if (this.props.space.getMyMembership() !== "join") return; ev.preventDefault(); ev.stopPropagation(); this.setState({ @@ -187,6 +188,8 @@ export class SpaceItem extends React.PureComponent { }; private renderContextMenu(): React.ReactElement { + if (this.props.space.getMyMembership() !== "join") return null; + let contextMenu = null; if (this.state.contextMenuPosition) { const userId = this.context.getUserId(); From 108a3088efe3bc30a090cc215fea77d5e8d6a52b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 11:25:11 +0100 Subject: [PATCH 4/6] Hide explore rooms quick action when active space is an invite --- src/components/views/rooms/RoomList.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 8ac706fc15..e4e638fc67 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -548,6 +548,9 @@ export default class RoomList extends React.PureComponent { } public render() { + const cli = MatrixClientPeg.get(); + const userId = cli.getUserId(); + let explorePrompt: JSX.Element; if (!this.props.isMinimized) { if (this.state.isNameFiltering) { @@ -568,21 +571,23 @@ export default class RoomList extends React.PureComponent { { this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") } ; - } else if (this.props.activeSpace) { + } else if ( + this.props.activeSpace?.canInvite(userId) || this.props.activeSpace?.getMyMembership() === "join" + ) { explorePrompt =
{ _t("Quick actions") }
- { this.props.activeSpace.canInvite(MatrixClientPeg.get().getUserId()) && {_t("Invite people")} } - {_t("Explore rooms")} - + }
; } else if (Object.values(this.state.sublists).some(list => list.length > 0)) { const unfilteredLists = RoomListStore.instance.unfilteredLists From e05200269f814b53b5a4b0ea7bcc1ecdf1af8392 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 12:07:03 +0100 Subject: [PATCH 5/6] fix comment --- src/components/views/spaces/SpaceTreeLevel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 2e2901ce64..6825d84013 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -69,7 +69,7 @@ export class SpaceItem extends React.PureComponent { super(props); this.state = { - collapsed: !props.isNested, // default to collapsed for root items + collapsed: !props.isNested, // default to collapsed for root items contextMenuPosition: null, }; } From e219fe082adf3c2669323251bd9571493bee58d3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 12:07:16 +0100 Subject: [PATCH 6/6] Tweak context switching edge case for space invites --- src/stores/SpaceStore.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index c28e24a460..a9a73e164f 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -123,7 +123,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // view last selected room from space const roomId = window.localStorage.getItem(getSpaceContextKey(this.activeSpace)); - if (roomId && this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join") { + // if the space being selected is an invite then always view that invite + // else if the last viewed room in this space is joined then view that + // else view space home or home depending on what is being clicked on + if (space?.getMyMembership !== "invite" && + this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join" + ) { defaultDispatcher.dispatch({ action: "view_room", room_id: roomId,