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

about page

+262
+21
src/routeTree.gen.ts
··· 14 14 import { Route as NotificationsRouteImport } from './routes/notifications' 15 15 import { Route as ModerationRouteImport } from './routes/moderation' 16 16 import { Route as FeedsRouteImport } from './routes/feeds' 17 + import { Route as AboutRouteImport } from './routes/about' 17 18 import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout' 18 19 import { Route as IndexRouteImport } from './routes/index' 19 20 import { Route as CallbackIndexRouteImport } from './routes/callback/index' ··· 53 54 const FeedsRoute = FeedsRouteImport.update({ 54 55 id: '/feeds', 55 56 path: '/feeds', 57 + getParentRoute: () => rootRouteImport, 58 + } as any) 59 + const AboutRoute = AboutRouteImport.update({ 60 + id: '/about', 61 + path: '/about', 56 62 getParentRoute: () => rootRouteImport, 57 63 } as any) 58 64 const PathlessLayoutRoute = PathlessLayoutRouteImport.update({ ··· 138 144 139 145 export interface FileRoutesByFullPath { 140 146 '/': typeof IndexRoute 147 + '/about': typeof AboutRoute 141 148 '/feeds': typeof FeedsRoute 142 149 '/moderation': typeof ModerationRoute 143 150 '/notifications': typeof NotificationsRoute ··· 158 165 } 159 166 export interface FileRoutesByTo { 160 167 '/': typeof IndexRoute 168 + '/about': typeof AboutRoute 161 169 '/feeds': typeof FeedsRoute 162 170 '/moderation': typeof ModerationRoute 163 171 '/notifications': typeof NotificationsRoute ··· 180 188 __root__: typeof rootRouteImport 181 189 '/': typeof IndexRoute 182 190 '/_pathlessLayout': typeof PathlessLayoutRouteWithChildren 191 + '/about': typeof AboutRoute 183 192 '/feeds': typeof FeedsRoute 184 193 '/moderation': typeof ModerationRoute 185 194 '/notifications': typeof NotificationsRoute ··· 203 212 fileRoutesByFullPath: FileRoutesByFullPath 204 213 fullPaths: 205 214 | '/' 215 + | '/about' 206 216 | '/feeds' 207 217 | '/moderation' 208 218 | '/notifications' ··· 223 233 fileRoutesByTo: FileRoutesByTo 224 234 to: 225 235 | '/' 236 + | '/about' 226 237 | '/feeds' 227 238 | '/moderation' 228 239 | '/notifications' ··· 244 255 | '__root__' 245 256 | '/' 246 257 | '/_pathlessLayout' 258 + | '/about' 247 259 | '/feeds' 248 260 | '/moderation' 249 261 | '/notifications' ··· 267 279 export interface RootRouteChildren { 268 280 IndexRoute: typeof IndexRoute 269 281 PathlessLayoutRoute: typeof PathlessLayoutRouteWithChildren 282 + AboutRoute: typeof AboutRoute 270 283 FeedsRoute: typeof FeedsRoute 271 284 ModerationRoute: typeof ModerationRoute 272 285 NotificationsRoute: typeof NotificationsRoute ··· 315 328 path: '/feeds' 316 329 fullPath: '/feeds' 317 330 preLoaderRoute: typeof FeedsRouteImport 331 + parentRoute: typeof rootRouteImport 332 + } 333 + '/about': { 334 + id: '/about' 335 + path: '/about' 336 + fullPath: '/about' 337 + preLoaderRoute: typeof AboutRouteImport 318 338 parentRoute: typeof rootRouteImport 319 339 } 320 340 '/_pathlessLayout': { ··· 475 495 const rootRouteChildren: RootRouteChildren = { 476 496 IndexRoute: IndexRoute, 477 497 PathlessLayoutRoute: PathlessLayoutRouteWithChildren, 498 + AboutRoute: AboutRoute, 478 499 FeedsRoute: FeedsRoute, 479 500 ModerationRoute: ModerationRoute, 480 501 NotificationsRoute: NotificationsRoute,
+241
src/routes/about.tsx
··· 1 + import { createFileRoute } from '@tanstack/react-router' 2 + 3 + import { FORCED_LABELER_DIDS, HOST_ABOUT_MARKDOWN, HOST_ADMIN, HOST_DESCRIPTION, HOST_HERO, HOST_LABELMERGE, HOST_SIGNUP_PDS } from '~/../policy'; 4 + import { Header } from '~/components/Header'; 5 + import { defaultconstellationURL, defaultImgCDN, defaultLycanURL, defaultslingshotURL, defaultVideoCDN } from '~/utils/atoms'; 6 + 7 + import { ProfileSmall } from './__root'; 8 + import { NotificationItem } from './notifications'; 9 + //import { SettingHeading } from './settings'; 10 + 11 + export const Route = createFileRoute('/about')({ 12 + component: RouteComponent, 13 + }) 14 + 15 + function RouteComponent() { 16 + return ( 17 + <div className=""> 18 + <Header 19 + title={`About ${window.location.host}`} 20 + backButtonCallback={() => { 21 + if (window.history.length > 1) { 22 + window.history.back(); 23 + } else { 24 + window.location.assign("/"); 25 + } 26 + }} 27 + bottomBorderDisabled={false} 28 + /> 29 + <div className="flex flex-col justify-around mt-4 mx-4 gap-4"> 30 + <img className="rounded-sm" src={HOST_HERO} /> 31 + <span className=" text-gray-500 dark:text-gray-400 leading-tight"><span className=" font-bold">{window.location.host}</span> is a Red Dwarf instance that you can use to participate in the Bluesky social network.</span> 32 + {/* <img className="rounded-sm" src={HOST_HERO} /> */} 33 + <span className=" text-gray-500 dark:text-gray-400">{HOST_DESCRIPTION}</span> 34 + <div className="flex flex-col gap-1 p-4 border-1 border-gray-200 dark:border-gray-700 rounded-3xl"> 35 + <span className="text-gray-500 dark:text-gray-400 font-bold">ADMINISTERED BY:</span> 36 + <ProfileSmall did={HOST_ADMIN} /> 37 + </div> 38 + 39 + <PolicyMarkdown source={HOST_ABOUT_MARKDOWN} /> 40 + </div> 41 + </div> 42 + ) 43 + } 44 + 45 + const REQUIRED_COMPONENTS = ["PolicyViewer"]; 46 + 47 + const COMPONENT_MAP: Record<string, React.FC> = { 48 + // todo replace with actual policy viewer 49 + PolicyViewer: () => <PolicyViewer />, 50 + }; 51 + 52 + function PolicyViewer() { 53 + return ( 54 + <> 55 + {/* TODO: render all of the layered overlay enforced moderation stuff here or something idk. 56 + still waiting on the server-sided queryLabels proxy and layered moderation spec and also feature bounded moderation spec to finish */} 57 + <PolicyRenderer /> 58 + </> 59 + ) 60 + } 61 + 62 + function assertRequiredComponents(input: string) { 63 + for (const name of REQUIRED_COMPONENTS) { 64 + const pattern = new RegExp(`<${name}\\s*/>`); 65 + if (!pattern.test(input)) { 66 + throw new Error( 67 + `Missing required policy component: <${name} />` 68 + ); 69 + } 70 + } 71 + } 72 + 73 + function renderInline(text: string) { 74 + const parts: React.ReactNode[] = []; 75 + let lastIndex = 0; 76 + 77 + const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; 78 + let match; 79 + 80 + while ((match = linkRegex.exec(text))) { 81 + const [full, label, url] = match; 82 + const start = match.index; 83 + 84 + if (start > lastIndex) { 85 + parts.push(text.slice(lastIndex, start)); 86 + } 87 + 88 + parts.push( 89 + <a key={start} href={url} className="underline" style={{color: "var(--link-text-color)"}}> 90 + {label} 91 + </a> 92 + ); 93 + 94 + lastIndex = start + full.length; 95 + } 96 + 97 + if (lastIndex < text.length) { 98 + parts.push(text.slice(lastIndex)); 99 + } 100 + 101 + return parts; 102 + } 103 + export function Heading2({title}:{title:string}){ 104 + return ( 105 + <span className="text-gray-700 dark:text-gray-300 font-medium text-xl pt-2 pb-1"> 106 + {title} 107 + </span> 108 + ) 109 + } 110 + export function Heading3({title}:{title:string}){ 111 + return ( 112 + <span className="text-gray-700 dark:text-gray-300 font-medium text-lg pt-2 pb-1"> 113 + {title} 114 + </span> 115 + ) 116 + } 117 + export function Heading4({title}:{title:string}){ 118 + return ( 119 + <span className="text-gray-600 dark:text-gray-400 font-medium text pt-0.5 pb-0"> 120 + {title} 121 + </span> 122 + ) 123 + } 124 + export function PolicyMarkdown({ source }: { source: string }) { 125 + assertRequiredComponents(source); 126 + 127 + const blocks = source 128 + .split(/\n{2,}/) // 2+ line breaks = new block 129 + .map(b => b.trim()) 130 + .filter(Boolean); 131 + 132 + return ( 133 + <div className="policy-doc flex flex-col gap-2"> 134 + {blocks.map((block, i) => { 135 + // Section heading 136 + if (block.startsWith("## ")) { 137 + const title = block.slice(3).trim(); 138 + return ( 139 + <Heading2 key={i} title={title} /> 140 + ); 141 + } 142 + 143 + // Self-closing component 144 + const componentMatch = block.match(/^<([A-Z][A-Za-z0-9_]*)\s*\/>$/); 145 + if (componentMatch) { 146 + const name = componentMatch[1]; 147 + const Component = COMPONENT_MAP[name]; 148 + 149 + if (!Component) { 150 + throw new Error(`Unknown policy component: <${name} />`); 151 + } 152 + 153 + return <Component key={i} />; 154 + } 155 + 156 + // Paragraph 157 + return ( 158 + <p key={i} className="text-gray-500 dark:text-gray-400"> 159 + {renderInline(block)} 160 + </p> 161 + ); 162 + })} 163 + </div> 164 + ); 165 + } 166 + 167 + 168 + function PolicyRenderer(){ 169 + // 170 + // policy.ts vars to show: 171 + 172 + // endorsed feeds (or should it be part of unauthed default experience?) 173 + // endorsed feeds (should be shown in the explore tab too in lieu of feed discovery) 174 + // - [ ] HOST_UNAUTHED_DEFAULT_FEEDS 175 + // endorsed PDS 176 + // - [ ] HOST_SIGNUP_PDS 177 + // todo move the other default services into policy.ts 178 + // todo re- sort policy.ts according to this component 179 + // also the default services used like microcosm stuff and lycan and maybe the reliance of an appview for search or some other hting 180 + 181 + // default general host moderation policies 182 + // todo: layerd moderataion later pls thanks 183 + // show the labelmerge insstance responsible 184 + // - [ ] HOST_LABELMERGE 185 + // show both the whitelisted source and labeler dids in the same spot. 186 + // like on hover / click it opens a dialog / popover to show what authority the labeler has 187 + // - [x] FORCED_LABELER_DIDS 188 + // - [ ] FORCE_HIDE_LABELS_WHITELISTED_SOURCE 189 + // - [ ] FORCE_HIDE_LABELS 190 + const hostmandate = FORCED_LABELER_DIDS; 191 + 192 + // unauthed experience 193 + // - [ ] UNAUTHED_FORCE_WARN_LABELS 194 + // - [ ] UNAUTHED_PREVENT_OPENING_WARNS 195 + 196 + 197 + return ( 198 + <> 199 + {/* settings heading or about heading? */} 200 + <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 + </div> 223 + {/* {hostmandate && (<Heading2 title="Host-Mandated Labelers" />)} */} 224 + <Heading3 title="General Moderation" /> 225 + {hostmandate && (<Heading4 title="Host-Mandated Labelers" />)} 226 + {hostmandate?.map((labeler) => { 227 + return ( 228 + // todo this sucks 229 + <NotificationItem 230 + key={labeler} 231 + notification={labeler} 232 + labeler={true} 233 + disablefollow={true} 234 + /> 235 + ); 236 + })} 237 + <div className='h-[300px] w-auto' /> 238 + 239 + </> 240 + ) 241 + }