an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app
93
fork

Configure Feed

Select the types of activity you want to include in your feed.

host-configurable moderation policy

+151 -61
+17
policy.ts
··· 1 + export const FORCED_LABELER_DIDS = [ 2 + "did:plc:ar7c4by46qjdydhdevvrndac" // bluesky moderation 3 + ]; 4 + 5 + export const UNAUTHED_FORCE_WARN_LABELS = new Set([ 6 + // i dont know if some of these are even valid labels 7 + "porn", 8 + "sexual", 9 + "graphic-media", 10 + "nudity", 11 + "nsfl", 12 + "corpse", 13 + "gore", 14 + "!no-unauthenticated" 15 + ]); 16 + 17 + export const UNAUTHED_PREVENT_OPENING_WARNS = true;
+113 -56
src/components/ModerationInitializer.tsx
··· 1 1 import { useQueries } from "@tanstack/react-query"; 2 - import { useSetAtom } from "jotai"; 3 - import { useEffect } from "react"; 2 + import { useAtom, useSetAtom } from "jotai"; 3 + import { useEffect, useRef } from "react"; 4 4 5 + import { FORCED_LABELER_DIDS, UNAUTHED_FORCE_WARN_LABELS } from "~/../policy"; 5 6 import { useAuth } from "~/providers/UnifiedAuthProvider"; 6 7 import { labelerConfigAtom } from "~/state/moderationAtoms"; 7 8 import type { LabelerDefinition, LabelPreference, LabelValueDefinition } from "~/types/moderation"; 9 + import { slingshotURLAtom } from "~/utils/atoms"; 8 10 import { useQueryIdentity } from "~/utils/useQuery"; 9 11 import { useQueryPreferences } from "~/utils/useQuery"; 10 12 11 - export const BSKY_LABELER_DID = "did:plc:ar7c4by46qjdydhdevvrndac"; 12 - 13 13 // Manual DID document resolution 14 14 const fetchDidDocument = async (did: string): Promise<any> => { 15 15 if (did.startsWith("did:plc:")) { 16 - // For PLC DIDs, fetch from plc.directory 17 16 const response = await fetch( 18 17 `https://plc.directory/${encodeURIComponent(did)}`, 19 18 ); ··· 21 20 throw new Error(`Failed to fetch PLC DID document for ${did}`); 22 21 return response.json(); 23 22 } else if (did.startsWith("did:web:")) { 24 - // For web DIDs, fetch from well-known 25 23 const handle = did.replace("did:web:", ""); 26 24 const url = `https://${handle}/.well-known/did.json`; 27 25 const response = await fetch(url); ··· 36 34 }; 37 35 38 36 export const ModerationInitializer = () => { 39 - const { agent } = useAuth(); 37 + const { agent, status } = useAuth(); 40 38 const setLabelerConfig = useSetAtom(labelerConfigAtom); 39 + const [slingshoturl] = useAtom(slingshotURLAtom); 41 40 42 - // 1. Get User Identity to get PDS URL 41 + // Define clear boolean for mode 42 + const isUnauthed = status === "signedOut" || !agent; 43 + 44 + // Track previous status to detect transitions 45 + const prevStatusRef = useRef(status); 46 + 47 + // --- 1. THE HARD FLUSH --- 48 + // When Auth Status changes (Logged In <-> Logged Out), immediately wipe the config. 49 + // This prevents "Authed" prefs from bleeding into "Unauthed" state and vice versa 50 + // while the async queries are spinning up. 51 + useEffect(() => { 52 + if (prevStatusRef.current !== status) { 53 + console.log(`[Moderation] Auth status changed (${prevStatusRef.current} -> ${status}). Flushing config.`); 54 + setLabelerConfig([]); // <--- WIPE CLEAN 55 + prevStatusRef.current = status; 56 + } 57 + }, [status, setLabelerConfig]); 58 + 59 + // 2. Get User Identity (Only if authed) 43 60 const { data: identity } = useQueryIdentity(agent?.did); 44 61 45 - // 2. Get User Preferences (Global: "porn" -> "hide") 62 + // 3. Get User Preferences (Only if authed) 46 63 const { data: prefs } = useQueryPreferences({ 47 64 agent: agent ?? undefined, 48 65 pdsUrl: identity?.pds, 49 66 }); 50 67 51 - // 3. Identify Labeler DIDs from prefs 52 - const userPrefDids = 53 - prefs?.preferences 54 - ?.find((pref: any) => pref.$type === "app.bsky.actor.defs#labelersPref") 55 - ?.labelers?.map((l: any) => l.did) ?? []; 68 + // 4. Identify Labeler DIDs 69 + // Important: If unauthed, userPrefDids MUST be empty, even if cache exists. 70 + const userPrefDids = !isUnauthed 71 + ? prefs?.preferences 72 + ?.find((pref: any) => pref.$type === "app.bsky.actor.defs#labelersPref") 73 + ?.labelers?.map((l: any) => l.did) ?? [] 74 + : []; 56 75 57 - // 2. MERGE: Force Bsky DID + User DIDs (Set removes duplicates) 76 + // 5. Force Bsky DID + User DIDs 58 77 const activeLabelerDids = Array.from( 59 - new Set([BSKY_LABELER_DID, ...userPrefDids]) 78 + new Set([...FORCED_LABELER_DIDS, ...userPrefDids]) 60 79 ); 61 80 62 - // 4. Parallel fetch all Labeler DID Documents and Service Records 81 + // 6. Parallel fetch DID Docs 63 82 const labelerDidDocQueries = useQueries({ 64 83 queries: activeLabelerDids.map((did: string) => ({ 65 84 queryKey: ["labelerDidDoc", did], 66 85 queryFn: () => fetchDidDocument(did), 67 - staleTime: 5 * 60 * 1000, // 5 minutes 68 - retry: 1, // Only retry once for DID docs 86 + staleTime: 1000 * 60 * 60 * 24, 69 87 })), 70 88 }); 71 89 90 + // 7. Parallel fetch Service Records 72 91 const labelerServiceQueries = useQueries({ 73 92 queries: activeLabelerDids.map((did: string) => ({ 74 93 queryKey: ["labelerService", did], 75 94 queryFn: async () => { 76 - if (!identity?.pds) throw new Error("No PDS URL"); 95 + const host = slingshoturl || "public.api.bsky.app"; 77 96 const response = await fetch( 78 - `${identity.pds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent("app.bsky.labeler.service")}&rkey=self`, 97 + `https://${host}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent("app.bsky.labeler.service")}&rkey=self`, 79 98 ); 80 99 if (!response.ok) throw new Error("Failed to fetch labeler service"); 81 100 return response.json(); 82 101 }, 83 - enabled: !!identity?.pds && !!agent, 84 - staleTime: 5 * 60 * 1000, // 5 minutes 102 + staleTime: 1000 * 60 * 60, 85 103 })), 86 104 }); 87 105 88 106 useEffect(() => { 107 + // Guard: Wait for queries 89 108 if ( 90 - !prefs || 91 109 labelerDidDocQueries.some((q) => q.isLoading) || 92 - labelerDidDocQueries.some((q) => q.isFetching) || 93 - labelerServiceQueries.some((q) => q.isLoading) || 94 - labelerServiceQueries.some((q) => q.isFetching) 95 - ) 110 + labelerServiceQueries.some((q) => q.isLoading) 111 + ) { 96 112 return; 113 + } 97 114 98 - // Extract content label preferences 99 - const contentLabelPrefs = 100 - prefs.preferences?.filter( 101 - (pref: any) => pref.$type === "app.bsky.actor.defs#contentLabelPref", 102 - ) ?? []; 115 + // Guard: If we are supposed to be Authed, but prefs haven't loaded yet, 116 + // DO NOT run the logic. Wait. This prevents falling back to defaults temporarily. 117 + if (!isUnauthed && !prefs) { 118 + return; 119 + } 103 120 121 + // A. Extract User Global Overrides 122 + // STRICT SEPARATION: If unauthed, force this to be empty to ensure no leakage. 104 123 const globalPrefs: Record<string, LabelPreference> = {}; 105 - contentLabelPrefs.forEach((pref: any) => { 106 - globalPrefs[pref.label] = pref.visibility as LabelPreference; 107 - }); 124 + 125 + if (!isUnauthed && prefs?.preferences) { 126 + const contentLabelPrefs = prefs.preferences.filter( 127 + (pref: any) => pref.$type === "app.bsky.actor.defs#contentLabelPref", 128 + ); 129 + contentLabelPrefs.forEach((pref: any) => { 130 + globalPrefs[pref.label] = pref.visibility as LabelPreference; 131 + }); 132 + } 108 133 109 134 const definitions: LabelerDefinition[] = activeLabelerDids 110 135 .map((did: string, index: number) => { ··· 113 138 114 139 if (!didDocQuery.data || !serviceQuery.data) return null; 115 140 116 - // Extract service endpoint from DID document 117 141 const didDoc = didDocQuery.data as any; 118 142 const atprotoLabelerService = didDoc?.service?.find( 119 143 (s: any) => s.id === "#atproto_labeler", 120 144 ); 121 145 122 - const record = (serviceQuery.data as any).value; // The raw ATProto record 146 + const record = (serviceQuery.data as any).value; 123 147 124 - // 1. Create the Metadata Map 148 + // B. Gather ALL identifiers 149 + const allIdentifiers = new Set<string>(); 150 + record.policies?.labelValues?.forEach((val: string) => allIdentifiers.add(val)); 151 + record.policies?.labelValueDefinitions?.forEach((def: any) => allIdentifiers.add(def.identifier)); 152 + 153 + // C. Create Metadata Map 125 154 const labelDefs: Record<string, LabelValueDefinition> = {}; 126 - 127 155 if (record.policies.labelValueDefinitions) { 128 156 record.policies.labelValueDefinitions.forEach((def: any) => { 129 157 labelDefs[def.identifier] = { ··· 132 160 blurs: def.blurs, 133 161 adultOnly: def.adultOnly, 134 162 defaultSetting: def.defaultSetting, 135 - locales: def.locales || [] // <--- Capture the locales array 163 + locales: def.locales || [] 136 164 }; 137 165 }); 138 166 } 139 167 140 - // RESOLUTION LOGIC: 141 - // Map record.policies.labelValueDefinitions to a lookup map. 142 - // Priority: User Global Pref > Labeler Default > 'ignore' 168 + // D. Resolve Preferences 143 169 const supportedLabels: Record<string, LabelPreference> = {}; 144 170 145 - record.policies?.labelValues?.forEach((val: string) => { 146 - // Does user have a global override for this string? 171 + allIdentifiers.forEach((val) => { 172 + // todo this works but with how useModeration hooks works right now old verdicts wont get stale-d 173 + // it only works right now because these are warns and warns are negligable i guess 174 + // --- BRANCH 1: UNAUTHED MODE --- 175 + if (isUnauthed) { 176 + // 1. Strict Force Overrides 177 + if (UNAUTHED_FORCE_WARN_LABELS.has(val)) { 178 + supportedLabels[val] = "warn"; // or 'hide' if that's what your policy constant implies 179 + return; 180 + } 181 + 182 + // 2. Default Labeler Settings 183 + const def = labelDefs[val]; 184 + const rawDefault = def?.defaultSetting || "ignore"; 185 + 186 + // 3. Apply Unauthed-Specific Aliasing (Optional) 187 + // e.g., if you want to hide 'inform' labels for unauthed users 188 + supportedLabels[val] = rawDefault as LabelPreference; 189 + return; 190 + } 191 + 192 + // --- BRANCH 2: AUTHED MODE --- 193 + // 1. User Global Override (Highest Priority) 147 194 const globalPref = globalPrefs[val]; 148 - // Or use labeler default 149 - const defaultPref = 150 - record.policies?.labelValueDefinitions?.find( 151 - (d: any) => d.identifier === val, 152 - )?.defaultSetting || "ignore"; 195 + if (globalPref) { 196 + supportedLabels[val] = globalPref; 197 + return; 198 + } 153 199 154 - supportedLabels[val] = (globalPref || defaultPref) as LabelPreference; 200 + // 2. Labeler Default 201 + const def = labelDefs[val]; 202 + const rawDefault = def?.defaultSetting || "ignore"; 203 + 204 + supportedLabels[val] = rawDefault as LabelPreference; 155 205 }); 156 206 157 207 return { 158 208 did: did, 159 209 url: atprotoLabelerService?.serviceEndpoint || record.serviceEndpoint, 160 - isDefault: false, // logic to determine if this is a default Bluesky labeler 210 + isDefault: FORCED_LABELER_DIDS.includes(did), 161 211 supportedLabels, 162 212 labelDefs, 163 213 }; ··· 165 215 .filter(Boolean) as LabelerDefinition[]; 166 216 167 217 setLabelerConfig(definitions); 168 - }, [prefs, labelerDidDocQueries, labelerServiceQueries, setLabelerConfig, identity?.pds, activeLabelerDids]); 218 + }, [ 219 + prefs, 220 + labelerDidDocQueries, 221 + labelerServiceQueries, 222 + setLabelerConfig, 223 + activeLabelerDids, 224 + isUnauthed // <--- Critical dependency triggers re-eval on login/out 225 + ]); 169 226 170 - return null; // Headless component 171 - }; 227 + return null; 228 + };
+21 -5
src/components/UniversalPostRenderer.tsx
··· 15 15 import * as React from "react"; 16 16 import { useEffect, useState } from "react"; 17 17 18 + import { UNAUTHED_PREVENT_OPENING_WARNS } from "~/../policy"; 18 19 import defaultpfp from "~/../public/defaultpfp.png"; 19 20 import { useLabelInfo } from "~/hooks/useLabelInfo"; 20 21 import { useModeration } from "~/hooks/useModeration"; ··· 599 600 post.viewer?.repost ? true : false, 600 601 ); 601 602 const [, setComposerPost] = useAtom(composerAtom); 602 - const { agent } = useAuth(); 603 + const { agent, status } = useAuth(); 603 604 const [retweetUri, setRetweetUri] = useState<string | undefined>( 604 605 post.viewer?.repost, 605 606 ); ··· 676 677 const isMainItem = false; 677 678 const setMainItem = (any: any) => { }; 678 679 680 + const hideWarnsWhenUnauthed = UNAUTHED_PREVENT_OPENING_WARNS && status === "signedOut"; 681 + 679 682 const showContentWarning = warnContentLabels.length > 0; 680 683 681 684 const [isOpen, setIsOpen] = useState(!showContentWarning); 685 + const [hasUserTouchedToggleYet, setHasUserTouchedToggleYet] = useState(false); 686 + 687 + useEffect(()=>{ 688 + if(!hasUserTouchedToggleYet && showContentWarning) { 689 + setIsOpen(false); 690 + } 691 + },[hasUserTouchedToggleYet, showContentWarning]) 682 692 683 693 684 694 if (hideAuthorLabels.length > 0 || hideContentLabels.length > 0) { ··· 977 987 {/* <ModerationInner subject={post.uri} /> */} 978 988 {showContentWarning && ( 979 989 <ContentWarning 990 + unauthedgate={hideWarnsWhenUnauthed} 980 991 labels={warnContentLabels} 981 992 isOpen={isOpen} 982 993 onPress={(e) => { 983 994 e.stopPropagation(); 984 - setIsOpen(!isOpen) 995 + setHasUserTouchedToggleYet(true); 996 + if (!hideWarnsWhenUnauthed) { 997 + setIsOpen(!isOpen) 998 + } 985 999 }} 986 1000 /> 987 1001 )} ··· 1214 1228 } 1215 1229 1216 1230 export function ContentWarning({ 1231 + unauthedgate, 1217 1232 labels, 1218 1233 isOpen, 1219 1234 onPress, 1220 1235 }: { 1236 + unauthedgate?: boolean; 1221 1237 labels: ContentLabel[]; 1222 1238 isOpen: boolean; 1223 1239 onPress: React.MouseEventHandler<HTMLDivElement>; ··· 1257 1273 1258 1274 {/* Chevron */} 1259 1275 <div className="flex items-center justify-center text-gray-500 dark:text-gray-400 pl-2 gap-2 text-sm"> 1260 - {isOpen ? "hide" : "show"} 1261 - <IconMdiChevronDown 1276 + {unauthedgate ? "please login to view" : isOpen ? "hide" : "show"} 1277 + {!unauthedgate && (<IconMdiChevronDown 1262 1278 className={`text-xl transition-transform duration-300 ease-[cubic-bezier(0.2,0,0,1)] ${isOpen ? "rotate-180" : "" 1263 1279 }`} 1264 - /> 1280 + />)} 1265 1281 </div> 1266 1282 </div> 1267 1283 </div>