import { createContext, type ReactNode, useContext, useEffect, useState } from 'react' import { messageOf } from '../components/useAction.ts' import type { StoreClient } from './client.ts' import type { Conversation, ConversationSummary } from './core.ts' import { isStoreClosed } from './errors.ts' import type { HandoverState } from './handover.ts' import type { SearchResult } from './search.ts' const StoreContext = createContext(null) export function StoreProvider({ store, children }: { store: StoreClient; children: ReactNode }) { return {children} } export function useStore(): StoreClient { const store = useContext(StoreContext) if (!store) throw new Error('useStore needs a StoreProvider') return store } export function useStoreState(): HandoverState { const store = useStore() const [state, setState] = useState(store.state()) useEffect(() => store.onState(setState), [store]) return state } /** Report store failures, except for the store moving to another tab. */ function reportUnless(setError: (message: string) => void) { return (err: unknown) => { if (isStoreClosed(err)) return console.error(err) setError(messageOf(err)) } } export function useConversations(): { conversations: ConversationSummary[]; error: string | null } { const store = useStore() const state = useStoreState() const [conversations, setConversations] = useState([]) const [error, setError] = useState(null) useEffect(() => { if (state !== 'active') return const load = () => void store.worker.listConversations().then(setConversations, reportUnless(setError)) load() return store.onChange((change) => change.type === 'index' && load()) }, [store, state]) return { conversations, error } } /** A conversation from the local copy, shown at once and refreshed from the PDS when opened. */ export function useConversation(skey: string): { conversation: Conversation | null error: string | null } { const store = useStore() const state = useStoreState() const [conversation, setConversation] = useState(null) const [error, setError] = useState(null) useEffect(() => { if (state !== 'active') return const report = reportUnless(setError) const load = () => void store.worker.getConversation(skey).then(setConversation, report) load() store.worker .refreshConversation(skey) .then(() => store.worker.reconcileConversation(skey)) .catch(report) return store.onChange( (change) => change.type === 'conversation' && change.skey === skey && load(), ) }, [store, state, skey]) return { conversation, error } } /** Search the local copy, rerunning as the download and live changes add to it. */ export function useSearch(query: string): { results: SearchResult[] remaining: number error: string | null } { const store = useStore() const state = useStoreState() const [found, setFound] = useState<{ results: SearchResult[]; remaining: number }>({ results: [], remaining: 0, }) const [error, setError] = useState(null) useEffect(() => { if (state !== 'active' || !query.trim()) return let current = true const report = reportUnless(setError) const load = () => void Promise.all([store.worker.search(query), store.worker.remainingDownloads()]).then( ([results, remaining]) => { if (current) setFound({ results, remaining }) }, report, ) load() const stop = store.onChange(load) return () => { current = false stop() } }, [store, state, query]) return { ...found, error } }