diff --git a/web/src/api/statestore/index.ts b/web/src/api/statestore/index.ts index 3bbe512..106a3f4 100644 --- a/web/src/api/statestore/index.ts +++ b/web/src/api/statestore/index.ts @@ -1,3 +1,4 @@ export * from "./main.ts" export * from "./room.ts" export * from "./hooks.ts" +export * from "./space.ts" diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index 3274b5e..78dadf0 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -111,6 +111,23 @@ export class StateStore { return true } + getSpaceByID(spaceID: string | undefined): RoomListFilter | null { + if (!spaceID) { + return null + } + const realSpace = this.spaceEdges.get(spaceID) + if (realSpace) { + return realSpace + } + for (const pseudoSpace of this.pseudoSpaces) { + if (pseudoSpace.id === spaceID) { + return pseudoSpace + } + } + console.warn("Failed to find space", spaceID) + return null + } + get roomListFilterFunc(): ((entry: RoomListEntry) => boolean) | null { if (!this.currentRoomListFilter && !this.currentRoomListQuery) { return null diff --git a/web/src/ui/MainScreen.tsx b/web/src/ui/MainScreen.tsx index 05f8b59..c735548 100644 --- a/web/src/ui/MainScreen.tsx +++ b/web/src/ui/MainScreen.tsx @@ -16,7 +16,7 @@ import { JSX, use, useEffect, useMemo, useReducer, useRef, useState } from "react" import { SyncLoader } from "react-spinners" import Client from "@/api/client.ts" -import { RoomStateStore } from "@/api/statestore" +import { RoomListFilter, RoomStateStore } from "@/api/statestore" import type { RoomID } from "@/api/types" import { useEventAsState } from "@/util/eventdispatcher.ts" import { ensureString, ensureStringArray, parseMatrixURI } from "@/util/validation.ts" @@ -52,6 +52,7 @@ class ContextFields implements MainScreenContextFields { constructor( private directSetRightPanel: (props: RightPanelProps | null) => void, private directSetActiveRoom: (room: RoomStateStore | RoomPreviewProps | null) => void, + private directSetSpace: (space: RoomListFilter | null) => void, private client: Client, ) { this.keybindings = new Keybindings(client.store, this) @@ -109,6 +110,24 @@ class ContextFields implements MainScreenContextFields { } } + setSpace = (space: RoomListFilter | null, pushState = true) => { + if (space === this.client.store.currentRoomListFilter) { + return + } + console.log("Switching to space", space?.id) + this.directSetSpace(space) + this.client.store.currentRoomListFilter = space + if (pushState) { + if (this.client.store.activeRoomID && space) { + const entry = this.client.store.roomListEntries.get(this.client.store.activeRoomID) + if (entry && !space.include(entry)) { + this.setActiveRoom(null) + } + } + history.replaceState({ ...(history.state || {}), space_id: space?.id }, "") + } + } + #setPreviewRoom(roomID: RoomID, pushState: boolean, meta?: Partial) { const invite = this.client.store.inviteRooms.get(roomID) this.#closeActiveRoom(false) @@ -120,6 +139,7 @@ class ContextFields implements MainScreenContextFields { room_id: roomID, source_via: meta?.via, source_alias: meta?.alias, + space_id: history.state.space_id, }, "") } } @@ -148,7 +168,7 @@ class ContextFields implements MainScreenContextFields { .querySelector(`div.room-entry[data-room-id="${CSS.escape(room.roomID)}"]`) ?.scrollIntoView({ block: "nearest" }) if (pushState) { - history.pushState({ room_id: room.roomID }, "") + history.pushState({ room_id: room.roomID, space_id: history.state.space_id }, "") } let roomNameForTitle = room.meta.current.name if (roomNameForTitle && roomNameForTitle.length > 48) { @@ -166,7 +186,7 @@ class ContextFields implements MainScreenContextFields { this.client.store.activeRoomIsPreview = false this.keybindings.activeRoom = null if (pushState) { - history.pushState({}, "") + history.pushState({ space_id: history.state.space_id }, "") } document.title = this.#getWindowTitle() } @@ -279,12 +299,13 @@ const activeRoomReducer = ( const MainScreen = () => { const [[prevActiveRoom, activeRoom], directSetActiveRoom] = useReducer(activeRoomReducer, [null, null]) + const [space, directSetSpace] = useState(null) const skipNextTransitionRef = useRef(false) const [rightPanel, directSetRightPanel] = useState(null) const client = use(ClientContext)! const syncStatus = useEventAsState(client.syncStatus) const context = useMemo( - () => new ContextFields(directSetRightPanel, directSetActiveRoom, client), + () => new ContextFields(directSetRightPanel, directSetActiveRoom, directSetSpace, client), [client], ) useEffect(() => { @@ -292,6 +313,10 @@ const MainScreen = () => { const listener = (evt: PopStateEvent) => { skipNextTransitionRef.current = evt.hasUAVisualTransition const roomID = evt.state?.room_id ?? null + const spaceID = evt.state?.space_id ?? undefined + if (spaceID !== client.store.currentRoomListFilter?.id) { + context.setSpace(client.store.getSpaceByID(spaceID), false) + } if (roomID !== client.store.activeRoomID) { context.setActiveRoom(roomID, { alias: ensureString(evt.state?.source_alias) || undefined, @@ -372,7 +397,7 @@ const MainScreen = () => {
- + {resizeHandle1} {renderedRoom ? renderedRoom instanceof RoomStateStore diff --git a/web/src/ui/MainScreenContext.ts b/web/src/ui/MainScreenContext.ts index 67c0b0b..28aa97d 100644 --- a/web/src/ui/MainScreenContext.ts +++ b/web/src/ui/MainScreenContext.ts @@ -14,12 +14,14 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import { createContext } from "react" +import { RoomListFilter } from "@/api/statestore" import type { RoomID } from "@/api/types" import type { RightPanelProps } from "./rightpanel/RightPanel.tsx" import type { RoomPreviewProps } from "./roomview/RoomPreview.tsx" export interface MainScreenContextFields { setActiveRoom: (roomID: RoomID | null, previewMeta?: Partial) => void + setSpace: (space: RoomListFilter | null, pushState?: boolean) => void clickRoom: (evt: React.MouseEvent) => void clearActiveRoom: () => void @@ -32,6 +34,9 @@ const stubContext = { get setActiveRoom(): never { throw new Error("MainScreenContext used outside main screen") }, + get setSpace(): never { + throw new Error("MainScreenContext used outside main screen") + }, get clickRoom(): never { throw new Error("MainScreenContext used outside main screen") }, diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index 2cbd9be..b4c0b78 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { use, useCallback, useRef, useState } from "react" -import { RoomListFilter, Space as SpaceStore, SpaceUnreadCounts } from "@/api/statestore/space.ts" +import { RoomListFilter, Space as SpaceStore, SpaceUnreadCounts } from "@/api/statestore" import type { RoomID } from "@/api/types" import { useEventAsState } from "@/util/eventdispatcher.ts" import reverseMap from "@/util/reversemap.ts" @@ -31,35 +31,25 @@ import "./RoomList.css" interface RoomListProps { activeRoomID: RoomID | null + space: RoomListFilter | null } -const RoomList = ({ activeRoomID }: RoomListProps) => { +const RoomList = ({ activeRoomID, space }: RoomListProps) => { const client = use(ClientContext)! const mainScreen = use(MainScreenContext) const roomList = useEventAsState(client.store.roomList) const spaces = useEventAsState(client.store.topLevelSpaces) const searchInputRef = useRef(null) const [query, directSetQuery] = useState("") - const [space, directSetSpace] = useState(null) const setQuery = (evt: React.ChangeEvent) => { client.store.currentRoomListQuery = toSearchableString(evt.target.value) directSetQuery(evt.target.value) } - const setSpace = useCallback((space: RoomListFilter | null) => { - directSetSpace(space) - client.store.currentRoomListFilter = space - if (client.store.activeRoomID && space) { - const entry = client.store.roomListEntries.get(client.store.activeRoomID) - if (entry && !space.include(entry)) { - mainScreen.setActiveRoom(null) - } - } - }, [client, mainScreen]) const onClickSpace = useCallback((evt: React.MouseEvent) => { const store = client.store.getSpaceStore(evt.currentTarget.getAttribute("data-target-space")!) - setSpace(store) - }, [setSpace, client]) + mainScreen.setSpace(store) + }, [mainScreen, client]) const onClickSpaceUnread = useCallback(( evt: React.MouseEvent | null, space?: SpaceStore | null, ) => { @@ -130,11 +120,11 @@ const RoomList = ({ activeRoomID }: RoomListProps) => {
- + {client.store.pseudoSpaces.map(pseudoSpace => )} diff --git a/web/src/ui/roomlist/UnreadCount.tsx b/web/src/ui/roomlist/UnreadCount.tsx index 82807c0..b1fb141 100644 --- a/web/src/ui/roomlist/UnreadCount.tsx +++ b/web/src/ui/roomlist/UnreadCount.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { SpaceUnreadCounts } from "@/api/statestore/space.ts" +import { SpaceUnreadCounts } from "@/api/statestore" interface UnreadCounts extends SpaceUnreadCounts { marked_unread?: boolean