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