+2
src/auto-imports.d.ts
+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
+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
+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
+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
+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
+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
+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
+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">