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.

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(