From b4258278d88deac25d5b57a98a1732d13bea5aae Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Thu, 9 Jul 2026 02:35:04 +1000 Subject: [PATCH] fix: restore rooms and DMs after React Router 7 upgrade Stop double-encoding Matrix IDs in pathUtils, decode route params on read, fix millify CJS import, and read space children from currentState so channels and joins work again. --- src/app/components/BackRouteHandler.tsx | 6 ++- src/app/features/room/Room.tsx | 4 +- src/app/hooks/router/useExploreSelected.ts | 4 +- src/app/hooks/router/useSelectedRoom.ts | 4 +- src/app/hooks/router/useSelectedSpace.ts | 4 +- src/app/hooks/useJoinedRoomId.ts | 8 ++-- src/app/pages/auth/AuthLayout.tsx | 7 ++-- src/app/pages/client/direct/RoomProvider.tsx | 12 +++++- src/app/pages/client/explore/Server.tsx | 5 ++- src/app/pages/client/home/RoomProvider.tsx | 7 +++- src/app/pages/client/space/RoomProvider.tsx | 9 ++-- src/app/pages/client/space/Space.tsx | 2 +- src/app/pages/client/space/SpaceProvider.tsx | 11 ++++- src/app/pages/pathUtils.ts | 43 +++++++++++++------- src/app/plugins/millify.ts | 6 +-- src/app/utils/room.ts | 29 +++++++++---- 16 files changed, 110 insertions(+), 51 deletions(-) diff --git a/src/app/components/BackRouteHandler.tsx b/src/app/components/BackRouteHandler.tsx index fa3d759..a1497b4 100644 --- a/src/app/components/BackRouteHandler.tsx +++ b/src/app/components/BackRouteHandler.tsx @@ -1,6 +1,7 @@ import { ReactNode, useCallback } from 'react'; import { matchPath, useLocation, useNavigate } from 'react-router-dom'; import { + decodeRouteParam, getDirectPath, getExplorePath, getHomePath, @@ -52,7 +53,10 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) { location.pathname ); if (spaceMatch?.params.spaceIdOrAlias) { - navigate(getSpacePath(spaceMatch.params.spaceIdOrAlias)); + const spaceIdOrAlias = decodeRouteParam(spaceMatch.params.spaceIdOrAlias); + if (spaceIdOrAlias) { + navigate(getSpacePath(spaceIdOrAlias)); + } return; } if ( diff --git a/src/app/features/room/Room.tsx b/src/app/features/room/Room.tsx index 20b09dd..27ac886 100644 --- a/src/app/features/room/Room.tsx +++ b/src/app/features/room/Room.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useEffect } from 'react'; import { Box, Line } from 'folds'; +import { decodeRouteParam } from '../../pages/pathUtils'; import { useParams } from 'react-router-dom'; import { isKeyHotkey } from 'is-hotkey'; import { useSetAtom } from 'jotai'; @@ -19,7 +20,8 @@ import { isForum } from '../../utils/room'; import { ForumRoomView } from './ForumRoomView'; export function Room() { - const { eventId } = useParams(); + const { eventId: rawEventId } = useParams(); + const eventId = decodeRouteParam(rawEventId); const room = useRoom(); const mx = useMatrixClient(); const setActiveRoomId = useSetAtom(activeRoomIdAtom); diff --git a/src/app/hooks/router/useExploreSelected.ts b/src/app/hooks/router/useExploreSelected.ts index f0ffdc8..7bb530c 100644 --- a/src/app/hooks/router/useExploreSelected.ts +++ b/src/app/hooks/router/useExploreSelected.ts @@ -1,5 +1,5 @@ import { useMatch, useParams } from 'react-router-dom'; -import { getExploreFeaturedPath, getExplorePath } from '../../pages/pathUtils'; +import { decodeRouteParam, getExploreFeaturedPath, getExplorePath } from '../../pages/pathUtils'; export const useExploreSelected = (): boolean => { const match = useMatch({ @@ -24,5 +24,5 @@ export const useExploreFeaturedSelected = (): boolean => { export const useExploreServer = (): string | undefined => { const { server } = useParams(); - return server; + return decodeRouteParam(server); }; diff --git a/src/app/hooks/router/useSelectedRoom.ts b/src/app/hooks/router/useSelectedRoom.ts index 6d0ee4f..33c6d8e 100644 --- a/src/app/hooks/router/useSelectedRoom.ts +++ b/src/app/hooks/router/useSelectedRoom.ts @@ -1,11 +1,13 @@ import { useParams } from 'react-router-dom'; +import { decodeRouteParam } from '../../pages/pathUtils'; import { getCanonicalAliasRoomId, isRoomAlias } from '../../utils/matrix'; import { useMatrixClient } from '../useMatrixClient'; export const useSelectedRoom = (): string | undefined => { const mx = useMatrixClient(); - const { roomIdOrAlias } = useParams(); + const { roomIdOrAlias: rawRoomIdOrAlias } = useParams(); + const roomIdOrAlias = decodeRouteParam(rawRoomIdOrAlias); const roomId = roomIdOrAlias && isRoomAlias(roomIdOrAlias) ? getCanonicalAliasRoomId(mx, roomIdOrAlias) diff --git a/src/app/hooks/router/useSelectedSpace.ts b/src/app/hooks/router/useSelectedSpace.ts index 056e566..0798a2c 100644 --- a/src/app/hooks/router/useSelectedSpace.ts +++ b/src/app/hooks/router/useSelectedSpace.ts @@ -1,11 +1,13 @@ import { useLocation, useParams } from 'react-router-dom'; +import { decodeRouteParam } from '../../pages/pathUtils'; import { getCanonicalAliasRoomId, isRoomAlias } from '../../utils/matrix'; import { useMatrixClient } from '../useMatrixClient'; export const useSelectedSpace = (): string | undefined => { const mx = useMatrixClient(); - const { spaceIdOrAlias } = useParams(); + const { spaceIdOrAlias: rawSpaceIdOrAlias } = useParams(); + const spaceIdOrAlias = decodeRouteParam(rawSpaceIdOrAlias); const spaceId = spaceIdOrAlias && isRoomAlias(spaceIdOrAlias) diff --git a/src/app/hooks/useJoinedRoomId.ts b/src/app/hooks/useJoinedRoomId.ts index 857defa..0873886 100644 --- a/src/app/hooks/useJoinedRoomId.ts +++ b/src/app/hooks/useJoinedRoomId.ts @@ -1,15 +1,17 @@ import { useMemo } from 'react'; import { useMatrixClient } from './useMatrixClient'; +import { decodeRouteParam } from '../pages/pathUtils'; import { getCanonicalAliasRoomId, isRoomAlias } from '../utils/matrix'; export const useJoinedRoomId = (allRooms: string[], roomIdOrAlias: string): string | undefined => { const mx = useMatrixClient(); const joinedRoomId = useMemo(() => { - const roomId = isRoomAlias(roomIdOrAlias) - ? getCanonicalAliasRoomId(mx, roomIdOrAlias) - : roomIdOrAlias; + const decoded = decodeRouteParam(roomIdOrAlias) ?? roomIdOrAlias; + const roomId = isRoomAlias(decoded) + ? getCanonicalAliasRoomId(mx, decoded) + : decoded; if (roomId && allRooms.includes(roomId)) return roomId; return undefined; diff --git a/src/app/pages/auth/AuthLayout.tsx b/src/app/pages/auth/AuthLayout.tsx index 44cc588..9944a4d 100644 --- a/src/app/pages/auth/AuthLayout.tsx +++ b/src/app/pages/auth/AuthLayout.tsx @@ -96,10 +96,11 @@ export function AuthLayout() { // if server is mismatches with path server, update path useEffect(() => { - if (!urlEncodedServer || tryDecodeURIComponent(urlEncodedServer) !== server) { + const decodedUrlServer = urlEncodedServer ? tryDecodeURIComponent(urlEncodedServer) : undefined; + if (!urlEncodedServer || decodedUrlServer !== server) { navigate( generatePath(currentAuthPath(location.pathname), { - server: encodeURIComponent(server), + server, }) + location.search, { replace: true } ); @@ -114,7 +115,7 @@ export function AuthLayout() { return; } navigate( - generatePath(currentAuthPath(location.pathname), { server: encodeURIComponent(newServer) }) + location.search + generatePath(currentAuthPath(location.pathname), { server: newServer }) + location.search ); }, [navigate, location, discoveryState, server, discoverServer] diff --git a/src/app/pages/client/direct/RoomProvider.tsx b/src/app/pages/client/direct/RoomProvider.tsx index 7c26ec5..275805d 100644 --- a/src/app/pages/client/direct/RoomProvider.tsx +++ b/src/app/pages/client/direct/RoomProvider.tsx @@ -1,5 +1,6 @@ import React, { ReactNode } from 'react'; import { useParams } from 'react-router-dom'; +import { decodeRouteParam } from '../../pathUtils'; import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom'; import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; @@ -10,12 +11,19 @@ export function DirectRouteRoomProvider({ children }: { children: ReactNode }) { const mx = useMatrixClient(); const rooms = useDirectRooms(); - const { roomIdOrAlias, eventId } = useParams(); + const { roomIdOrAlias: rawRoomIdOrAlias, eventId: rawEventId } = useParams(); + const roomIdOrAlias = decodeRouteParam(rawRoomIdOrAlias); + const eventId = decodeRouteParam(rawEventId); const roomId = useSelectedRoom(); const room = mx.getRoom(roomId); if (!room || !rooms.includes(room.roomId)) { - return ; + return ( + + ); } return ( diff --git a/src/app/pages/client/explore/Server.tsx b/src/app/pages/client/explore/Server.tsx index 1d06996..a7a2a1b 100644 --- a/src/app/pages/client/explore/Server.tsx +++ b/src/app/pages/client/explore/Server.tsx @@ -20,7 +20,7 @@ import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { RoomTopicViewer } from '../../../components/room-topic-viewer'; import { RoomCard, RoomCardBase, RoomCardGrid } from '../../../components/room-card'; import { ExploreServerPathSearchParams } from '../../paths'; -import { getExploreServerPath, withSearchParam } from '../../pathUtils'; +import { getExploreServerPath, decodeRouteParam, withSearchParam } from '../../pathUtils'; import * as css from './style.css'; import { allRoomsAtom } from '../../../state/room-list/roomList'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; @@ -324,7 +324,8 @@ function LimitButton({ limit, onLimitChange }: LimitButtonProps) { } export function PublicRooms() { - const { server } = useParams(); + const { server: rawServer } = useParams(); + const server = decodeRouteParam(rawServer); const mx = useMatrixClient(); const userId = mx.getUserId(); const userServer = userId && getMxIdServer(userId); diff --git a/src/app/pages/client/home/RoomProvider.tsx b/src/app/pages/client/home/RoomProvider.tsx index 4e16f79..ebd88c5 100644 --- a/src/app/pages/client/home/RoomProvider.tsx +++ b/src/app/pages/client/home/RoomProvider.tsx @@ -1,5 +1,6 @@ import React, { ReactNode } from 'react'; import { useParams } from 'react-router-dom'; +import { decodeRouteParam } from '../../pathUtils'; import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom'; import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; @@ -11,7 +12,9 @@ export function HomeRouteRoomProvider({ children }: { children: ReactNode }) { const mx = useMatrixClient(); const rooms = useHomeRooms(); - const { roomIdOrAlias, eventId } = useParams(); + const { roomIdOrAlias: rawRoomIdOrAlias, eventId: rawEventId } = useParams(); + const roomIdOrAlias = decodeRouteParam(rawRoomIdOrAlias); + const eventId = decodeRouteParam(rawEventId); const viaServers = useSearchParamsViaServers(); const roomId = useSelectedRoom(); const room = mx.getRoom(roomId); @@ -19,7 +22,7 @@ export function HomeRouteRoomProvider({ children }: { children: ReactNode }) { if (!room || !rooms.includes(room.roomId)) { return ( diff --git a/src/app/pages/client/space/RoomProvider.tsx b/src/app/pages/client/space/RoomProvider.tsx index 61f5a75..088cae3 100644 --- a/src/app/pages/client/space/RoomProvider.tsx +++ b/src/app/pages/client/space/RoomProvider.tsx @@ -1,6 +1,7 @@ import React, { ReactNode } from 'react'; import { useParams } from 'react-router-dom'; import { useAtom, useAtomValue } from 'jotai'; +import { decodeRouteParam } from '../../pathUtils'; import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom'; import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; @@ -22,7 +23,9 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) { const mDirects = useAtomValue(mDirectAtom); const allRooms = useAtomValue(allRoomsAtom); - const { roomIdOrAlias, eventId } = useParams(); + const { roomIdOrAlias: rawRoomIdOrAlias, eventId: rawEventId } = useParams(); + const roomIdOrAlias = decodeRouteParam(rawRoomIdOrAlias); + const eventId = decodeRouteParam(rawEventId); const viaServers = useSearchParamsViaServers(); const roomId = useSelectedRoom(); const room = mx.getRoom(roomId); @@ -31,7 +34,7 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) { // room is not joined return ( @@ -59,7 +62,7 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) { return ( diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index 1c1eabc..71473bf 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -324,7 +324,7 @@ export function Space() { const virtualizer = useVirtualizer({ count: flattenedHierarchy.length, getScrollElement: () => scrollRef.current, - estimateSize: () => 0, + estimateSize: () => 32, overscan: 10, }); diff --git a/src/app/pages/client/space/SpaceProvider.tsx b/src/app/pages/client/space/SpaceProvider.tsx index 2fa6f76..a1724ed 100644 --- a/src/app/pages/client/space/SpaceProvider.tsx +++ b/src/app/pages/client/space/SpaceProvider.tsx @@ -1,5 +1,6 @@ import React, { ReactNode } from 'react'; import { useParams } from 'react-router-dom'; +import { decodeRouteParam } from '../../pathUtils'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useAutoJoinOnSpaceVisit } from '../../../hooks/useAutoJoinSpaceRooms'; import { useSpaces } from '../../../state/hooks/roomList'; @@ -16,7 +17,8 @@ export function RouteSpaceProvider({ children }: RouteSpaceProviderProps) { const mx = useMatrixClient(); const joinedSpaces = useSpaces(mx, allRoomsAtom); - const { spaceIdOrAlias } = useParams(); + const { spaceIdOrAlias: rawSpaceIdOrAlias } = useParams(); + const spaceIdOrAlias = decodeRouteParam(rawSpaceIdOrAlias); const viaServers = useSearchParamsViaServers(); const selectedSpaceId = useSelectedSpace(); @@ -25,7 +27,12 @@ export function RouteSpaceProvider({ children }: RouteSpaceProviderProps) { useAutoJoinOnSpaceVisit(space ?? undefined); if (!space || !joinedSpaces.includes(space.roomId)) { - return ; + return ( + + ); } return ( diff --git a/src/app/pages/pathUtils.ts b/src/app/pages/pathUtils.ts index b1ccfac..b055840 100644 --- a/src/app/pages/pathUtils.ts +++ b/src/app/pages/pathUtils.ts @@ -28,6 +28,7 @@ import { CREATE_PATH, } from './paths'; import { trimLeadingSlash, trimTrailingSlash } from '../utils/common'; +import { tryDecodeURIComponent } from '../utils/dom'; import { HashRouterConfig } from '../hooks/useClientConfig'; import type { MatrixClient } from 'matrix-js-sdk'; import { getCanonicalAliasOrRoomId } from '../utils/matrix'; @@ -46,6 +47,18 @@ export const withSearchParam = >( export const encodeSearchParamValueArray = (ids: string[]): string => ids.join(','); export const decodeSearchParamValueArray = (idsParam: string): string[] => idsParam.split(','); +/** Decode route params (React Router 7 encodes in generatePath — do not pre-encode). */ +export const decodeRouteParam = (param: string | undefined): string | undefined => { + if (!param) return param; + let decoded = param; + let next = tryDecodeURIComponent(decoded); + while (next !== decoded) { + decoded = next; + next = tryDecodeURIComponent(decoded); + } + return decoded; +}; + export const getOriginBaseUrl = (hashRouterConfig?: HashRouterConfig): string => { const baseUrl = `${trimTrailingSlash(window.location.origin)}${import.meta.env.BASE_URL}`; @@ -79,17 +92,17 @@ export const getAppPathFromHref = (baseUrl: string, href: string): string => { export const getRootPath = (): string => ROOT_PATH; export const getLoginPath = (server?: string): string => { - const params = server ? { server: encodeURIComponent(server) } : undefined; + const params = server ? { server } : undefined; return generatePath(LOGIN_PATH, params); }; export const getRegisterPath = (server?: string): string => { - const params = server ? { server: encodeURIComponent(server) } : undefined; + const params = server ? { server } : undefined; return generatePath(REGISTER_PATH, params); }; export const getResetPasswordPath = (server?: string): string => { - const params = server ? { server: encodeURIComponent(server) } : undefined; + const params = server ? { server } : undefined; return generatePath(RESET_PASSWORD_PATH, params); }; @@ -99,8 +112,8 @@ export const getHomeJoinPath = (): string => HOME_JOIN_PATH; export const getHomeSearchPath = (): string => HOME_SEARCH_PATH; export const getHomeRoomPath = (roomIdOrAlias: string, eventId?: string): string => { const params = { - roomIdOrAlias: encodeURIComponent(roomIdOrAlias), - eventId: eventId ? encodeURIComponent(eventId) : null, + roomIdOrAlias, + eventId: eventId ?? null, }; return generatePath(HOME_ROOM_PATH, params); @@ -110,8 +123,8 @@ export const getDirectPath = (): string => DIRECT_PATH; export const getDirectCreatePath = (): string => DIRECT_CREATE_PATH; export const getDirectRoomPath = (roomIdOrAlias: string, eventId?: string): string => { const params = { - roomIdOrAlias: encodeURIComponent(roomIdOrAlias), - eventId: eventId ? encodeURIComponent(eventId) : null, + roomIdOrAlias, + eventId: eventId ?? null, }; return generatePath(DIRECT_ROOM_PATH, params); @@ -121,20 +134,20 @@ export const getDirectInvitesPath = (): string => DIRECT_INVITES_PATH; export const getSpacePath = (spaceIdOrAlias: string): string => { const params = { - spaceIdOrAlias: encodeURIComponent(spaceIdOrAlias), + spaceIdOrAlias, }; return generatePath(SPACE_PATH, params); }; export const getSpaceLobbyPath = (spaceIdOrAlias: string): string => { const params = { - spaceIdOrAlias: encodeURIComponent(spaceIdOrAlias), + spaceIdOrAlias, }; return generatePath(SPACE_LOBBY_PATH, params); }; export const getSpaceFeedPath = (spaceIdOrAlias: string): string => { const params = { - spaceIdOrAlias: encodeURIComponent(spaceIdOrAlias), + spaceIdOrAlias, }; return generatePath(SPACE_FEED_PATH, params); }; @@ -149,7 +162,7 @@ export const getSpaceDefaultPath = (mx: MatrixClient, spaceId: string): string = }; export const getSpaceSearchPath = (spaceIdOrAlias: string): string => { const params = { - spaceIdOrAlias: encodeURIComponent(spaceIdOrAlias), + spaceIdOrAlias, }; return generatePath(SPACE_SEARCH_PATH, params); }; @@ -159,9 +172,9 @@ export const getSpaceRoomPath = ( eventId?: string ): string => { const params = { - spaceIdOrAlias: encodeURIComponent(spaceIdOrAlias), - roomIdOrAlias: encodeURIComponent(roomIdOrAlias), - eventId: eventId ? encodeURIComponent(eventId) : null, + spaceIdOrAlias, + roomIdOrAlias, + eventId: eventId ?? null, }; return generatePath(SPACE_ROOM_PATH, params); @@ -171,7 +184,7 @@ export const getExplorePath = (): string => EXPLORE_PATH; export const getExploreFeaturedPath = (): string => EXPLORE_FEATURED_PATH; export const getExploreServerPath = (server: string): string => { const params = { - server: encodeURIComponent(server), + server, }; return generatePath(EXPLORE_SERVER_PATH, params); }; diff --git a/src/app/plugins/millify.ts b/src/app/plugins/millify.ts index d8608de..f03c22a 100644 --- a/src/app/plugins/millify.ts +++ b/src/app/plugins/millify.ts @@ -1,8 +1,8 @@ -import millifyPlugin from 'millify'; -import { MillifyOptions } from 'millify/dist/options'; +import { millify as millifyFn } from 'millify'; +import type { MillifyOptions } from 'millify/dist/options'; export const millify = (count: number, options?: Partial): string => - millifyPlugin(count, { + millifyFn(count, { precision: 1, locales: [], ...options, diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index c548a8d..e6645bc 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -34,12 +34,20 @@ export const getStateEvent = ( room: Room, eventType: StateEvent, stateKey = '' -): MatrixEvent | undefined => - room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getStateEvents(eventType, stateKey) ?? - undefined; +): MatrixEvent | undefined => { + const fromCurrent = room.currentState?.getStateEvents(eventType, stateKey); + if (fromCurrent) return fromCurrent; + return ( + room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getStateEvents(eventType, stateKey) ?? + undefined + ); +}; -export const getStateEvents = (room: Room, eventType: StateEvent): MatrixEvent[] => - room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getStateEvents(eventType) ?? []; +export const getStateEvents = (room: Room, eventType: StateEvent): MatrixEvent[] => { + const fromCurrent = room.currentState?.getStateEvents(eventType); + if (fromCurrent && fromCurrent.length > 0) return fromCurrent; + return room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getStateEvents(eventType) ?? []; +}; export const getAccountData = ( mx: MatrixClient, @@ -170,10 +178,13 @@ export const isUnsupportedRoom = (room: Room | null): boolean => { }; export function isValidChild(mEvent: MatrixEvent): boolean { - return ( - mEvent.getType() === StateEvent.SpaceChild && - Array.isArray(mEvent.getContent<{ via: string[] }>().via) - ); + if (mEvent.getType() !== StateEvent.SpaceChild) return false; + const stateKey = mEvent.getStateKey(); + if (!stateKey || !stateKey.startsWith('!')) return false; + const { via } = mEvent.getContent<{ via?: string[] }>(); + // via is optional on m.space.child; only reject malformed values + if (via !== undefined && !Array.isArray(via)) return false; + return true; } export const getAllParents = (roomToParents: RoomToParents, roomId: string): Set => {