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 => {