Retro Bulletin Board Systems on atproto. Web app and TUI. lazy mirror of alyraffauf/atbbs atbbs.xyz
forums python tui atproto bbs
3
fork

Configure Feed

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

web: remove useSuspenseQuery for BBS/Board/Dashboard/Home/Profile

+78 -50
+9
web/src/components/layout/ListSkeleton.tsx
··· 1 + export default function ListSkeleton() { 2 + return ( 3 + <div className="space-y-2 animate-pulse" aria-hidden> 4 + <div className="h-16 w-full bg-neutral-900 rounded" /> 5 + <div className="h-16 w-full bg-neutral-900 rounded" /> 6 + <div className="h-16 w-full bg-neutral-900 rounded" /> 7 + </div> 8 + ); 9 + }
+10 -3
web/src/pages/BBS.tsx
··· 29 29 import ActionBar from "../components/nav/ActionBar"; 30 30 import { ActionLink } from "../components/nav/ActionButton"; 31 31 import PinButton from "../components/PinButton"; 32 + import ListSkeleton from "../components/layout/ListSkeleton"; 32 33 33 34 const INITIAL_NEWS_COUNT = 3; 34 35 ··· 41 42 const [showAllNews, setShowAllNews] = useState(false); 42 43 43 44 const { data: bbs } = useSuspenseQuery(bbsQuery(handle!)); 44 - const { data: news } = useSuspenseQuery(newsQuery(bbs.identity.did)); 45 + const { data: news } = useQuery(newsQuery(bbs.identity.did)); 45 46 const { data: pins } = useQuery({ 46 47 ...pinsQuery(user?.pdsUrl ?? "", user?.did ?? ""), 47 48 enabled: !!user, ··· 104 105 }); 105 106 } 106 107 107 - const visibleNews = showAllNews ? news : news.slice(0, INITIAL_NEWS_COUNT); 108 + const visibleNews = news 109 + ? showAllNews 110 + ? news 111 + : news.slice(0, INITIAL_NEWS_COUNT) 112 + : []; 108 113 109 114 return ( 110 115 <> ··· 185 190 </details> 186 191 )} 187 192 188 - {news.length ? ( 193 + {!news ? ( 194 + <ListSkeleton /> 195 + ) : news.length ? ( 189 196 <> 190 197 {visibleNews.map((item, i) => ( 191 198 <Link
+17 -14
web/src/pages/Board.tsx
··· 2 2 import { PenLine } from "lucide-react"; 3 3 import { useNavigate, useParams } from "react-router-dom"; 4 4 import { 5 + useInfiniteQuery, 5 6 useMutation, 6 - useSuspenseInfiniteQuery, 7 + useQuery, 7 8 useSuspenseQuery, 8 9 type InfiniteData, 9 10 type QueryKey, ··· 27 28 import type { ThreadItem, ThreadPageResult } from "../lib/boardThreads"; 28 29 import ThreadLink, { ThreadListHeader } from "../components/nav/ThreadLink"; 29 30 import ComposeForm from "../components/form/ComposeForm"; 31 + import ListSkeleton from "../components/layout/ListSkeleton"; 30 32 31 33 // Constellation indexes PDS writes asynchronously — usually within a second, 32 34 // occasionally longer. After creating a thread we refetch with backoff until ··· 64 66 fetchNextPage, 65 67 hasNextPage, 66 68 isFetchingNextPage, 67 - } = useSuspenseInfiniteQuery( 68 - boardThreadsInfiniteQuery(bbs.identity.did, slug!), 69 - ); 70 - const { data: moderation } = useSuspenseQuery( 69 + } = useInfiniteQuery(boardThreadsInfiniteQuery(bbs.identity.did, slug!)); 70 + const { data: moderation } = useQuery( 71 71 bbsModerationQuery(bbs.identity.pds ?? "", bbs.identity.did), 72 72 ); 73 73 const isSysop = !!(user && user.did === bbs.identity.did); 74 - const allThreads = threadPages.pages.flatMap((page) => page.threads); 75 - const threads = isSysop 76 - ? allThreads 77 - : allThreads.filter( 78 - (t) => 79 - !moderation.banRkeys[t.did] && 80 - !moderation.hideRkeys[t.uri], 81 - ); 74 + const ready = !!threadPages && !!moderation; 75 + const allThreads = threadPages?.pages.flatMap((page) => page.threads) ?? []; 76 + const threads = 77 + isSysop || !moderation 78 + ? allThreads 79 + : allThreads.filter( 80 + (t) => 81 + !moderation.banRkeys[t.did] && !moderation.hideRkeys[t.uri], 82 + ); 82 83 83 84 const [title, setTitle] = useState(""); 84 85 const [body, setBody] = useState(""); ··· 195 196 )} 196 197 197 198 <div> 198 - {threads.length ? ( 199 + {!ready ? ( 200 + <ListSkeleton /> 201 + ) : threads.length ? ( 199 202 <> 200 203 <ThreadListHeader /> 201 204 {threads.map((t) => (
+24 -21
web/src/pages/Dashboard.tsx
··· 1 1 import { useMemo, useState } from "react"; 2 - import { useSuspenseQuery, useMutation } from "@tanstack/react-query"; 2 + import { useQuery, useMutation } from "@tanstack/react-query"; 3 3 import { useAuth, type AuthUser } from "../lib/auth"; 4 4 import { deleteBBS } from "../lib/deletebbs"; 5 5 import { usePageTitle } from "../hooks/usePageTitle"; ··· 20 20 import MyThreadList from "../components/dashboard/MyThreadList"; 21 21 import ActivityList from "../components/dashboard/ActivityList"; 22 22 import BBSPanel from "../components/dashboard/BBSPanel"; 23 + import ListSkeleton from "../components/layout/ListSkeleton"; 23 24 24 25 type Tab = "inbox" | "threads" | "pinned" | "bbs"; 25 26 ··· 37 38 const [tab, setTab] = useState<Tab>("inbox"); 38 39 usePageTitle("atbbs"); 39 40 40 - const { data: sysopInfo } = useSuspenseQuery(homeSysopQuery(user.did)); 41 - const { data: pins } = useSuspenseQuery(pinsQuery(user.pdsUrl, user.did)); 42 - const { data: threads } = useSuspenseQuery( 43 - myThreadsQuery(user.pdsUrl, user.did), 44 - ); 45 - const { data: activity } = useSuspenseQuery( 46 - activityQuery(user.pdsUrl, user.did), 47 - ); 48 - const { data: discovered } = useSuspenseQuery(discoveryQuery()); 41 + const { data: sysopInfo } = useQuery(homeSysopQuery(user.did)); 42 + const { data: pins } = useQuery(pinsQuery(user.pdsUrl, user.did)); 43 + const { data: threads } = useQuery(myThreadsQuery(user.pdsUrl, user.did)); 44 + const { data: activity } = useQuery(activityQuery(user.pdsUrl, user.did)); 45 + const { data: discovered } = useQuery(discoveryQuery()); 49 46 50 - const suggestions = useMemo<Suggestion[]>(() => { 47 + const suggestions = useMemo<Suggestion[] | undefined>(() => { 48 + if (!pins || !discovered) return undefined; 51 49 const pinnedDids = new Set(pins.map((pin) => pin.did)); 52 50 const fromPins = pins.map(bbsToSuggestion); 53 51 const fromDiscovery = discovered ··· 120 118 <p className="text-neutral-400 text-xs mb-4"> 121 119 Recent replies from other users. 122 120 </p> 123 - <ActivityList items={activity} /> 121 + {activity ? <ActivityList items={activity} /> : <ListSkeleton />} 124 122 </> 125 123 )} 126 124 ··· 129 127 <p className="text-neutral-400 text-xs mb-4"> 130 128 Threads you've posted across all communities. 131 129 </p> 132 - <MyThreadList threads={threads} /> 130 + {threads ? <MyThreadList threads={threads} /> : <ListSkeleton />} 133 131 </> 134 132 )} 135 133 ··· 138 136 <p className="text-neutral-400 text-xs mb-4"> 139 137 Communities you've pinned for quick access. 140 138 </p> 141 - <PinnedList pins={pins} /> 139 + {pins ? <PinnedList pins={pins} /> : <ListSkeleton />} 142 140 </> 143 141 )} 144 142 ··· 147 145 <p className="text-neutral-400 text-xs mb-4"> 148 146 Manage your community. 149 147 </p> 150 - <BBSPanel 151 - hasBBS={sysopInfo.hasBBS} 152 - userHandle={user.handle} 153 - userDid={user.did} 154 - bbsName={sysopInfo.bbsName} 155 - onDelete={handleDeleteBBS} 156 - /> 148 + {sysopInfo ? ( 149 + <BBSPanel 150 + hasBBS={sysopInfo.hasBBS} 151 + userHandle={user.handle} 152 + userDid={user.did} 153 + bbsName={sysopInfo.bbsName} 154 + onDelete={handleDeleteBBS} 155 + /> 156 + ) : ( 157 + <ListSkeleton /> 158 + )} 157 159 </> 158 160 )} 159 161 </> 160 162 ); 161 163 } 164 +
+5 -5
web/src/pages/LoggedOutHome.tsx
··· 1 1 import { useMemo, useState } from "react"; 2 2 import { Phone, Copy, Check } from "lucide-react"; 3 - import { useSuspenseQuery } from "@tanstack/react-query"; 3 + import { useQuery } from "@tanstack/react-query"; 4 4 import { usePageTitle } from "../hooks/usePageTitle"; 5 5 import { discoveryQuery } from "../lib/queries"; 6 6 import DialBBS, { ··· 10 10 import DiscoveryList from "../components/dashboard/DiscoveryList"; 11 11 12 12 export default function LoggedOutHome() { 13 - const { data: discovered } = useSuspenseQuery(discoveryQuery()); 14 - const suggestions = useMemo<Suggestion[]>( 15 - () => discovered.map(bbsToSuggestion), 13 + const { data: discovered } = useQuery(discoveryQuery()); 14 + const suggestions = useMemo<Suggestion[] | undefined>( 15 + () => discovered?.map(bbsToSuggestion), 16 16 [discovered], 17 17 ); 18 18 const [tab, setTab] = useState<"brew" | "uv" | "telnet">("brew"); ··· 73 73 <div className="mb-6"> 74 74 <DialBBS discovered={discovered} suggestions={suggestions} /> 75 75 </div> 76 - <DiscoveryList discovered={discovered} /> 76 + {discovered && <DiscoveryList discovered={discovered} />} 77 77 </div> 78 78 79 79 <div className="border-t border-neutral-800 py-4">
+13 -7
web/src/pages/Profile.tsx
··· 1 1 import { useState } from "react"; 2 2 import { useParams } from "react-router-dom"; 3 - import { useSuspenseQuery, useMutation } from "@tanstack/react-query"; 3 + import { useQuery, useMutation } from "@tanstack/react-query"; 4 4 import { MessageSquare } from "lucide-react"; 5 5 import { useAuth } from "../lib/auth"; 6 6 import { usePageTitle } from "../hooks/usePageTitle"; ··· 10 10 import ViewProfile from "../components/profile/ViewProfile"; 11 11 import EditProfile from "../components/profile/EditProfile"; 12 12 import MyThreadList from "../components/dashboard/MyThreadList"; 13 + import ListSkeleton from "../components/layout/ListSkeleton"; 13 14 14 15 export default function Profile() { 15 16 const { handle } = useParams(); 16 17 const { user, agent } = useAuth(); 17 18 const [editing, setEditing] = useState(false); 18 19 19 - const { data: profile } = useSuspenseQuery(profileQuery(handle!)); 20 - const { data: threads } = useSuspenseQuery( 21 - myThreadsQuery(profile?.pdsUrl ?? "", profile?.did ?? ""), 22 - ); 20 + const { data: profile } = useQuery(profileQuery(handle!)); 21 + const { data: threads } = useQuery({ 22 + ...myThreadsQuery(profile?.pdsUrl ?? "", profile?.did ?? ""), 23 + enabled: !!profile, 24 + }); 23 25 24 26 usePageTitle(`${profile?.name ?? handle} — atbbs`); 25 27 ··· 58 60 <> 59 61 <ViewProfile 60 62 handle={handle!} 61 - profile={profile} 63 + profile={profile ?? null} 62 64 isOwner={isOwner} 63 65 onEdit={() => setEditing(true)} 64 66 /> ··· 66 68 <p className="text-xs text-neutral-400 uppercase tracking-wide mb-3 inline-flex items-center gap-1.5"> 67 69 <MessageSquare size={12} /> Recent Threads 68 70 </p> 69 - <MyThreadList threads={threads.slice(0, 5)} /> 71 + {threads ? ( 72 + <MyThreadList threads={threads.slice(0, 5)} /> 73 + ) : ( 74 + <ListSkeleton /> 75 + )} 70 76 </div> 71 77 </> 72 78 );