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

Configure Feed

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

[moderation] initial display of prefs

rimar1337 665413c9 7edbb928

+495 -19
+2
src/auto-imports.d.ts
··· 23 23 const IconMdiClose: typeof import('~icons/mdi/close.jsx').default 24 24 const IconMdiMessageReplyTextOutline: typeof import('~icons/mdi/message-reply-text-outline.jsx').default 25 25 const IconMdiPencilOutline: typeof import('~icons/mdi/pencil-outline.jsx').default 26 + const IconMdiShield: typeof import('~icons/mdi/shield.jsx').default 27 + const IconMdiShieldOutline: typeof import('~icons/mdi/shield-outline.jsx').default 26 28 }
+21
src/routeTree.gen.ts
··· 12 12 import { Route as SettingsRouteImport } from './routes/settings' 13 13 import { Route as SearchRouteImport } from './routes/search' 14 14 import { Route as NotificationsRouteImport } from './routes/notifications' 15 + import { Route as ModerationRouteImport } from './routes/moderation' 15 16 import { Route as FeedsRouteImport } from './routes/feeds' 16 17 import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout' 17 18 import { Route as IndexRouteImport } from './routes/index' ··· 42 43 const NotificationsRoute = NotificationsRouteImport.update({ 43 44 id: '/notifications', 44 45 path: '/notifications', 46 + getParentRoute: () => rootRouteImport, 47 + } as any) 48 + const ModerationRoute = ModerationRouteImport.update({ 49 + id: '/moderation', 50 + path: '/moderation', 45 51 getParentRoute: () => rootRouteImport, 46 52 } as any) 47 53 const FeedsRoute = FeedsRouteImport.update({ ··· 133 139 export interface FileRoutesByFullPath { 134 140 '/': typeof IndexRoute 135 141 '/feeds': typeof FeedsRoute 142 + '/moderation': typeof ModerationRoute 136 143 '/notifications': typeof NotificationsRoute 137 144 '/search': typeof SearchRoute 138 145 '/settings': typeof SettingsRoute ··· 152 159 export interface FileRoutesByTo { 153 160 '/': typeof IndexRoute 154 161 '/feeds': typeof FeedsRoute 162 + '/moderation': typeof ModerationRoute 155 163 '/notifications': typeof NotificationsRoute 156 164 '/search': typeof SearchRoute 157 165 '/settings': typeof SettingsRoute ··· 173 181 '/': typeof IndexRoute 174 182 '/_pathlessLayout': typeof PathlessLayoutRouteWithChildren 175 183 '/feeds': typeof FeedsRoute 184 + '/moderation': typeof ModerationRoute 176 185 '/notifications': typeof NotificationsRoute 177 186 '/search': typeof SearchRoute 178 187 '/settings': typeof SettingsRoute ··· 195 204 fullPaths: 196 205 | '/' 197 206 | '/feeds' 207 + | '/moderation' 198 208 | '/notifications' 199 209 | '/search' 200 210 | '/settings' ··· 214 224 to: 215 225 | '/' 216 226 | '/feeds' 227 + | '/moderation' 217 228 | '/notifications' 218 229 | '/search' 219 230 | '/settings' ··· 234 245 | '/' 235 246 | '/_pathlessLayout' 236 247 | '/feeds' 248 + | '/moderation' 237 249 | '/notifications' 238 250 | '/search' 239 251 | '/settings' ··· 256 268 IndexRoute: typeof IndexRoute 257 269 PathlessLayoutRoute: typeof PathlessLayoutRouteWithChildren 258 270 FeedsRoute: typeof FeedsRoute 271 + ModerationRoute: typeof ModerationRoute 259 272 NotificationsRoute: typeof NotificationsRoute 260 273 SearchRoute: typeof SearchRoute 261 274 SettingsRoute: typeof SettingsRoute ··· 288 301 path: '/notifications' 289 302 fullPath: '/notifications' 290 303 preLoaderRoute: typeof NotificationsRouteImport 304 + parentRoute: typeof rootRouteImport 305 + } 306 + '/moderation': { 307 + id: '/moderation' 308 + path: '/moderation' 309 + fullPath: '/moderation' 310 + preLoaderRoute: typeof ModerationRouteImport 291 311 parentRoute: typeof rootRouteImport 292 312 } 293 313 '/feeds': { ··· 456 476 IndexRoute: IndexRoute, 457 477 PathlessLayoutRoute: PathlessLayoutRouteWithChildren, 458 478 FeedsRoute: FeedsRoute, 479 + ModerationRoute: ModerationRoute, 459 480 NotificationsRoute: NotificationsRoute, 460 481 SearchRoute: SearchRoute, 461 482 SettingsRoute: SettingsRoute,
+32 -3
src/routes/__root.tsx
··· 213 213 const isSettings = location.pathname.startsWith("/settings"); 214 214 const isSearch = location.pathname.startsWith("/search"); 215 215 const isFeeds = location.pathname.startsWith("/feeds"); 216 + const isModeration = location.pathname.startsWith("/moderation"); 216 217 217 218 const locationEnum: 218 219 | "feeds" ··· 220 221 | "settings" 221 222 | "notifications" 222 223 | "profile" 224 + | "moderation" 223 225 | "home" = isFeeds 224 226 ? "feeds" 225 227 : isSearch ··· 230 232 ? "notifications" 231 233 : isProfile 232 234 ? "profile" 233 - : "home"; 235 + : isModeration 236 + ? "moderation" 237 + : "home"; 234 238 235 239 const [, setComposerPost] = useAtom(composerAtom); 236 240 ··· 309 313 }) 310 314 } 311 315 text="Feeds" 316 + /> 317 + <MaterialNavItem 318 + InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />} 319 + ActiveIcon={<IconMdiShield className="w-6 h-6" />} 320 + active={locationEnum === "moderation"} 321 + onClickCallbback={() => 322 + navigate({ 323 + to: "/moderation", 324 + //params: { did: agent.assertDid }, 325 + }) 326 + } 327 + text="Moderation" 312 328 /> 313 329 <MaterialNavItem 314 330 InactiveIcon={ ··· 555 571 /> 556 572 <MaterialNavItem 557 573 small 574 + InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />} 575 + ActiveIcon={<IconMdiShield className="w-6 h-6" />} 576 + active={locationEnum === "moderation"} 577 + onClickCallbback={() => 578 + navigate({ 579 + to: "/moderation", 580 + //params: { did: agent.assertDid }, 581 + }) 582 + } 583 + text="Moderation" 584 + /> 585 + <MaterialNavItem 586 + small 558 587 InactiveIcon={ 559 588 <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 560 589 } ··· 778 807 <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 779 808 } 780 809 ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 781 - active={locationEnum === "settings"} 810 + active={locationEnum === "settings" || locationEnum === "feeds" || locationEnum === "moderation"} 782 811 onClickCallbback={() => 783 812 navigate({ 784 813 to: "/settings", ··· 833 862 ); 834 863 } 835 864 836 - function MaterialNavItem({ 865 + export function MaterialNavItem({ 837 866 InactiveIcon, 838 867 ActiveIcon, 839 868 text,
+18 -1
src/routes/feeds.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 2 3 + import { Header } from "~/components/Header"; 4 + 3 5 export const Route = createFileRoute("/feeds")({ 4 6 component: Feeds, 5 7 }); 6 8 7 9 export function Feeds() { 8 - return <div className="p-6">Feeds page (coming soon)</div>; 10 + return ( 11 + <div className=""> 12 + <Header 13 + title={`Feeds`} 14 + backButtonCallback={() => { 15 + if (window.history.length > 1) { 16 + window.history.back(); 17 + } else { 18 + window.location.assign("/"); 19 + } 20 + }} 21 + bottomBorderDisabled={true} 22 + /> 23 + Feeds page (coming soon) 24 + </div> 25 + ); 9 26 }
+269
src/routes/moderation.tsx
··· 1 + import * as ATPAPI from "@atproto/api"; 2 + import { 3 + isAdultContentPref, 4 + isBskyAppStatePref, 5 + isContentLabelPref, 6 + isFeedViewPref, 7 + isLabelersPref, 8 + isMutedWordsPref, 9 + isSavedFeedsPref, 10 + } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 11 + import { createFileRoute } from "@tanstack/react-router"; 12 + import { useAtom } from "jotai"; 13 + import { Switch } from "radix-ui"; 14 + 15 + import { Header } from "~/components/Header"; 16 + import { useAuth } from "~/providers/UnifiedAuthProvider"; 17 + import { quickAuthAtom } from "~/utils/atoms"; 18 + import { useQueryIdentity, useQueryPreferences } from "~/utils/useQuery"; 19 + 20 + import { renderSnack } from "./__root"; 21 + import { NotificationItem } from "./notifications"; 22 + import { SettingHeading } from "./settings"; 23 + 24 + export const Route = createFileRoute("/moderation")({ 25 + component: RouteComponent, 26 + }); 27 + 28 + function RouteComponent() { 29 + const { agent } = useAuth(); 30 + 31 + const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom); 32 + const isAuthRestoring = quickAuth ? status === "loading" : false; 33 + 34 + const identityresultmaybe = useQueryIdentity( 35 + !isAuthRestoring ? agent?.did : undefined 36 + ); 37 + const identity = identityresultmaybe?.data; 38 + 39 + const prefsresultmaybe = useQueryPreferences({ 40 + agent: !isAuthRestoring ? (agent ?? undefined) : undefined, 41 + pdsUrl: !isAuthRestoring ? identity?.pds : undefined, 42 + }); 43 + const rawprefs = prefsresultmaybe?.data?.preferences as 44 + | ATPAPI.AppBskyActorGetPreferences.OutputSchema["preferences"] 45 + | undefined; 46 + 47 + //console.log(JSON.stringify(prefs, null, 2)) 48 + 49 + const parsedPref = parsePreferences(rawprefs); 50 + 51 + return ( 52 + <div> 53 + <Header 54 + title={`Moderation`} 55 + backButtonCallback={() => { 56 + if (window.history.length > 1) { 57 + window.history.back(); 58 + } else { 59 + window.location.assign("/"); 60 + } 61 + }} 62 + bottomBorderDisabled={true} 63 + /> 64 + {/* <SettingHeading title="Moderation Tools" /> 65 + <p> 66 + todo: add all these: 67 + <br /> 68 + - Interaction settings 69 + <br /> 70 + - Muted words & tags 71 + <br /> 72 + - Moderation lists 73 + <br /> 74 + - Muted accounts 75 + <br /> 76 + - Blocked accounts 77 + <br /> 78 + - Verification settings 79 + <br /> 80 + </p> */} 81 + <SettingHeading title="Content Filters" /> 82 + <div> 83 + <div className="flex items-center gap-4 px-4 py-2 border-b"> 84 + <label 85 + htmlFor={`switch-${"hardcoded"}`} 86 + className="flex flex-row flex-1" 87 + > 88 + <div className="flex flex-col"> 89 + <span className="text-md">{"Adult Content"}</span> 90 + <span className="text-sm text-gray-500 dark:text-gray-400"> 91 + {"Enable adult content"} 92 + </span> 93 + </div> 94 + </label> 95 + 96 + <Switch.Root 97 + id={`switch-${"hardcoded"}`} 98 + checked={parsedPref?.adultContentEnabled} 99 + onCheckedChange={(v) => { 100 + renderSnack({ 101 + title: "Sorry... Modifying preferences is not implemented yet", 102 + description: "You can use another app to change preferences", 103 + //button: { label: 'Try Again', onClick: () => console.log('whatever') }, 104 + }); 105 + }} 106 + className="m3switch root" 107 + > 108 + <Switch.Thumb className="m3switch thumb " /> 109 + </Switch.Root> 110 + </div> 111 + <div className=""> 112 + {Object.entries(parsedPref?.contentLabelPrefs ?? {}).map( 113 + ([label, visibility]) => ( 114 + <div 115 + key={label} 116 + className="flex justify-between border-b py-2 px-4" 117 + > 118 + <label 119 + htmlFor={`switch-${"hardcoded"}`} 120 + className="flex flex-row flex-1" 121 + > 122 + <div className="flex flex-col"> 123 + <span className="text-md">{label}</span> 124 + <span className="text-sm text-gray-500 dark:text-gray-400"> 125 + {"uknown labeler"} 126 + </span> 127 + </div> 128 + </label> 129 + {/* <span className="text-md text-gray-500 dark:text-gray-400"> 130 + {visibility} 131 + </span> */} 132 + <TripleToggle 133 + value={visibility as "ignore" | "warn" | "hide"} 134 + /> 135 + </div> 136 + ) 137 + )} 138 + </div> 139 + </div> 140 + <SettingHeading title="Advanced" /> 141 + {parsedPref?.labelers.map((labeler) => { 142 + return ( 143 + <NotificationItem 144 + key={labeler} 145 + notification={labeler} 146 + labeler={true} 147 + /> 148 + ); 149 + })} 150 + </div> 151 + ); 152 + } 153 + 154 + export function TripleToggle({ 155 + value, 156 + onChange, 157 + }: { 158 + value: "ignore" | "warn" | "hide"; 159 + onChange?: (newValue: "ignore" | "warn" | "hide") => void; 160 + }) { 161 + const options: Array<"ignore" | "warn" | "hide"> = ["ignore", "warn", "hide"]; 162 + return ( 163 + <div className="flex rounded-full bg-gray-200 dark:bg-gray-800 p-1 text-sm"> 164 + {options.map((opt) => { 165 + const isActive = opt === value; 166 + return ( 167 + <button 168 + key={opt} 169 + onClick={() => { 170 + renderSnack({ 171 + title: "Sorry... Modifying preferences is not implemented yet", 172 + description: "You can use another app to change preferences", 173 + //button: { label: 'Try Again', onClick: () => console.log('whatever') }, 174 + }); 175 + onChange?.(opt); 176 + }} 177 + className={`flex-1 px-3 py-1.5 rounded-full transition-colors ${ 178 + isActive 179 + ? "bg-gray-400 dark:bg-gray-600 text-white" 180 + : "text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-700" 181 + }`} 182 + > 183 + {" "} 184 + {opt.charAt(0).toUpperCase() + opt.slice(1)} 185 + </button> 186 + ); 187 + })} 188 + </div> 189 + ); 190 + } 191 + 192 + type PrefItem = 193 + ATPAPI.AppBskyActorGetPreferences.OutputSchema["preferences"][number]; 194 + 195 + export interface NormalizedPreferences { 196 + contentLabelPrefs: Record<string, string>; 197 + mutedWords: string[]; 198 + feedViewPrefs: Record<string, any>; 199 + labelers: string[]; 200 + adultContentEnabled: boolean; 201 + savedFeeds: { 202 + pinned: string[]; 203 + saved: string[]; 204 + }; 205 + nuxs: string[]; 206 + } 207 + 208 + export function parsePreferences( 209 + prefs?: PrefItem[] 210 + ): NormalizedPreferences | undefined { 211 + if (!prefs) return undefined; 212 + const normalized: NormalizedPreferences = { 213 + contentLabelPrefs: {}, 214 + mutedWords: [], 215 + feedViewPrefs: {}, 216 + labelers: [], 217 + adultContentEnabled: false, 218 + savedFeeds: { pinned: [], saved: [] }, 219 + nuxs: [], 220 + }; 221 + 222 + for (const pref of prefs) { 223 + switch (pref.$type) { 224 + case "app.bsky.actor.defs#contentLabelPref": 225 + if (!isContentLabelPref(pref)) break; 226 + normalized.contentLabelPrefs[pref.label] = pref.visibility; 227 + break; 228 + 229 + case "app.bsky.actor.defs#mutedWordsPref": 230 + if (!isMutedWordsPref(pref)) break; 231 + for (const item of pref.items ?? []) { 232 + normalized.mutedWords.push(item.value); 233 + } 234 + break; 235 + 236 + case "app.bsky.actor.defs#feedViewPref": 237 + if (!isFeedViewPref(pref)) break; 238 + normalized.feedViewPrefs[pref.feed] = pref; 239 + break; 240 + 241 + case "app.bsky.actor.defs#labelersPref": 242 + if (!isLabelersPref(pref)) break; 243 + normalized.labelers.push(...(pref.labelers?.map((l) => l.did) ?? [])); 244 + break; 245 + 246 + case "app.bsky.actor.defs#adultContentPref": 247 + if (!isAdultContentPref(pref)) break; 248 + normalized.adultContentEnabled = !!pref.enabled; 249 + break; 250 + 251 + case "app.bsky.actor.defs#savedFeedsPref": 252 + if (!isSavedFeedsPref(pref)) break; 253 + normalized.savedFeeds.pinned.push(...(pref.pinned ?? [])); 254 + normalized.savedFeeds.saved.push(...(pref.saved ?? [])); 255 + break; 256 + 257 + case "app.bsky.actor.defs#bskyAppStatePref": 258 + if (!isBskyAppStatePref(pref)) break; 259 + normalized.nuxs.push(...(pref.nuxs?.map((n) => n.id) ?? [])); 260 + break; 261 + 262 + default: 263 + // unknown pref type — just ignore for now 264 + break; 265 + } 266 + } 267 + 268 + return normalized; 269 + }
+2 -2
src/routes/notifications.tsx
··· 572 572 ); 573 573 } 574 574 575 - export function NotificationItem({ notification }: { notification: string }) { 575 + export function NotificationItem({ notification, labeler }: { notification: string, labeler?: boolean }) { 576 576 const aturi = new AtUri(notification); 577 577 const bite = aturi.collection === "net.wafrn.feed.bite"; 578 578 const navigate = useNavigate(); ··· 618 618 <img 619 619 src={avatar || defaultpfp} 620 620 alt={identity?.handle} 621 - className="w-10 h-10 rounded-full" 621 + className={`w-10 h-10 ${labeler ? "rounded-md" : "rounded-full"}`} 622 622 /> 623 623 ) : ( 624 624 <div className="w-10 h-10 rounded-full bg-gray-300 dark:bg-gray-700" />
+119 -9
src/routes/profile.$did/index.tsx
··· 32 32 useQueryIdentity, 33 33 useQueryProfile, 34 34 } from "~/utils/useQuery"; 35 + import IconMdiShieldOutline from "~icons/mdi/shield-outline.jsx"; 35 36 36 37 import { renderSnack } from "../__root"; 37 38 import { Chip } from "../notifications"; ··· 51 52 isLoading: isIdentityLoading, 52 53 error: identityError, 53 54 } = useQueryIdentity(did); 55 + 56 + // i was gonna check the did doc but useQueryIdentity doesnt return that info (slingshot minidoc) 57 + // so instead we should query the labeler profile 58 + 59 + const { data: labelerProfile } = useQueryArbitrary( 60 + identity?.did 61 + ? `at://${identity?.did}/app.bsky.labeler.service/self` 62 + : undefined 63 + ); 64 + 65 + const isLabeler = !!labelerProfile?.cid; 66 + const labelerRecord = isLabeler 67 + ? (labelerProfile?.value as ATPAPI.AppBskyLabelerService.Record) 68 + : undefined; 54 69 55 70 const resolvedDid = did.startsWith("did:") ? did : identity?.did; 56 71 const resolvedHandle = did.startsWith("did:") ? identity?.handle : did; ··· 141 156 142 157 {/* Avatar (PFP) */} 143 158 <div className="absolute left-[16px] top-[100px] "> 144 - <img 145 - src={getAvatarUrl(profile) || "/favicon.png"} 146 - alt="avatar" 147 - className="w-28 h-28 rounded-full object-cover border-4 border-white dark:border-gray-950 bg-gray-300 dark:bg-gray-700" 148 - /> 159 + {!getAvatarUrl(profile) && isLabeler ? ( 160 + <div 161 + className={`w-28 h-28 ${isLabeler ? "rounded-md" : "rounded-full"} items-center justify-center flex object-cover border-4 border-white dark:border-gray-950 bg-gray-300 dark:bg-gray-700`} 162 + > 163 + <IconMdiShieldOutline className="w-20 h-20" /> 164 + </div> 165 + ) : ( 166 + <img 167 + src={getAvatarUrl(profile) || "/favicon.png"} 168 + alt="avatar" 169 + className={`w-28 h-28 ${isLabeler ? "rounded-md" : "rounded-full"} object-cover border-4 border-white dark:border-gray-950 bg-gray-300 dark:bg-gray-700`} 170 + /> 171 + )} 149 172 </div> 150 173 151 174 <div className="absolute right-[16px] top-[170px] flex flex-row gap-2.5"> ··· 206 229 <ReusableTabRoute 207 230 route={`Profile` + did} 208 231 tabs={{ 209 - Posts: <PostsTab did={did} />, 210 - Reposts: <RepostsTab did={did} />, 211 - Feeds: <FeedsTab did={did} />, 212 - Lists: <ListsTab did={did} />, 232 + ...(isLabeler 233 + ? { 234 + Labels: <LabelsTab did={did} labelerRecord={labelerRecord} />, 235 + } 236 + : {}), 237 + ...{ 238 + Posts: <PostsTab did={did} />, 239 + Reposts: <RepostsTab did={did} />, 240 + Feeds: <FeedsTab did={did} />, 241 + Lists: <ListsTab did={did} />, 242 + }, 213 243 ...(identity?.did === agent?.did 214 244 ? { Likes: <SelfLikesTab did={did} /> } 215 245 : {}), ··· 529 559 {feeds.length === 0 && !arePostsLoading && ( 530 560 <div className="p-4 text-center text-gray-500">No feeds found.</div> 531 561 )} 562 + </> 563 + ); 564 + } 565 + 566 + function LabelsTab({ 567 + did, 568 + labelerRecord, 569 + }: { 570 + did: string; 571 + labelerRecord?: ATPAPI.AppBskyLabelerService.Record; 572 + }) { 573 + useReusableTabScrollRestore(`Profile` + did); 574 + const { agent } = useAuth(); 575 + // const { 576 + // data: identity, 577 + // isLoading: isIdentityLoading, 578 + // error: identityError, 579 + // } = useQueryIdentity(did); 580 + 581 + // const resolvedDid = did.startsWith("did:") ? did : identity?.did; 582 + 583 + const labelMap = new Map( 584 + labelerRecord?.policies?.labelValueDefinitions?.map((def) => { 585 + const locale = def.locales.find((l) => l.lang === "en") ?? def.locales[0]; 586 + return [ 587 + def.identifier, 588 + { 589 + name: locale?.name, 590 + description: locale?.description, 591 + blur: def.blurs, 592 + severity: def.severity, 593 + adultOnly: def.adultOnly, 594 + defaultSetting: def.defaultSetting, 595 + }, 596 + ]; 597 + }) 598 + ); 599 + 600 + return ( 601 + <> 602 + <div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4"> 603 + Labels 604 + </div> 605 + <div> 606 + {[...labelMap.entries()].map(([key, item]) => ( 607 + <div 608 + key={key} 609 + className="border-gray-300 dark:border-gray-700 border-b px-4 py-4" 610 + > 611 + <div className="font-semibold text-lg">{item.name}</div> 612 + <div className="text-sm text-gray-500 dark:text-gray-400"> 613 + {item.description} 614 + </div> 615 + <div className="mt-1 text-xs text-gray-400"> 616 + {item.blur && <span>Blur: {item.blur} </span>} 617 + {item.severity && <span>• Severity: {item.severity} </span>} 618 + {item.adultOnly && <span>• 18+ only</span>} 619 + </div> 620 + </div> 621 + ))} 622 + </div> 623 + 624 + {/* Loading and "Load More" states */} 625 + {!labelerRecord && ( 626 + <div className="p-4 text-center text-gray-500">Loading labels...</div> 627 + )} 628 + {/* {!labelerRecord && ( 629 + <div className="p-4 text-center text-gray-500">Loading more...</div> 630 + )} */} 631 + {/* {hasNextPage && !isFetchingNextPage && ( 632 + <button 633 + onClick={() => fetchNextPage()} 634 + className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold" 635 + > 636 + Load More Feeds 637 + </button> 638 + )} 639 + {feeds.length === 0 && !arePostsLoading && ( 640 + <div className="p-4 text-center text-gray-500">No feeds found.</div> 641 + )} */} 532 642 </> 533 643 ); 534 644 }
+32 -4
src/routes/settings.tsx
··· 1 - import { createFileRoute } from "@tanstack/react-router"; 1 + import { createFileRoute, useNavigate } from "@tanstack/react-router"; 2 2 import { useAtom, useAtomValue, useSetAtom } from "jotai"; 3 3 import { Slider, Switch } from "radix-ui"; 4 4 import { useEffect, useState } from "react"; ··· 21 21 videoCDNAtom, 22 22 } from "~/utils/atoms"; 23 23 24 + import { MaterialNavItem } from "./__root"; 25 + 24 26 export const Route = createFileRoute("/settings")({ 25 27 component: Settings, 26 28 }); 27 29 28 30 export function Settings() { 31 + const navigate = useNavigate(); 29 32 return ( 30 33 <> 31 34 <Header ··· 41 44 <div className="lg:hidden"> 42 45 <Login /> 43 46 </div> 47 + <div className="sm:hidden flex flex-col justify-around mt-4"> 48 + <SettingHeading title="Other Pages" top /> 49 + <MaterialNavItem 50 + InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 51 + ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 52 + active={false} 53 + onClickCallbback={() => 54 + navigate({ 55 + to: "/feeds", 56 + //params: { did: agent.assertDid }, 57 + }) 58 + } 59 + text="Feeds" 60 + /> 61 + <MaterialNavItem 62 + InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />} 63 + ActiveIcon={<IconMdiShield className="w-6 h-6" />} 64 + active={false} 65 + onClickCallbback={() => 66 + navigate({ 67 + to: "/moderation", 68 + //params: { did: agent.assertDid }, 69 + }) 70 + } 71 + text="Moderation" 72 + /> 73 + </div> 44 74 <div className="h-4" /> 45 75 46 76 <SettingHeading title="Personalization" top /> ··· 102 132 <SwitchSetting 103 133 atom={enableWafrnTextAtom} 104 134 title={"Wafrn Text"} 105 - description={ 106 - "Show the original text of posts from Wafrn instances" 107 - } 135 + description={"Show the original text of posts from Wafrn instances"} 108 136 //init={false} 109 137 /> 110 138 <p className="text-gray-500 dark:text-gray-400 py-4 px-4 text-sm border rounded-xl mx-4 mt-8 mb-4">