import * as ATPAPI from "@atproto/api"; import { isContentLabelPref, isLabelersPref } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; import { useQueries, useQueryClient } from "@tanstack/react-query"; import { useAtom } from "jotai"; import { Dialog } from "radix-ui"; import React, { createContext, useContext, useMemo } from "react"; import { FORCED_LABELER_DIDS } from "~/../policy"; import { useAuth } from "~/providers/UnifiedAuthProvider"; import { NotificationItem } from "~/routes/notifications"; import { disabledLabelersAtom, slingshotURLAtom } from "~/utils/atoms"; import { useQueryIdentity, useQueryPreferences } from "~/utils/useQuery"; // higher prio overwrites lower prio export const LABEL_PRIO_DEFAULT = 0 export const LABEL_PRIO_USER = 5 // export const LABEL_PRIO_FORCED = 20 // Not used in pref merging export interface HydratedLabelValueDefinition extends ATPAPI.ComAtprotoLabelDefs.LabelValueDefinition { pref: ATPAPI.ComAtprotoLabelDefs.LabelValueDefinition["defaultSetting"] } export type labelpref = { did: string, label: string, visibility: ATPAPI.AppBskyActorDefs.ContentLabelPref["visibility"], priority: number } type labeldefpref = { did: string, labeldefs?: ATPAPI.ComAtprotoLabelDefs.LabelValueDefinition[], err?: string } type hydratedlabeldefpref = { did: string, labeldefs?: HydratedLabelValueDefinition[], err?: string } type AutoLabelConfig = { activeLabelerDids: string[]; mergedPrefContentLabels: labelpref[]; hydratedLabelDefs: Map; isLoading: boolean; isError: boolean; sendError: (error: LabelerError) => void; }; export type LabelerError = { did: string; message: string }; const AutoLabelContext = createContext(undefined); export function useAutoLabelConfig(): AutoLabelConfig { const context = useContext(AutoLabelContext); if (!context) { throw new Error('useAutoLabelConfig must be used within an AutoLabelProvider'); } return context; } export function AutoLabelProvider({ children }: { children: React.ReactNode }) { const { agent, status } = useAuth(); const [slingshoturl] = useAtom(slingshotURLAtom); const queryClient = useQueryClient(); const [disabledLabelers, setDisabledLabelers] = useAtom(disabledLabelersAtom); // Modal state const [errorModalOpen, setErrorModalOpen] = React.useState(false); const [labelerErrors, setLabelerErrors] = React.useState([]); const erroredLabelerDids = new Set( labelerErrors.map(e => e.did) ); const sendError = React.useCallback((error: LabelerError) => { setLabelerErrors((prev) => { // Avoid duplicates if (prev.find(e => e.did === error.did)) return prev; return [...prev, error]; }); setErrorModalOpen(true); }, []); const isUnauthed = status === "signedOut" || !agent; // 1. Get User Identity & Preferences const { data: identity } = useQueryIdentity(agent?.did); const { data: prefs, isLoading: prefsLoading, isError: prefsError } = useQueryPreferences({ agent: agent ?? undefined, pdsUrl: identity?.pds, }); // 2. Identify Labeler DIDs & User Preferences (Use useMemo for stability) const userPrefLabelerDids = useMemo(() => prefs?.preferences?.find(isLabelersPref)?.labelers.map(l => l.did) ?? [] , [prefs]); const userPrefContentLabelsRaw = useMemo(() => prefs?.preferences?.filter(isContentLabelPref) ?? [] , [prefs]); const userPrefContentLabels: labelpref[] = useMemo(() => userPrefContentLabelsRaw.map((pref) => { const res: labelpref = { did: pref.labelerDid || "global", label: pref.label, visibility: pref.visibility, priority: LABEL_PRIO_USER } return res }), [userPrefContentLabelsRaw]); // 3. Force Bsky DID + User DIDs const userPrefLabelerDidsFiltered = React.useMemo( () => userPrefLabelerDids.filter(did => !disabledLabelers.includes(did)), [userPrefLabelerDids, disabledLabelers] ); const activeLabelerDids = useMemo(() => Array.from( new Set([...FORCED_LABELER_DIDS, ...userPrefLabelerDidsFiltered]) ), [userPrefLabelerDidsFiltered]); // 4. Parallel fetch Service Records (The expensive part) const labelerServiceQueries = useQueries({ queries: activeLabelerDids.map((did: string) => ({ queryKey: ["labelerServiceDefaultstestwhatever", did], queryFn: async () => { const host = slingshoturl; const response = await fetch( `https://${host}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=app.bsky.labeler.service&rkey=self`, ); if (!response.ok) throw new Error("Failed to fetch labeler service"); try { const res = await response.json() as { uri: string; cid: string; value: ATPAPI.AppBskyLabelerService.Record; }; const labeldefs = res.value.policies.labelValueDefinitions return { did: did, labeldefs: labeldefs } as labeldefpref } catch { sendError({ did, message: ".json()" }); return { did: did, err: ".json()" } as labeldefpref } }, staleTime: 1000 * 60 * 60, // Cache for 1 hour })), }); const defaultPrefContentLabels: labelpref[] = useMemo(() => labelerServiceQueries.flatMap(labeler => { if (labeler.error || labeler.data?.err || !labeler.data) { return [] } const labelerDid = labeler.data.did return labeler.data.labeldefs?.map(label => ({ did: labelerDid, label: label.identifier, visibility: label.defaultSetting, priority: LABEL_PRIO_DEFAULT, })) ?? [] }) , [labelerServiceQueries]); // 5. Merge Default and User Preferences const mergedPrefContentLabels: labelpref[] = useMemo(() => { const allrawPrefContentLabels = [...defaultPrefContentLabels, ...userPrefContentLabels] return Object.values( allrawPrefContentLabels.reduce( (acc, pref) => { const key = `${pref.did}::${pref.label}` const existing = acc[key] if (!existing || pref.priority > existing.priority) { acc[key] = pref } return acc }, {} as Record ) ) }, [defaultPrefContentLabels, userPrefContentLabels]); const hydratedLabelDefs = convertLabelDefsToMap( labelerServiceQueries .map(item => item.data) .filter((data): data is labeldefpref => data !== undefined), mergedPrefContentLabels ) const labelerServiceLoading = labelerServiceQueries.some(q => q.isLoading); const labelerServiceError = labelerServiceQueries.some(q => q.isError); const isLoading = prefsLoading || labelerServiceLoading; const isError = (!isUnauthed && prefsError) || labelerServiceError; const value = useMemo(() => ({ activeLabelerDids, mergedPrefContentLabels, hydratedLabelDefs, isLoading, isError, sendError }), [activeLabelerDids, mergedPrefContentLabels, hydratedLabelDefs, isLoading, isError, sendError]); // The provider must wrap the application root where useAutoLabels is called return ( <> {children} {errorModalOpen && (
Labeler Errors These labelers have returned an error.
You can disable them to continue using the app.
{labelerErrors.map(e => ( //
  • // {e.did}: {e.message} // //
  • ))}
    {/* */}
    )} ); } function convertLabelDefsToMap( labelDefPrefs: labeldefpref[], mergedPrefContentLabels: labelpref[] ): Map { const labelMap = new Map(); for (const pref of labelDefPrefs) { const did = pref.did; // Only process if labeldefs array exists if (pref.labeldefs && pref.labeldefs.length > 0) { for (const def of pref.labeldefs) { // Construct the key: did + "::" + identifier const key = `${did}::${def.identifier}`; const prefdlabelvis = mergedPrefContentLabels.find(i => i.did === did && i.label === def.identifier)?.visibility const hydrateddef: HydratedLabelValueDefinition = { ...def, pref: collapsePrefs(prefdlabelvis ? prefdlabelvis : def.defaultSetting) }; // Set the key and the LabelValueDefinition as the value labelMap.set(key, hydrateddef); } } } return labelMap; } function collapsePrefs(def: "ignore" | "warn" | "hide" | "show" | (string & {})): "ignore" | "warn" | "hide" { if (def === "ignore") return "ignore" if (def === "warn") return "warn" if (def === "hide") return "hide" if (def === "show") return "ignore" if (def === "alert") return "warn" if (def === "inform") return "warn" if (def === "none") return "ignore" return "ignore" }