Two review agents found the preview card degrades to 'id + logo + Join' for real
reasons. Fixes:
- **via was dropped at the summary fetch** (root cause). RoomSummaryLoader now
accepts + forwards `via` to getRoomSummary (and keys the query on it);
JoinBeforeNavigate passes viaServers; the lobby Preview chip carries data-via and
Lobby.handleOpenRoom appends it to the navigated URL. Without this, previews of
rooms the HS isn't already in came back sparse/404.
- **No loading/error state** -> RoomSummaryLoader now surfaces {loading,error};
JoinBeforeNavigate shows a Spinner while loading instead of the degraded card.
- **Room id leaked as name AND topic AND header** -> stop using the raw `!id` as
the topic fallback (show 'No description'); name falls back to canonical_alias
then alias-localpart; the page header shows the summary name.
- **Richer, membership-aware card**: forward canonical_alias (shown under the name),
world_readable ('Readable' badge), and membership -> the button now shows Accept
invite / Requested / Banned correctly instead of a Join that lies; a 'hero' layout
for the single-preview page (primary Join button, unclamped topic). Removed dead
`|| undefined ||`.
IRoomSummary re-adds canonical_alias (the SDK type Omit<>s it though MSC3266
returns it). 738 tests pass, build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
109 lines
3.0 KiB
TypeScript
109 lines
3.0 KiB
TypeScript
import { ReactNode, useCallback, useState } from 'react';
|
|
import { MatrixClient, Room } from 'matrix-js-sdk';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
|
|
import { useMatrixClient } from '../hooks/useMatrixClient';
|
|
import { LocalRoomSummary, useLocalRoomSummary } from '../hooks/useLocalRoomSummary';
|
|
import { AsyncState, AsyncStatus } from '../hooks/useAsyncCallback';
|
|
|
|
// MSC3266 returns canonical_alias at runtime, but the SDK's RoomSummary type
|
|
// Omit<>s it — add it back so callers can use it.
|
|
export type IRoomSummary = Awaited<ReturnType<MatrixClient['getRoomSummary']>> & {
|
|
canonical_alias?: string;
|
|
};
|
|
|
|
export type RoomSummaryState = { loading: boolean; error: unknown };
|
|
|
|
type RoomSummaryLoaderProps = {
|
|
roomIdOrAlias: string;
|
|
/**
|
|
* Resident servers to route the summary request through. REQUIRED for a room
|
|
* the local homeserver isn't already in (the exact case a preview exists for) —
|
|
* without it the summary comes back sparse or 404s.
|
|
*/
|
|
via?: string[];
|
|
children: (roomSummary: IRoomSummary | undefined, state: RoomSummaryState) => ReactNode;
|
|
};
|
|
|
|
export function RoomSummaryLoader({ roomIdOrAlias, via, children }: RoomSummaryLoaderProps) {
|
|
const mx = useMatrixClient();
|
|
|
|
const fetchSummary = useCallback(
|
|
() => mx.getRoomSummary(roomIdOrAlias, via),
|
|
[mx, roomIdOrAlias, via],
|
|
);
|
|
|
|
const { data, isLoading, error } = useQuery({
|
|
// `via` is part of the key so a no-via failure isn't reused once we have servers.
|
|
queryKey: [roomIdOrAlias, 'summary', via ?? []],
|
|
queryFn: fetchSummary,
|
|
retry: 1,
|
|
staleTime: 5 * 60 * 1000,
|
|
});
|
|
|
|
return children(data, { loading: isLoading, error });
|
|
}
|
|
|
|
export function LocalRoomSummaryLoader({
|
|
room,
|
|
children,
|
|
}: {
|
|
room: Room;
|
|
children: (roomSummary: LocalRoomSummary) => ReactNode;
|
|
}) {
|
|
const summary = useLocalRoomSummary(room);
|
|
|
|
return children(summary);
|
|
}
|
|
|
|
export function HierarchyRoomSummaryLoader({
|
|
roomId,
|
|
children,
|
|
}: {
|
|
roomId: string;
|
|
children: (state: AsyncState<IHierarchyRoom, Error>) => ReactNode;
|
|
}) {
|
|
const mx = useMatrixClient();
|
|
|
|
const fetchSummary = useCallback(() => mx.getRoomHierarchy(roomId, 1, 1), [mx, roomId]);
|
|
const [errorMemo, setError] = useState<Error>();
|
|
|
|
const { data, error } = useQuery({
|
|
queryKey: [roomId, `hierarchy`],
|
|
queryFn: fetchSummary,
|
|
retryOnMount: false,
|
|
refetchOnWindowFocus: false,
|
|
retry: (failureCount, err) => {
|
|
setError(err);
|
|
if (failureCount > 3) return false;
|
|
return true;
|
|
},
|
|
});
|
|
|
|
let state: AsyncState<IHierarchyRoom, Error> = {
|
|
status: AsyncStatus.Loading,
|
|
};
|
|
if (error) {
|
|
state = {
|
|
status: AsyncStatus.Error,
|
|
error,
|
|
};
|
|
}
|
|
if (errorMemo) {
|
|
state = {
|
|
status: AsyncStatus.Error,
|
|
error: errorMemo,
|
|
};
|
|
}
|
|
|
|
const summary = data?.rooms[0] ?? undefined;
|
|
if (summary) {
|
|
state = {
|
|
status: AsyncStatus.Success,
|
|
data: summary,
|
|
};
|
|
}
|
|
|
|
return children(state);
|
|
}
|