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.

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 + }