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

attampt at fixing av via fast bypasses

+253 -57
+10 -6
src/providers/PollMutationQueueProvider.tsx
··· 295 295 return context; 296 296 } 297 297 298 - function usePollSelfVotes(pollUri: string) { 298 + function usePollSelfVotes(pollUri: string, enabled?: boolean) { 299 299 const { agent } = useAuth(); 300 300 const agentDid = agent?.did; 301 301 302 302 const { uris: userVotesA } = useGetOneToOneState( 303 - agentDid 303 + agentDid && enabled 304 304 ? { 305 305 target: pollUri, 306 306 user: agentDid, 307 307 collection: "app.reddwarf.poll.vote.a", 308 308 path: ".subject.uri", 309 + enabled: enabled 309 310 } 310 311 : undefined, 311 312 ); 312 313 const { uris: userVotesB } = useGetOneToOneState( 313 - agentDid 314 + agentDid && enabled 314 315 ? { 315 316 target: pollUri, 316 317 user: agentDid, 317 318 collection: "app.reddwarf.poll.vote.b", 318 319 path: ".subject.uri", 320 + enabled: enabled 319 321 } 320 322 : undefined, 321 323 ); 322 324 const { uris: userVotesC } = useGetOneToOneState( 323 - agentDid 325 + agentDid && enabled 324 326 ? { 325 327 target: pollUri, 326 328 user: agentDid, 327 329 collection: "app.reddwarf.poll.vote.c", 328 330 path: ".subject.uri", 331 + enabled: enabled 329 332 } 330 333 : undefined, 331 334 ); 332 335 const { uris: userVotesD } = useGetOneToOneState( 333 - agentDid 336 + agentDid && enabled 334 337 ? { 335 338 target: pollUri, 336 339 user: agentDid, 337 340 collection: "app.reddwarf.poll.vote.d", 338 341 path: ".subject.uri", 342 + enabled: enabled 339 343 } 340 344 : undefined, 341 345 ); ··· 361 365 const myDid = agent?.did; 362 366 363 367 const { castVoteRaw, getLocalVotes } = usePollMutationQueue(); 364 - const serverUserVotes = usePollSelfVotes(pollUri); // Our own votes from server 368 + const serverUserVotes = usePollSelfVotes(pollUri, enabled); // Our own votes from server 365 369 const localVotes = getLocalVotes(pollUri); // Pending local actions 366 370 367 371 // 1. FETCHING - Move the logic here
+88 -27
src/routes/about.tsx
··· 1 1 import { createFileRoute } from '@tanstack/react-router' 2 + import React from 'react'; 2 3 3 4 import { FORCED_LABELER_DIDS, HOST_ABOUT_MARKDOWN, HOST_ADMIN, HOST_DESCRIPTION, HOST_HERO, HOST_LABELMERGE, HOST_SIGNUP_PDS } from '~/../policy'; 4 5 import { Header } from '~/components/Header'; ··· 173 174 // endorsed feeds (should be shown in the explore tab too in lieu of feed discovery) 174 175 // - [ ] HOST_UNAUTHED_DEFAULT_FEEDS 175 176 // endorsed PDS 176 - // - [ ] HOST_SIGNUP_PDS 177 + // - [x] HOST_SIGNUP_PDS 177 178 // todo move the other default services into policy.ts 178 179 // todo re- sort policy.ts according to this component 179 180 // also the default services used like microcosm stuff and lycan and maybe the reliance of an appview for search or some other hting ··· 181 182 // default general host moderation policies 182 183 // todo: layerd moderataion later pls thanks 183 184 // show the labelmerge insstance responsible 184 - // - [ ] HOST_LABELMERGE 185 + // - [x] HOST_LABELMERGE 185 186 // show both the whitelisted source and labeler dids in the same spot. 186 187 // like on hover / click it opens a dialog / popover to show what authority the labeler has 187 188 // - [x] FORCED_LABELER_DIDS ··· 197 198 return ( 198 199 <> 199 200 {/* settings heading or about heading? */} 201 + <Heading3 title="Instance Configuration" /> 202 + <KeyValueGrid 203 + items={[ 204 + { 205 + label: "PDS Signups (Account Storage):", 206 + value: HOST_SIGNUP_PDS || "", 207 + }, 208 + { 209 + label: "Labelmerge (Label Cache):", 210 + value: HOST_LABELMERGE, 211 + }, 212 + ]} 213 + /> 200 214 <Heading3 title="Instance Defaults" /> 201 - <div className="grid grid-cols-2 gap-x-2 gap-y-2 text-sm text-gray-700 dark:text-gray-300 mr-auto ml-2"> 202 - <span className="font-medium">PDS (User Account Storage):</span> 203 - <span className={HOST_SIGNUP_PDS ? "" : "italic"}>{HOST_SIGNUP_PDS || "not set"}</span> 204 - 205 - <span className="font-medium">Labelmerge (Label Cache):</span> 206 - <span>{HOST_LABELMERGE || "not set"}</span> 207 - 208 - <span className="font-medium">Constellation (Backlink Index):</span> 209 - <span>{defaultconstellationURL || "not set"}</span> 210 - 211 - <span className="font-medium">Slingshot (Record Cache):</span> 212 - <span>{defaultslingshotURL || "not set"}</span> 213 - 214 - <span className="font-medium">Image Provider (CDN):</span> 215 - <span>{defaultImgCDN || "not set"}</span> 216 - 217 - <span className="font-medium">Video Provider (CDN):</span> 218 - <span>{defaultVideoCDN || "not set"}</span> 219 - 220 - <span className="font-medium">Lycan (Personal Search):</span> 221 - <span className={defaultLycanURL ? "" : "italic"}>{defaultLycanURL || "not set"}</span> 222 - 223 - <span className="font-medium">AppView (Bluesky Index):</span> 224 - <span className={defaultAppviewURL? "" : "italic"}>{defaultAppviewURL || "not set"}</span> 225 - </div> 215 + <KeyValueGrid 216 + items={[ 217 + { 218 + label: "Constellation (Backlink Index):", 219 + value: defaultconstellationURL, 220 + //italicIfEmpty: true, 221 + }, 222 + { 223 + label: "Slingshot (Record Cache):", 224 + value: defaultslingshotURL, 225 + }, 226 + { 227 + label: "Image Provider (CDN):", 228 + value: defaultImgCDN, 229 + }, 230 + { 231 + label: "Video Provider (CDN):", 232 + value: defaultVideoCDN, 233 + }, 234 + { 235 + label: "Lycan (Personal Search):", 236 + value: defaultLycanURL, 237 + }, 238 + { 239 + label: "AppView (Bluesky Index):", 240 + value: defaultAppviewURL, 241 + }, 242 + ]} 243 + /> 226 244 {/* {hostmandate && (<Heading2 title="Host-Mandated Labelers" />)} */} 227 245 <Heading3 title="General Moderation" /> 228 246 {hostmandate && (<Heading4 title="Host-Mandated Labelers" />)} ··· 240 258 <div className='h-[300px] w-auto' /> 241 259 242 260 </> 261 + ) 262 + } 263 + 264 + type KeyValueItem = { 265 + label: string 266 + value?: string | null 267 + //italicIfEmpty?: boolean 268 + } 269 + 270 + interface KeyValueGridProps { 271 + items: KeyValueItem[] 272 + className?: string 273 + } 274 + 275 + export function KeyValueGrid({ items, className = "" }: KeyValueGridProps) { 276 + return ( 277 + <div 278 + className={`grid grid-cols-2 gap-x-2 gap-y-2 text-sm mr-auto ml-2 ${className}`} 279 + > 280 + {items.map((item, i) => { 281 + const isEmpty = !item.value 282 + 283 + return ( 284 + <React.Fragment key={i}> 285 + {/* Label */} 286 + <span className="font-medium text-gray-500 dark:text-gray-400"> 287 + {item.label} 288 + </span> 289 + 290 + {/* Value */} 291 + <span 292 + className={ 293 + isEmpty 294 + ? "text-gray-400 dark:text-gray-500 italic" 295 + : "text-gray-600 dark:text-gray-300" 296 + } 297 + > 298 + {item.value || "not set"} 299 + </span> 300 + </React.Fragment> 301 + ) 302 + })} 303 + </div> 243 304 ) 244 305 }
+77 -20
src/routes/profile.$did/post.$rkey.tsx
··· 1 1 import { AtUri } from "@atproto/api"; 2 - import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; 2 + import { QueryClient, useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; 3 3 import { createFileRoute, Outlet, useMatchRoute } from "@tanstack/react-router"; 4 - import { useAtom } from "jotai"; 4 + import { useAtom, useAtomValue } from "jotai"; 5 + import { loadable } from "jotai/utils"; 5 6 import React, { useLayoutEffect } from "react"; 6 7 7 8 import { Header } from "~/components/Header"; 8 9 import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 9 - import { constellationURLAtom, slingshotURLAtom } from "~/utils/atoms"; 10 + import { appviewUrlAtom, constellationURLAtom, enableAppViewAtom, slingshotURLAtom } from "~/utils/atoms"; 10 11 //import { usePersistentStore } from '~/providers/PersistentStoreProvider'; 11 12 import { 12 13 constructPostQuery, 14 + constructSingularAVPostQuery, 13 15 type linksAllResponse, 14 16 type linksRecordsResponse, 15 17 useQueryConstellation, 16 - useQueryIdentity, 18 + useQueryFastAVIdentity, 19 + //useQueryIdentity, 17 20 useQueryPost, 18 21 yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks, 19 22 } from "~/utils/useQuery"; ··· 189 192 // }; 190 193 // }, [atUri]); 191 194 195 + const [slingshoturl] = useAtom(slingshotURLAtom); 196 + 192 197 const { 193 198 data: identity, 194 199 isLoading: isIdentityLoading, 195 200 error: identityError, 196 - } = useQueryIdentity(showMainPostRoute ? did : undefined); 201 + } = useQueryFastAVIdentity(showMainPostRoute ? did : undefined, slingshoturl, queryClient, true); 197 202 198 203 const resolvedDid = did.startsWith("did:") ? did : identity?.did; 199 204 ··· 207 212 208 213 const { data: mainPost } = useQueryPost(showMainPostRoute ? atUri : undefined); 209 214 210 - console.log("atUri",atUri) 211 - 215 + console.log("atUri", atUri) 216 + 212 217 const opdid = React.useMemo( 213 218 () => 214 219 atUri ··· 218 223 ); 219 224 220 225 // @ts-expect-error i hate overloads 221 - const { data: links } = useQueryConstellation(atUri&&showMainPostRoute?{ 226 + const { data: links } = useQueryConstellation(atUri && showMainPostRoute ? { 222 227 method: "/links/all", 223 228 target: atUri, 224 229 } : { 225 230 method: "undefined", 226 231 target: "" 227 - })as { data: linksAllResponse | undefined }; 232 + }) as { data: linksAllResponse | undefined }; 228 233 229 234 //const [likes, setLikes] = React.useState<number | null>(null); 230 235 //const [reposts, setReposts] = React.useState<number | null>(null); ··· 245 250 setReplyCount( 246 251 links 247 252 ? links?.links?.["app.bsky.feed.post"]?.[".reply.parent.uri"] 248 - ?.records || 0 253 + ?.records || 0 249 254 : null 250 255 ); 251 256 }, [links]); ··· 388 393 389 394 hasPerformedInitialLayout.current = true; 390 395 } 391 - 396 + 392 397 // todo idk what to do with this 393 398 // eslint-disable-next-line react-hooks/set-state-in-effect 394 399 setLayoutReady(true); ··· 396 401 }, [parents, layoutReady, showMainPostRoute]); 397 402 398 403 399 - const [slingshoturl] = useAtom(slingshotURLAtom) 400 - 404 + const [isAppviewEnabled] = useAtom(enableAppViewAtom); 405 + const loadablePrefs = useAtomValue(loadable(enableAppViewAtom)) 406 + React.useEffect(() => { 407 + console.log("why is this fucked isAppviewEnabled?:", isAppviewEnabled); 408 + },[isAppviewEnabled]) 409 + const [appviewUrl] = useAtom(appviewUrlAtom); 410 + //const [slingshoturl] = useAtom(slingshotURLAtom) 411 + 401 412 React.useEffect(() => { 402 413 if (parentsLoading || !showMainPostRoute) { 403 414 setLayoutReady(false); ··· 412 423 const directparent = mainPost?.value.reply?.parent.uri; 413 424 414 425 React.useEffect(() => { 426 + console.log("parent fetching useeffect called!") 427 + // if (loadablePrefs.state !== "hasData") { 428 + // setParentsLoading(true); 429 + // return; 430 + // } 415 431 if (!mainPost?.value?.reply?.parent?.uri) { 416 432 setParents([]); 417 433 return; ··· 420 436 let ignore = false; 421 437 const fetchParents = async () => { 422 438 setParentsLoading(true); 423 - const parentChain: ({uri: string;cid: string;value: any;} | undefined)[] = []; 439 + const parentChain: ({ uri: string; cid: string; value: any; } | undefined)[] = []; 424 440 let currentParentUri = mainPost?.value.reply?.parent.uri; 425 441 const MAX_PARENTS = 25; 426 442 let safetyCounter = 0; 427 443 428 444 while (currentParentUri && safetyCounter < MAX_PARENTS) { 429 445 try { 430 - const parentPost = await queryClient.fetchQuery( 431 - constructPostQuery(currentParentUri, slingshoturl) 432 - ); 446 + const parentPost = await getProfilePostTryFail({isAppviewEnabled, appviewUrl, queryClient, currentParentUri, slingshoturl}) 433 447 if (!parentPost) break; 434 448 parentChain.push(parentPost); 435 449 currentParentUri = parentPost.value?.reply?.parent?.uri; 436 450 } catch (error) { 437 451 console.error("Failed to fetch a parent post:", error); 438 452 // its okay to always add one invalid parent then stop 439 - if (currentParentUri){ 453 + if (currentParentUri) { 440 454 parentChain.push({ 441 455 uri: currentParentUri, 442 456 cid: "sorry", ··· 458 472 return () => { 459 473 ignore = true; 460 474 }; 461 - }, [mainPost, queryClient, slingshoturl]); 475 + }, [appviewUrl, isAppviewEnabled, loadablePrefs.state, mainPost, queryClient, slingshoturl]); 462 476 463 477 if ((!did || !rkey) && showMainPostRoute) return <div>Invalid post URI</div>; 464 478 if (isIdentityLoading && showMainPostRoute) return <div>Resolving handle...</div>; ··· 545 559 /> 546 560 ); 547 561 })} 548 - {hasNextPage && ( 562 + {hasNextPage && ( 549 563 <button 550 564 onClick={() => fetchNextPage()} 551 565 disabled={isFetchingNextPage} ··· 560 574 </> 561 575 ); 562 576 } 577 + 578 + 579 + async function getProfilePostTryFail({ 580 + isAppviewEnabled, 581 + appviewUrl, 582 + queryClient, 583 + currentParentUri, 584 + slingshoturl, 585 + }: { 586 + isAppviewEnabled?: boolean, 587 + appviewUrl?: string, 588 + queryClient: QueryClient, 589 + currentParentUri: string, 590 + slingshoturl: string, 591 + }): Promise<{ 592 + uri: string, 593 + cid: string, 594 + value: any 595 + } | undefined> { 596 + try { 597 + if (isAppviewEnabled && appviewUrl) { 598 + console.log("why is this called? isAppviewEnabled:",isAppviewEnabled," appviewUrl:",appviewUrl) 599 + const result = await queryClient.fetchQuery( 600 + constructSingularAVPostQuery({ aturi: currentParentUri, avurl: appviewUrl, instantBypass: true }) 601 + ) 602 + if (result?.uri && result?.cid && result?.record) { 603 + return { 604 + uri: result?.uri, 605 + cid: result?.cid, 606 + value: result?.record as any 607 + } 608 + } else { 609 + throw "whatever"; 610 + } 611 + } else { 612 + throw "sure"; 613 + } 614 + } catch { 615 + console.log("whatever why is this called? isAppviewEnabled:",isAppviewEnabled," appviewUrl:",appviewUrl) 616 + return await queryClient.fetchQuery( 617 + constructPostQuery(currentParentUri, slingshoturl)) 618 + } 619 + }
+1 -1
src/routes/settings.tsx
··· 205 205 <SwitchSetting 206 206 atom={enableAppViewAtom} 207 207 title={"AppView-First"} 208 - description={"Prioritize using an AppView to hydrate posts & profiles before using microcosm"} 208 + description={"Prioritize using an AppView to fetch posts before using microcosm"} 209 209 //init={false} 210 210 /> 211 211 <div className={`${isAppViewEnabled ? "" : "opacity-50 pointer-events-none"}`}>
+3 -1
src/utils/followState.ts
··· 134 134 user: string; 135 135 collection: string; 136 136 path: string; 137 + enabled?: boolean; 137 138 }): { 138 139 uris: string[], 139 140 isLoading: boolean; ··· 152 153 customkey: params.collection.includes("reddwarf.poll.vote") 153 154 ? "constellation-polls" 154 155 : undefined, 156 + enabled: params.enabled || false, 155 157 } 156 - : { method: "undefined", target: "whatever" }, 158 + : { method: "undefined", target: "whatever", enabled: false }, 157 159 // overloading sucks so much 158 160 ) as UseQueryResult<linksRecordsResponse | undefined, Error>; 159 161 if (!params || !params.user) return {
+74 -2
src/utils/useQuery.ts
··· 7 7 useInfiniteQuery, 8 8 useQueries, 9 9 useQuery, 10 + //useQueryClient, 10 11 type UseQueryResult, 11 12 } from "@tanstack/react-query"; 12 13 import { create, windowScheduler } from "@yornaath/batshit"; ··· 73 74 export function useQueryIdentity(didorhandle?: string) { 74 75 const [slingshoturl] = useAtom(slingshotURLAtom); 75 76 return useQuery(constructIdentityQuery(didorhandle, slingshoturl)); 77 + } 78 + 79 + export function constructFastAVIdentityQuery( 80 + didorhandle?: string, 81 + slingshoturl?: string, 82 + queryClient?: QueryClient, 83 + enabled?: boolean 84 + ) { 85 + return queryOptions({ 86 + queryKey: ["identity", didorhandle], 87 + queryFn: async () => { 88 + try { 89 + console.log("whathuh trying", ["savpq", didorhandle]) 90 + if (!queryClient) throw "whatever" 91 + const datas = queryClient.getQueriesData<SingularAVPostResult | undefined>({ 92 + queryKey: ["savpq", didorhandle], 93 + }) 94 + console.log("whathuh checking", datas) 95 + const data = datas[0][1]; 96 + if (!data) { 97 + throw "whatever" 98 + } 99 + //const parsedaturi = new ATPAPI.AtUri(data.uri) 100 + console.log("whathuh success") 101 + return { 102 + did: data.author.did, 103 + handle: data.author.handle 104 + } 105 + } catch { 106 + console.log("whathuh failure") 107 + if (!didorhandle) return undefined as undefined; 108 + const res = await fetch( 109 + `https://${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}`, 110 + ); 111 + if (!res.ok) throw new Error("Failed to fetch post"); 112 + try { 113 + return (await res.json()) as { 114 + did: string; 115 + handle: string; 116 + pds: string; 117 + signing_key: string; 118 + }; 119 + } catch (_e) { 120 + return undefined; 121 + } 122 + } 123 + }, 124 + enabled, 125 + staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes 126 + gcTime: /*0//*/ 5 * 60 * 1000, 127 + }); 128 + } 129 + 130 + 131 + export function useQueryFastAVIdentity(didorhandle?: string, slingshoturl?: string, queryClient?: QueryClient, enabled: boolean = true) { 132 + return useQuery(constructFastAVIdentityQuery(didorhandle, slingshoturl, queryClient, enabled)); 76 133 } 77 134 78 135 export function constructPostQuery(uri?: string, slingshoturl?: string) { ··· 1398 1455 type SingularAVPostQuery = { 1399 1456 aturi: string, 1400 1457 avurl: string, 1458 + instantBypass?: boolean, 1401 1459 } 1402 1460 type SingularAVPostResult = ATPAPI.AppBskyFeedDefs.PostView 1403 1461 ··· 1533 1591 ); 1534 1592 1535 1593 export function constructSingularAVPostQuery(options: SingularAVPostQuery) { 1536 - const { aturi, avurl } = options; 1594 + const { aturi, avurl, instantBypass } = options; 1595 + const parsedaturi = new ATPAPI.AtUri(aturi) 1537 1596 1538 1597 return queryOptions({ 1539 - queryKey: ["__volatile","savpq", aturi], 1598 + queryKey: ["savpq", parsedaturi.host, /*"__volatile", */aturi], 1540 1599 1541 1600 enabled: !!aturi && !!avurl, 1542 1601 ··· 1546 1605 // throw result.error 1547 1606 // } 1548 1607 // return result; 1608 + if (instantBypass) { 1609 + const params = new URLSearchParams(); 1610 + params.append("uris", aturi) 1611 + const url = `${avurl}/xrpc/app.bsky.feed.getPosts?${params.toString()}`; 1612 + 1613 + const res = await fetch(url); 1614 + if (!res.ok) { 1615 + throw new Error(`Labelmerge fetch failed: ${res.status} ${res.statusText}`); 1616 + } 1617 + 1618 + const result = (await res.json()) as ATPAPI.AppBskyFeedGetPosts.OutputSchema; 1619 + return result.posts[0] 1620 + } 1549 1621 const result = (await postquerymerge 1550 1622 .fetch(options))as SingularAVPostResult; 1551 1623 // .catch(