an independent Bluesky client using Constellation, PDS Queries, and other services
0
fork

Configure Feed

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

initial getprofile loading

+351 -35
+180 -9
src/routes/__root.tsx
··· 40 40 import { seo } from "~/utils/seo"; 41 41 import { useQueryIdentity, useQueryPreferences, useQueryProfile } from "~/utils/useQuery"; 42 42 43 + import { ConvenFeedIcon } from "./c.$did"; 44 + 43 45 export const Route = createRootRouteWithContext<{ 44 46 queryClient: QueryClient; 45 47 }>()({ ··· 221 223 // useAtomCssVar(hueAccentAtom, "--system-hue-accent"); 222 224 // useAtomCssVar(hueContrastAtom, "--system-hue-contrast"); 223 225 // useAtomCssVar(hueMutedAtom, "--system-hue-muted"); 226 + const location = useLocation(); 227 + const navigate = useNavigate(); 228 + const { agent } = useAuth(); 229 + const isNotifications = location.pathname.startsWith("/notifications"); 230 + const authed = !!agent?.did; 231 + const isProfile = 232 + agent && 233 + (location.pathname === `/profile/${agent?.did}` || 234 + location.pathname === `/profile/${encodeURIComponent(agent?.did ?? "")}`); 235 + const isSettings = location.pathname.startsWith("/settings"); 236 + const isSearch = location.pathname.startsWith("/search"); 237 + const isFeeds = location.pathname.startsWith("/feeds"); 238 + const isModeration = location.pathname.startsWith("/moderation"); 239 + const isAbout = location.pathname.startsWith("/about"); 240 + 241 + const locationEnum: 242 + | "feeds" 243 + | "search" 244 + | "settings" 245 + | "notifications" 246 + | "profile" 247 + | "moderation" 248 + | "about" 249 + | "home" = isFeeds 250 + ? "feeds" 251 + : isSearch 252 + ? "search" 253 + : isSettings 254 + ? "settings" 255 + : isNotifications 256 + ? "notifications" 257 + : isProfile 258 + ? "profile" 259 + : isModeration 260 + ? "moderation" 261 + : isAbout ? 262 + "about" 263 + : "home"; 264 + 265 + const dummysubscribedcommunitieslist = [ 266 + { 267 + did: "did:plc:vplx2bwt4krwpjta6pwpatwh", 268 + lastknownhandle: "deadlock.pds-nb.whey.party", 269 + title: "Deadlock", 270 + imgurl: "https://cdn.bsky.app/img/avatar/plain/did:plc:vplx2bwt4krwpjta6pwpatwh/bafkreidcmgnp63yobo2ktd3sz7hloswtcjqzdlhcn6d3sgcccgvek3kney" 271 + }, 272 + ] 224 273 225 274 return ( 226 275 <> 227 - <div className="h-14 sticky top-0 z-50 bg-accent-700 flex items-center justify-between px-6"> 228 - hello i am the persistent top bar ! <Login compact /> 229 - </div> 230 - {/* Layout */} 231 - <div className="flex"> 232 - <nav className="w-16 sticky top-14 self-start h-[calc(100vh-3.5rem)] p-2 bg-base-800"> 276 + <div className="flex flex-row"> 277 + <nav className="w-16 sticky top-0 self-start h-screen p-2 bg-base-800 flex flex-col items-center gap-2"> 278 + {dummysubscribedcommunitieslist.map((i)=>{ 279 + return ( 280 + <Link key={i.did} to="/c/$did" params={{did: i.did}} className=" rounded-full overflow-clip hover:rounded-2xl"> 281 + <ConvenFeedIcon key={i.did} imgurl={i.imgurl} /> 282 + </Link> 283 + ) 284 + })} 233 285 <span>hello i am the discord-like persistent left sidebar</span> 234 286 </nav> 235 - <main className="flex-1 min-h-[200vh]"> 236 - {children} 237 - </main> 287 + <div className="flex flex-1 flex-col"> 288 + <div className="h-14 sticky top-0 z-50 bg-accent-700 flex items-center justify-between px-6 gap-4"> 289 + <MaterialNavItem 290 + small 291 + InactiveIcon={ 292 + <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 293 + } 294 + ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 295 + active={locationEnum === "home"} 296 + onClickCallbback={() => 297 + navigate({ 298 + to: "/", 299 + //params: { did: agent.assertDid }, 300 + }) 301 + } 302 + text="Home" 303 + /> 304 + <div className="flex-1" /> 305 + 306 + <MaterialNavItem 307 + small 308 + InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 309 + ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 310 + active={locationEnum === "search"} 311 + onClickCallbback={() => 312 + navigate({ 313 + to: "/search", 314 + //params: { did: agent.assertDid }, 315 + }) 316 + } 317 + text="Explore" 318 + /> 319 + <span>hello i am the persistent top bar !</span> 320 + <div className="flex-1" /> 321 + <MaterialNavItem 322 + small 323 + //visible={!!agent?.did} 324 + InactiveIcon={ 325 + <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 326 + } 327 + ActiveIcon={ 328 + <IconMaterialSymbolsNotifications className="w-6 h-6" /> 329 + } 330 + active={locationEnum === "notifications"} 331 + onClickCallbback={() => 332 + navigate({ 333 + to: "/notifications", 334 + //params: { did: agent.assertDid }, 335 + }) 336 + } 337 + text="Notifications" 338 + /> 339 + <MaterialNavItem 340 + small 341 + //visible={!!agent?.did} 342 + InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 343 + ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 344 + active={locationEnum === "feeds"} 345 + onClickCallbback={() => 346 + navigate({ 347 + to: "/feeds", 348 + //params: { did: agent.assertDid }, 349 + }) 350 + } 351 + text="Feeds" 352 + /> 353 + <MaterialNavItem 354 + small 355 + //visible={!!agent?.did} 356 + InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />} 357 + ActiveIcon={<IconMdiShield className="w-6 h-6" />} 358 + active={locationEnum === "moderation"} 359 + onClickCallbback={() => 360 + navigate({ 361 + to: "/moderation", 362 + //params: { did: agent.assertDid }, 363 + }) 364 + } 365 + text="Moderation" 366 + /> 367 + <MaterialNavItem 368 + small 369 + // visible={!!agent?.did} 370 + InactiveIcon={ 371 + <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 372 + } 373 + ActiveIcon={ 374 + <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 375 + } 376 + active={locationEnum === "profile"} 377 + onClickCallbback={() => { 378 + if (authed && agent && agent.assertDid) { 379 + //window.location.href = `/profile/${agent.assertDid}`; 380 + navigate({ 381 + to: "/profile/$did", 382 + params: { did: agent.assertDid }, 383 + }); 384 + } 385 + }} 386 + text="Profile" 387 + /> 388 + <MaterialNavItem 389 + small 390 + InactiveIcon={ 391 + <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 392 + } 393 + ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 394 + active={locationEnum === "settings"} 395 + onClickCallbback={() => 396 + navigate({ 397 + to: "/settings", 398 + //params: { did: agent.assertDid }, 399 + }) 400 + } 401 + text="Settings" 402 + /> 403 + <Login compact /> 404 + </div> 405 + <main className="flex-1 min-h-[200vh]"> 406 + {children} 407 + </main> 408 + </div> 238 409 </div> 239 410 </> 240 411 )
+118 -26
src/routes/c.$did.tsx
··· 1 - import { createFileRoute, Outlet, useLocation, useNavigate } from '@tanstack/react-router' 2 - import { useAtom } from 'jotai' 1 + import AtpAgent, { AtUri } from '@atproto/api' 2 + import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; 3 + import { createFileRoute, Link, Outlet, useRouterState } from '@tanstack/react-router' 4 + import { useAtom } from 'jotai'; 5 + import { useEffect, useState } from 'react'; 3 6 4 - import { selectedFeedUriAtom } from '~/utils/atoms' 7 + import { useAuth } from '~/providers/UnifiedAuthProvider'; 8 + import { appviewUrlAtom } from '~/utils/atoms'; 9 + //import { useQueryGetProfiles } from '~/utils/useQuery'; 5 10 6 11 export const Route = createFileRoute('/c/$did')({ 7 12 component: RouteComponent, 13 + // loader: async () => { 14 + // const res = await agent 15 + // } 8 16 }) 9 17 10 18 const dummyfeedlist: ConvenFeed[] = [ ··· 31 39 ] 32 40 33 41 function RouteComponent() { 42 + const { agent: authAgent, status } = useAuth(); 43 + const [avurl] = useAtom(appviewUrlAtom); 44 + const state = useRouterState(); 45 + 46 + // Local state for the manual fetch 47 + const [profile, setProfile] = useState<ProfileViewDetailed | null>(null); 48 + const [loading, setLoading] = useState(false); 49 + const [error, setError] = useState<string | null>(null); 50 + 51 + const pathParts = state.location.pathname.split("/"); 52 + const c_did = pathParts[2]; 53 + 54 + // const res = useQueryGetProfiles( 55 + // status === "loading", 56 + // c_did ? [c_did] : [], // Only pass array if did exists 57 + // avurl, 58 + // agent || undefined 59 + // ); 60 + 61 + // shitty fallback im not sure why i even need this ??? but my hunch 62 + // is that the current agent is not built for public fetches? probably. im not sure. 63 + // blame the red dwarf dev 64 + useEffect(() => { 65 + async function fetchProfileManually() { 66 + if (!c_did) return; 67 + 68 + setLoading(true); 69 + setError(null); 70 + console.log("Manual fetch starting for:", c_did); 71 + 72 + try { 73 + // 2. FALLBACK: If authAgent is missing, create a temporary public one 74 + // This ensures a request happens even if signedOut 75 + const defaultagent = new AtpAgent({ service: 'https://public.api.bsky.app' }); 76 + const agent = status === "signedIn" ? authAgent || defaultagent : defaultagent; 77 + 78 + const res = await agent.app.bsky.actor.getProfile({ 79 + actor: decodeURIComponent(c_did) 80 + }); 81 + 82 + console.log("Manual fetch success:", res.data); 83 + setProfile(res.data); 84 + } catch (err: any) { 85 + console.error("Manual fetch FAILED:", err); 86 + setError(err.message || "Unknown error"); 87 + } finally { 88 + setLoading(false); 89 + } 90 + } 91 + 92 + fetchProfileManually(); 93 + }, [c_did, authAgent, status]); // Re-run if DID or Auth status changes 94 + 95 + // UI STATES 96 + if (loading) return <div>⏳ Manual Loading Profile... (DID: {c_did})</div>; 97 + 98 + if (error) return ( 99 + <div className="bg-red-100 p-4 border border-red-400 text-red-700"> 100 + <strong>Fetch Error:</strong> {error} <br /> 101 + <small>Check console for network logs.</small> 102 + </div> 103 + ); 104 + 105 + if (!profile) return ( 106 + <div> 107 + No profile data found. <br /> 108 + Status: {status} <br /> 109 + DID from URL: {c_did} 110 + </div> 111 + ); 112 + 113 + // ACTUAL UI 34 114 return ( 35 115 <> 36 - <div className="h-64 bg-accent-400 flex justify-center"> 116 + <div className="h-64 bg-accent-400 flex justify-center" 117 + style={{ 118 + backgroundImage: `url(${profile.banner})`, 119 + //backgroundColor: strictModerationLoading ? "var(--color-placeholder)" : undefined, 120 + backgroundSize: "cover", 121 + backgroundPosition: "center", 122 + }} 123 + > 37 124 <div className="w-full max-w-[calc(600px+16px+16px+224px+300px)] py-8 flex flex-col justify-end"> 38 125 <div className="flex items-center justify-between"> 39 126 <span className="text-muted-200 text-5xl font-bold"> 40 - hello i am the community header 127 + {profile.displayName} 41 128 </span> 42 - <button>Action</button> 129 + <button>Follow</button> 43 130 </div> 44 131 </div> 45 132 </div> ··· 52 139 <Outlet /> 53 140 </main> 54 141 <aside className="min-h-14 top-14 sticky z-10 bg-contrast-700 w-[300px]"> 55 - hello i am the sticky right sidebar ! 142 + <span>{profile.description}</span> 143 + {/* hello i am the sticky right sidebar ! */} 56 144 </aside> 57 145 </div> 58 146 </> ··· 89 177 idx: number, 90 178 rightDesktopSidebar?: boolean 91 179 }) { 92 - const location = useLocation(); 93 - const navigate = useNavigate(); 94 - const isAtHome = location.pathname == "/" || location.pathname == ""; 95 - const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom); 96 - const selectedFeed = persistentSelectedFeed 97 - const setSelectedFeed = setPersistentSelectedFeed 98 - const rkey = item.feeduri.split("/").pop() || item.feeduri; 99 - const isActive = selectedFeed === item.feeduri; 180 + const aturi = new AtUri(item.feeduri); 181 + const rkey = aturi.rkey 182 + const state = useRouterState() 183 + const [, c_constant, c_did, feed_expected, feed_rkey] = state.location.pathname.split("/"); 184 + const isActive = feed_rkey == rkey // selectedFeed === item.feeduri; 100 185 return ( 101 - <button 186 + <Link 187 + to='/c/$did/feed/$rkey' 188 + params={{ 189 + did: c_did, 190 + rkey: rkey 191 + }} 102 192 key={item.feeduri || idx} 103 193 className={`${rightDesktopSidebar ? "flex flex-row items-center gap-2 pr-4 pl-2.5 py-1.5" : "px-3 py-1 font-medium"} rounded-full whitespace-nowrap transition-colors ${isActive 104 194 ? "text-accent-900 dark:text-accent-100 hover:bg-muted-300 dark:bg-muted-700 bg-muted-200 hover:dark:bg-muted-600 font-medium" ··· 108 198 // ? "bg-muted-200 text-accent-700 dark:bg-muted-700 dark:text-accent-200" 109 199 // : "bg-muted-100 text-accent-700 dark:bg-muted-800 dark:text-accent-200" 110 200 }`} 111 - onClick={() => { 112 - if (rightDesktopSidebar && !isAtHome) { 113 - navigate({ 114 - to: "/" 115 - }) 116 - } 117 - setSelectedFeed(item.feeduri) 118 - }} 201 + // onClick={() => { 202 + // // if (rightDesktopSidebar && !isAtHome) { 203 + // // navigate({ 204 + // // to: "/" 205 + // // }) 206 + // // } 207 + // // setSelectedFeed(item.feeduri) 208 + // }} 119 209 title={item.feeduri} 120 210 > 211 + {/* {"userouterstate: " + JSON.stringify(routekey, null, 2)} */} 212 + {/* {"rkey: " + rkey} */} 121 213 {rightDesktopSidebar && ( 122 214 <ConvenFeedIcon imgurl={item.imgurl} className="w-5 h-5 rounded-sm object-cover" /> 123 215 )} ··· 132 224 133 225 </span> 134 226 )} */} 135 - </button> 227 + </Link> 136 228 ); 137 229 } 138 230 139 231 140 - function ConvenFeedIcon({ imgurl, className = "w-10 h-10 rounded-sm object-cover" }: { imgurl: string, className?: string }) { 232 + export function ConvenFeedIcon({ imgurl, className = "w-10 h-10 rounded-sm object-cover" }: { imgurl: string, className?: string }) { 141 233 142 234 const avatarUrl = imgurl; 143 235 if (!avatarUrl) {
+53
src/utils/useQuery.ts
··· 1638 1638 export function useQuerySingularAVPostQuery(options: SingularAVPostQuery) { 1639 1639 return useQuery(constructSingularAVPostQuery(options)); 1640 1640 } 1641 + 1642 + // conven 1643 + 1644 + 1645 + /** 1646 + * Doesnt work 1647 + * @deprecated doesnt work 1648 + */ 1649 + export function constructGetProfilesQuery( 1650 + disabled?: boolean, 1651 + ids?: string[], 1652 + appviewdid?: string, 1653 + agent?: ATPAPI.Agent 1654 + ) { 1655 + // Ensure we have a valid agent and actual IDs (not an array of undefined) 1656 + const hasValidIds = ids && ids.length > 0 && ids.every(id => !!id); 1657 + const canRun = !disabled && !!agent && !!appviewdid && hasValidIds; 1658 + 1659 + return queryOptions({ 1660 + enabled: !!canRun, 1661 + queryKey: ["getprofiles", ids, appviewdid], 1662 + queryFn: async () => { 1663 + // Guard clause inside the fetcher 1664 + if (!agent || !ids || ids.length === 0) { 1665 + throw new Error("Query attempted to run without required credentials"); 1666 + } 1667 + 1668 + const decodedids = ids.map(id => decodeURIComponent(id)); 1669 + 1670 + const res = await agent.app.bsky.actor.getProfiles({ 1671 + actors: decodedids 1672 + }); 1673 + 1674 + if (!res.success) throw new Error("Failed to fetch profiles"); 1675 + return res.data; 1676 + }, 1677 + staleTime: 5 * 60 * 1000, 1678 + gcTime: 5 * 60 * 1000, 1679 + }); 1680 + } 1681 + 1682 + /** 1683 + * Doesnt work 1684 + * @deprecated doesnt work 1685 + */ 1686 + export function useQueryGetProfiles( 1687 + disabled?: boolean, 1688 + ids?: string[], 1689 + appviewdid?: string, 1690 + agent?: ATPAPI.Agent 1691 + ) { 1692 + return useQuery(constructGetProfilesQuery(disabled, ids, appviewdid, agent)); 1693 + }