1
0
Fork 0
forked from Mirrors/gomuks

web/roomlist: restore open space when using browser history

This commit is contained in:
Tulir Asokan 2025-01-01 12:21:10 +02:00
parent 59e1b760d6
commit 8b7d0fe6b6
6 changed files with 61 additions and 23 deletions

View file

@ -1,3 +1,4 @@
export * from "./main.ts" export * from "./main.ts"
export * from "./room.ts" export * from "./room.ts"
export * from "./hooks.ts" export * from "./hooks.ts"
export * from "./space.ts"

View file

@ -111,6 +111,23 @@ export class StateStore {
return true 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 { get roomListFilterFunc(): ((entry: RoomListEntry) => boolean) | null {
if (!this.currentRoomListFilter && !this.currentRoomListQuery) { if (!this.currentRoomListFilter && !this.currentRoomListQuery) {
return null return null

View file

@ -16,7 +16,7 @@
import { JSX, use, useEffect, useMemo, useReducer, useRef, useState } from "react" import { JSX, use, useEffect, useMemo, useReducer, useRef, useState } from "react"
import { SyncLoader } from "react-spinners" import { SyncLoader } from "react-spinners"
import Client from "@/api/client.ts" import Client from "@/api/client.ts"
import { RoomStateStore } from "@/api/statestore" import { RoomListFilter, RoomStateStore } from "@/api/statestore"
import type { RoomID } from "@/api/types" import type { RoomID } from "@/api/types"
import { useEventAsState } from "@/util/eventdispatcher.ts" import { useEventAsState } from "@/util/eventdispatcher.ts"
import { ensureString, ensureStringArray, parseMatrixURI } from "@/util/validation.ts" import { ensureString, ensureStringArray, parseMatrixURI } from "@/util/validation.ts"
@ -52,6 +52,7 @@ class ContextFields implements MainScreenContextFields {
constructor( constructor(
private directSetRightPanel: (props: RightPanelProps | null) => void, private directSetRightPanel: (props: RightPanelProps | null) => void,
private directSetActiveRoom: (room: RoomStateStore | RoomPreviewProps | null) => void, private directSetActiveRoom: (room: RoomStateStore | RoomPreviewProps | null) => void,
private directSetSpace: (space: RoomListFilter | null) => void,
private client: Client, private client: Client,
) { ) {
this.keybindings = new Keybindings(client.store, this) 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<RoomPreviewProps>) { #setPreviewRoom(roomID: RoomID, pushState: boolean, meta?: Partial<RoomPreviewProps>) {
const invite = this.client.store.inviteRooms.get(roomID) const invite = this.client.store.inviteRooms.get(roomID)
this.#closeActiveRoom(false) this.#closeActiveRoom(false)
@ -120,6 +139,7 @@ class ContextFields implements MainScreenContextFields {
room_id: roomID, room_id: roomID,
source_via: meta?.via, source_via: meta?.via,
source_alias: meta?.alias, 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)}"]`) .querySelector(`div.room-entry[data-room-id="${CSS.escape(room.roomID)}"]`)
?.scrollIntoView({ block: "nearest" }) ?.scrollIntoView({ block: "nearest" })
if (pushState) { 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 let roomNameForTitle = room.meta.current.name
if (roomNameForTitle && roomNameForTitle.length > 48) { if (roomNameForTitle && roomNameForTitle.length > 48) {
@ -166,7 +186,7 @@ class ContextFields implements MainScreenContextFields {
this.client.store.activeRoomIsPreview = false this.client.store.activeRoomIsPreview = false
this.keybindings.activeRoom = null this.keybindings.activeRoom = null
if (pushState) { if (pushState) {
history.pushState({}, "") history.pushState({ space_id: history.state.space_id }, "")
} }
document.title = this.#getWindowTitle() document.title = this.#getWindowTitle()
} }
@ -279,12 +299,13 @@ const activeRoomReducer = (
const MainScreen = () => { const MainScreen = () => {
const [[prevActiveRoom, activeRoom], directSetActiveRoom] = useReducer(activeRoomReducer, [null, null]) const [[prevActiveRoom, activeRoom], directSetActiveRoom] = useReducer(activeRoomReducer, [null, null])
const [space, directSetSpace] = useState<RoomListFilter | null>(null)
const skipNextTransitionRef = useRef(false) const skipNextTransitionRef = useRef(false)
const [rightPanel, directSetRightPanel] = useState<RightPanelProps | null>(null) const [rightPanel, directSetRightPanel] = useState<RightPanelProps | null>(null)
const client = use(ClientContext)! const client = use(ClientContext)!
const syncStatus = useEventAsState(client.syncStatus) const syncStatus = useEventAsState(client.syncStatus)
const context = useMemo( const context = useMemo(
() => new ContextFields(directSetRightPanel, directSetActiveRoom, client), () => new ContextFields(directSetRightPanel, directSetActiveRoom, directSetSpace, client),
[client], [client],
) )
useEffect(() => { useEffect(() => {
@ -292,6 +313,10 @@ const MainScreen = () => {
const listener = (evt: PopStateEvent) => { const listener = (evt: PopStateEvent) => {
skipNextTransitionRef.current = evt.hasUAVisualTransition skipNextTransitionRef.current = evt.hasUAVisualTransition
const roomID = evt.state?.room_id ?? null 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) { if (roomID !== client.store.activeRoomID) {
context.setActiveRoom(roomID, { context.setActiveRoom(roomID, {
alias: ensureString(evt.state?.source_alias) || undefined, alias: ensureString(evt.state?.source_alias) || undefined,
@ -372,7 +397,7 @@ const MainScreen = () => {
<ModalWrapper> <ModalWrapper>
<StylePreferences client={client} activeRoom={activeRealRoom}/> <StylePreferences client={client} activeRoom={activeRealRoom}/>
<main className={classNames.join(" ")} style={extraStyle}> <main className={classNames.join(" ")} style={extraStyle}>
<RoomList activeRoomID={activeRoom?.roomID ?? null}/> <RoomList activeRoomID={activeRoom?.roomID ?? null} space={space}/>
{resizeHandle1} {resizeHandle1}
{renderedRoom {renderedRoom
? renderedRoom instanceof RoomStateStore ? renderedRoom instanceof RoomStateStore

View file

@ -14,12 +14,14 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { createContext } from "react" import { createContext } from "react"
import { RoomListFilter } from "@/api/statestore"
import type { RoomID } from "@/api/types" import type { RoomID } from "@/api/types"
import type { RightPanelProps } from "./rightpanel/RightPanel.tsx" import type { RightPanelProps } from "./rightpanel/RightPanel.tsx"
import type { RoomPreviewProps } from "./roomview/RoomPreview.tsx" import type { RoomPreviewProps } from "./roomview/RoomPreview.tsx"
export interface MainScreenContextFields { export interface MainScreenContextFields {
setActiveRoom: (roomID: RoomID | null, previewMeta?: Partial<RoomPreviewProps>) => void setActiveRoom: (roomID: RoomID | null, previewMeta?: Partial<RoomPreviewProps>) => void
setSpace: (space: RoomListFilter | null, pushState?: boolean) => void
clickRoom: (evt: React.MouseEvent) => void clickRoom: (evt: React.MouseEvent) => void
clearActiveRoom: () => void clearActiveRoom: () => void
@ -32,6 +34,9 @@ const stubContext = {
get setActiveRoom(): never { get setActiveRoom(): never {
throw new Error("MainScreenContext used outside main screen") throw new Error("MainScreenContext used outside main screen")
}, },
get setSpace(): never {
throw new Error("MainScreenContext used outside main screen")
},
get clickRoom(): never { get clickRoom(): never {
throw new Error("MainScreenContext used outside main screen") throw new Error("MainScreenContext used outside main screen")
}, },

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import React, { use, useCallback, useRef, useState } from "react" 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 type { RoomID } from "@/api/types"
import { useEventAsState } from "@/util/eventdispatcher.ts" import { useEventAsState } from "@/util/eventdispatcher.ts"
import reverseMap from "@/util/reversemap.ts" import reverseMap from "@/util/reversemap.ts"
@ -31,35 +31,25 @@ import "./RoomList.css"
interface RoomListProps { interface RoomListProps {
activeRoomID: RoomID | null activeRoomID: RoomID | null
space: RoomListFilter | null
} }
const RoomList = ({ activeRoomID }: RoomListProps) => { const RoomList = ({ activeRoomID, space }: RoomListProps) => {
const client = use(ClientContext)! const client = use(ClientContext)!
const mainScreen = use(MainScreenContext) const mainScreen = use(MainScreenContext)
const roomList = useEventAsState(client.store.roomList) const roomList = useEventAsState(client.store.roomList)
const spaces = useEventAsState(client.store.topLevelSpaces) const spaces = useEventAsState(client.store.topLevelSpaces)
const searchInputRef = useRef<HTMLInputElement>(null) const searchInputRef = useRef<HTMLInputElement>(null)
const [query, directSetQuery] = useState("") const [query, directSetQuery] = useState("")
const [space, directSetSpace] = useState<RoomListFilter | null>(null)
const setQuery = (evt: React.ChangeEvent<HTMLInputElement>) => { const setQuery = (evt: React.ChangeEvent<HTMLInputElement>) => {
client.store.currentRoomListQuery = toSearchableString(evt.target.value) client.store.currentRoomListQuery = toSearchableString(evt.target.value)
directSetQuery(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<HTMLDivElement>) => { const onClickSpace = useCallback((evt: React.MouseEvent<HTMLDivElement>) => {
const store = client.store.getSpaceStore(evt.currentTarget.getAttribute("data-target-space")!) const store = client.store.getSpaceStore(evt.currentTarget.getAttribute("data-target-space")!)
setSpace(store) mainScreen.setSpace(store)
}, [setSpace, client]) }, [mainScreen, client])
const onClickSpaceUnread = useCallback(( const onClickSpaceUnread = useCallback((
evt: React.MouseEvent<HTMLDivElement> | null, space?: SpaceStore | null, evt: React.MouseEvent<HTMLDivElement> | null, space?: SpaceStore | null,
) => { ) => {
@ -130,11 +120,11 @@ const RoomList = ({ activeRoomID }: RoomListProps) => {
</button> </button>
</div> </div>
<div className="space-bar"> <div className="space-bar">
<FakeSpace space={null} setSpace={setSpace} isActive={space === null} /> <FakeSpace space={null} setSpace={mainScreen.setSpace} isActive={space === null} />
{client.store.pseudoSpaces.map(pseudoSpace => <FakeSpace {client.store.pseudoSpaces.map(pseudoSpace => <FakeSpace
key={pseudoSpace.id} key={pseudoSpace.id}
space={pseudoSpace} space={pseudoSpace}
setSpace={setSpace} setSpace={mainScreen.setSpace}
onClickUnread={onClickSpaceUnread} onClickUnread={onClickSpaceUnread}
isActive={space?.id === pseudoSpace.id} isActive={space?.id === pseudoSpace.id}
/>)} />)}

View file

@ -13,7 +13,7 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { SpaceUnreadCounts } from "@/api/statestore/space.ts" import { SpaceUnreadCounts } from "@/api/statestore"
interface UnreadCounts extends SpaceUnreadCounts { interface UnreadCounts extends SpaceUnreadCounts {
marked_unread?: boolean marked_unread?: boolean