···11+import { useEffect, useState } from 'react';
22+import { useDidResolution } from './useDidResolution';
13import { usePdsEndpoint } from './usePdsEndpoint';
24import { createAtprotoClient } from '../utils/atproto-client';
33-import { useEffect, useState } from 'react';
4556/**
67 * Shape of the state returned by {@link useLatestRecord}.
···2122/**
2223 * Fetches the most recent record from a collection using `listRecords(limit=1)`.
2324 *
2424- * @param did - DID that owns the collection.
2525+ * @param handleOrDid - Handle or DID that owns the collection.
2526 * @param collection - NSID of the collection to query.
2627 * @returns {LatestRecordState<T>} Object reporting the latest record value, derived rkey, loading status, emptiness, and any error.
2728 */
2828-export function useLatestRecord<T = unknown>(did: string | undefined, collection: string): LatestRecordState<T> {
2929- const { endpoint, error: endpointError } = usePdsEndpoint(did);
3030- const [state, setState] = useState<LatestRecordState<T>>({ loading: !!did, empty: false });
3131- // simple one-shot fetch; no refresh logic
2929+export function useLatestRecord<T = unknown>(handleOrDid: string | undefined, collection: string): LatestRecordState<T> {
3030+ const { did, error: didError, loading: resolvingDid } = useDidResolution(handleOrDid);
3131+ const { endpoint, error: endpointError, loading: resolvingEndpoint } = usePdsEndpoint(did);
3232+ const [state, setState] = useState<LatestRecordState<T>>({ loading: !!handleOrDid, empty: false });
32333334 useEffect(() => {
3435 let cancelled = false;
3535- async function run() {
3636- if (!did || !endpoint) return;
3737- setState(s => ({ ...s, loading: true }));
3636+3737+ const assign = (next: Partial<LatestRecordState<T>>) => {
3838+ if (cancelled) return;
3939+ setState(prev => ({ ...prev, ...next }));
4040+ };
4141+4242+ if (!handleOrDid) {
4343+ assign({ loading: false, record: undefined, rkey: undefined, error: undefined, empty: false });
4444+ return () => { cancelled = true; };
4545+ }
4646+4747+ if (didError) {
4848+ assign({ loading: false, error: didError, empty: false });
4949+ return () => { cancelled = true; };
5050+ }
5151+5252+ if (endpointError) {
5353+ assign({ loading: false, error: endpointError, empty: false });
5454+ return () => { cancelled = true; };
5555+ }
5656+5757+ if (resolvingDid || resolvingEndpoint || !did || !endpoint) {
5858+ assign({ loading: true, error: undefined });
5959+ return () => { cancelled = true; };
6060+ }
6161+6262+ assign({ loading: true, error: undefined, empty: false });
6363+6464+ (async () => {
3865 try {
3966 const { rpc } = await createAtprotoClient({ service: endpoint });
4040- const res = await (rpc as unknown as { get: (nsid: string, opts: { params: Record<string, string | number | boolean> }) => Promise<{ ok: boolean; data: { records: Array<{ uri: string; rkey?: string; value: T }> } }> }).get('com.atproto.repo.listRecords', {
6767+ const res = await (rpc as unknown as {
6868+ get: (
6969+ nsid: string,
7070+ opts: { params: Record<string, string | number | boolean> }
7171+ ) => Promise<{ ok: boolean; data: { records: Array<{ uri: string; rkey?: string; value: T }> } }>;
7272+ }).get('com.atproto.repo.listRecords', {
4173 params: { repo: did, collection, limit: 1, reverse: false }
4274 });
4375 if (!res.ok) throw new Error('Failed to list records');
4476 const list = res.data.records;
4577 if (list.length === 0) {
4646- if (!cancelled) setState({ loading: false, empty: true });
7878+ assign({ loading: false, empty: true, record: undefined, rkey: undefined });
4779 return;
4880 }
4981 const first = list[0];
5050- // derive rkey if not present
5151- let rkey = first.rkey;
5252- if (!rkey && first.uri) {
5353- const parts = first.uri.split('/');
5454- rkey = parts[parts.length - 1];
5555- }
5656- if (!cancelled) setState({ record: first.value, rkey, loading: false, empty: false });
8282+ const derivedRkey = first.rkey ?? extractRkey(first.uri);
8383+ assign({ record: first.value, rkey: derivedRkey, loading: false, empty: false });
5784 } catch (e) {
5858- if (!cancelled) setState({ error: e as Error, loading: false, empty: false });
8585+ assign({ error: e as Error, loading: false, empty: false });
5986 }
6060- }
6161- run();
6262- return () => { cancelled = true; };
6363- }, [did, endpoint, collection]);
8787+ })();
64886565- if (endpointError && !state.error) return { ...state, error: endpointError };
8989+ return () => {
9090+ cancelled = true;
9191+ };
9292+ }, [handleOrDid, did, endpoint, collection, resolvingDid, resolvingEndpoint, didError, endpointError]);
9393+6694 return state;
6795}
9696+9797+function extractRkey(uri: string): string | undefined {
9898+ if (!uri) return undefined;
9999+ const parts = uri.split('/');
100100+ return parts[parts.length - 1];
101101+}
+35-7
lib/hooks/usePaginatedRecords.ts
···11import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
22+import { useDidResolution } from './useDidResolution';
23import { usePdsEndpoint } from './usePdsEndpoint';
34import { createAtprotoClient } from '../utils/atproto-client';
45···2324 * Options accepted by {@link usePaginatedRecords}.
2425 */
2526export interface UsePaginatedRecordsOptions {
2626- /** DID whose repository should be queried. */
2727+ /** DID or handle whose repository should be queried. */
2728 did?: string;
2829 /** NSID collection containing the target records. */
2930 collection: string;
···5859/**
5960 * React hook that fetches a repository collection with cursor-based pagination and prefetching.
6061 *
6161- * @param did - DID whose repository should be queried.
6262+ * @param did - Handle or DID whose repository should be queried.
6263 * @param collection - NSID collection to read from.
6364 * @param limit - Maximum number of records to request per page. Defaults to `5`.
6465 * @returns {UsePaginatedRecordsResult<T>} Object containing the current page, pagination metadata, and navigation callbacks.
6566 */
6666-export function usePaginatedRecords<T>({ did, collection, limit = 5 }: UsePaginatedRecordsOptions): UsePaginatedRecordsResult<T> {
6767- const { endpoint, error: endpointError } = usePdsEndpoint(did);
6767+export function usePaginatedRecords<T>({ did: handleOrDid, collection, limit = 5 }: UsePaginatedRecordsOptions): UsePaginatedRecordsResult<T> {
6868+ const { did, error: didError, loading: resolvingDid } = useDidResolution(handleOrDid);
6969+ const { endpoint, error: endpointError, loading: resolvingEndpoint } = usePdsEndpoint(did);
6870 const [pages, setPages] = useState<PageData<T>[]>([]);
6971 const [pageIndex, setPageIndex] = useState(0);
7072 const [loading, setLoading] = useState(false);
···126128 }, [did, endpoint, collection, limit]);
127129128130 useEffect(() => {
131131+ if (!handleOrDid) {
132132+ resetState();
133133+ setLoading(false);
134134+ setError(undefined);
135135+ return;
136136+ }
137137+138138+ if (didError) {
139139+ resetState();
140140+ setLoading(false);
141141+ setError(didError);
142142+ return;
143143+ }
144144+145145+ if (endpointError) {
146146+ resetState();
147147+ setLoading(false);
148148+ setError(endpointError);
149149+ return;
150150+ }
151151+152152+ if (resolvingDid || resolvingEndpoint || !did || !endpoint) {
153153+ setLoading(true);
154154+ setError(undefined);
155155+ return;
156156+ }
157157+129158 resetState();
130130- if (!did || !endpoint) return;
131159 fetchPage(undefined, 0, 'active').catch(() => {
132160 /* error handled in state */
133161 });
134134- }, [did, endpoint, fetchPage, resetState]);
162162+ }, [handleOrDid, did, endpoint, fetchPage, resetState, resolvingDid, resolvingEndpoint, didError, endpointError]);
135163136164 const currentPage = pages[pageIndex];
137165 const hasNext = !!currentPage?.cursor || !!pages[pageIndex + 1];
···156184157185 const records = useMemo(() => currentPage?.records ?? [], [currentPage]);
158186159159- const effectiveError = error ?? (endpointError as Error | undefined);
187187+ const effectiveError = error ?? (endpointError as Error | undefined) ?? (didError as Error | undefined);
160188161189 useEffect(() => {
162190 const cursor = pages[pageIndex]?.cursor;
-1
lib/index.ts
···1919export * from './hooks/useBlob';
2020export * from './hooks/useBlueskyProfile';
2121export * from './hooks/useColorScheme';
2222-export * from './hooks/useDidHandle';
2322export * from './hooks/useDidResolution';
2423export * from './hooks/useLatestRecord';
2524export * from './hooks/usePaginatedRecords';
+2-2
lib/renderers/BlueskyPostRenderer.tsx
···22import type { FeedPostRecord } from '../types/bluesky';
33import { useColorScheme, type ColorSchemePreference } from '../hooks/useColorScheme';
44import { parseAtUri, toBlueskyPostUrl, formatDidForLabel, type ParsedAtUri } from '../utils/at-uri';
55-import { useDidHandle } from '../hooks/useDidHandle';
55+import { useDidResolution } from '../hooks/useDidResolution';
66import { useBlob } from '../hooks/useBlob';
77import { BlueskyIcon } from '../components/BlueskyIcon';
88···2626 const scheme = useColorScheme(colorScheme);
2727 const replyParentUri = record.reply?.parent?.uri;
2828 const replyTarget = replyParentUri ? parseAtUri(replyParentUri) : undefined;
2929- const { handle: parentHandle, loading: parentHandleLoading } = useDidHandle(replyTarget?.did);
2929+ const { handle: parentHandle, loading: parentHandleLoading } = useDidResolution(replyTarget?.did);
30303131 if (error) return <div style={{ padding: 8, color: 'crimson' }}>Failed to load post.</div>;
3232 if (loading && !record) return <div style={{ padding: 8 }}>Loading…</div>;