Something went wrong. Try again.
ATProto-based AI Assistant Chat interface.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110import { 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<StoreClient | null>(null)
export function StoreProvider({ store, children }: { store: StoreClient; children: ReactNode }) { return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>}
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<ConversationSummary[]>([]) const [error, setError] = useState<string | null>(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<Conversation | null>(null) const [error, setError] = useState<string | null>(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<string | null>(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 }}