forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React from 'react'
2import {View} from 'react-native'
3import {
4 type AppBskyActorDefs,
5 type AppBskyFeedDefs,
6 type AppBskyFeedPost,
7 type ComAtprotoLabelDefs,
8 interpretLabelValueDefinition,
9 type LabelPreference,
10 LABELS,
11 mock,
12 moderatePost,
13 moderateProfile,
14 type ModerationBehavior,
15 type ModerationDecision,
16 type ModerationOpts,
17 RichText,
18} from '@atproto/api'
19import {msg} from '@lingui/macro'
20import {useLingui} from '@lingui/react'
21
22import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
23import {
24 type CommonNavigatorParams,
25 type NativeStackScreenProps,
26} from '#/lib/routes/types'
27import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
28import {useModerationOpts} from '#/state/preferences/moderation-opts'
29import {moderationOptsOverrideContext} from '#/state/preferences/moderation-opts'
30import {type FeedNotification} from '#/state/queries/notifications/types'
31import {
32 groupNotifications,
33 shouldFilterNotif,
34} from '#/state/queries/notifications/util'
35import {threadPost} from '#/state/queries/usePostThread/views'
36import {useSession} from '#/state/session'
37import {CenteredView, ScrollView} from '#/view/com/util/Views'
38import {ThreadItemAnchor} from '#/screens/PostThread/components/ThreadItemAnchor'
39import {ThreadItemPost} from '#/screens/PostThread/components/ThreadItemPost'
40import {ProfileHeaderStandard} from '#/screens/Profile/Header/ProfileHeaderStandard'
41import {atoms as a, useTheme} from '#/alf'
42import {Button, ButtonIcon, ButtonText} from '#/components/Button'
43import {Divider} from '#/components/Divider'
44import * as Toggle from '#/components/forms/Toggle'
45import * as ToggleButton from '#/components/forms/ToggleButton'
46import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
47import {
48 ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottom,
49 ChevronTop_Stroke2_Corner0_Rounded as ChevronTop,
50} from '#/components/icons/Chevron'
51import * as Layout from '#/components/Layout'
52import * as ProfileCard from '#/components/ProfileCard'
53import {H1, H3, P, Text} from '#/components/Typography'
54import {ScreenHider} from '../../components/moderation/ScreenHider'
55import {NotificationFeedItem} from '../com/notifications/NotificationFeedItem'
56import {PostFeedItem} from '../com/posts/PostFeedItem'
57
58const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
59 LABELS,
60) as (keyof typeof LABELS)[]
61
62export const DebugModScreen = ({}: NativeStackScreenProps<
63 CommonNavigatorParams,
64 'DebugMod'
65>) => {
66 const t = useTheme()
67 const [scenario, setScenario] = React.useState<string[]>(['label'])
68 const [scenarioSwitches, setScenarioSwitches] = React.useState<string[]>([])
69 const [label, setLabel] = React.useState<string[]>([LABEL_VALUES[0]])
70 const [target, setTarget] = React.useState<string[]>(['account'])
71 const [visibility, setVisiblity] = React.useState<string[]>(['warn'])
72 const [customLabelDef, setCustomLabelDef] =
73 React.useState<ComAtprotoLabelDefs.LabelValueDefinition>({
74 identifier: 'custom',
75 blurs: 'content',
76 severity: 'alert',
77 defaultSetting: 'warn',
78 locales: [
79 {
80 lang: 'en',
81 name: 'Custom label',
82 description: 'A custom label created in this test environment',
83 },
84 ],
85 })
86 const [view, setView] = React.useState<string[]>(['post'])
87 const labelStrings = useGlobalLabelStrings()
88 const {currentAccount} = useSession()
89
90 const enableSquareButtons = useEnableSquareButtons()
91
92 const isTargetMe =
93 scenario[0] === 'label' && scenarioSwitches.includes('targetMe')
94 const isSelfLabel =
95 scenario[0] === 'label' && scenarioSwitches.includes('selfLabel')
96 const noAdult =
97 scenario[0] === 'label' && scenarioSwitches.includes('noAdult')
98 const isLoggedOut =
99 scenario[0] === 'label' && scenarioSwitches.includes('loggedOut')
100 const isFollowing = scenarioSwitches.includes('following')
101
102 const did =
103 isTargetMe && currentAccount ? currentAccount.did : 'did:web:bob.test'
104
105 const profile = React.useMemo(() => {
106 const mockedProfile = mock.profileViewBasic({
107 handle: `bob.test`,
108 displayName: 'Bob Robertson',
109 description: 'User with this as their bio',
110 labels:
111 scenario[0] === 'label' && target[0] === 'account'
112 ? [
113 mock.label({
114 src: isSelfLabel ? did : undefined,
115 val: label[0],
116 uri: `at://${did}/`,
117 }),
118 ]
119 : scenario[0] === 'label' && target[0] === 'profile'
120 ? [
121 mock.label({
122 src: isSelfLabel ? did : undefined,
123 val: label[0],
124 uri: `at://${did}/app.bsky.actor.profile/self`,
125 }),
126 ]
127 : undefined,
128 viewer: mock.actorViewerState({
129 following: isFollowing
130 ? `at://${currentAccount?.did || ''}/app.bsky.graph.follow/1234`
131 : undefined,
132 muted: scenario[0] === 'mute',
133 mutedByList: undefined,
134 blockedBy: undefined,
135 blocking:
136 scenario[0] === 'block'
137 ? `at://did:web:alice.test/app.bsky.actor.block/fake`
138 : undefined,
139 blockingByList: undefined,
140 }),
141 })
142 mockedProfile.did = did
143 mockedProfile.avatar = 'https://bsky.social/about/images/favicon-32x32.png'
144 // @ts-expect-error ProfileViewBasic is close enough -esb
145 mockedProfile.banner =
146 'https://bsky.social/about/images/social-card-default-gradient.png'
147 return mockedProfile
148 }, [scenario, target, label, isSelfLabel, did, isFollowing, currentAccount])
149
150 const post = React.useMemo(() => {
151 return mock.postView({
152 record: mock.post({
153 text: "This is the body of the post. It's where the text goes. You get the idea.",
154 }),
155 author: profile,
156 labels:
157 scenario[0] === 'label' && target[0] === 'post'
158 ? [
159 mock.label({
160 src: isSelfLabel ? did : undefined,
161 val: label[0],
162 uri: `at://${did}/app.bsky.feed.post/fake`,
163 }),
164 ]
165 : undefined,
166 embed:
167 target[0] === 'embed'
168 ? mock.embedRecordView({
169 record: mock.post({
170 text: 'Embed',
171 }),
172 labels:
173 scenario[0] === 'label' && target[0] === 'embed'
174 ? [
175 mock.label({
176 src: isSelfLabel ? did : undefined,
177 val: label[0],
178 uri: `at://${did}/app.bsky.feed.post/fake`,
179 }),
180 ]
181 : undefined,
182 author: profile,
183 })
184 : {
185 $type: 'app.bsky.embed.images#view',
186 images: [
187 {
188 thumb:
189 'https://bsky.social/about/images/social-card-default-gradient.png',
190 fullsize:
191 'https://bsky.social/about/images/social-card-default-gradient.png',
192 alt: '',
193 },
194 ],
195 },
196 })
197 }, [scenario, label, target, profile, isSelfLabel, did])
198
199 const replyNotif = React.useMemo(() => {
200 const notif = mock.replyNotification({
201 record: mock.post({
202 text: "This is the body of the post. It's where the text goes. You get the idea.",
203 reply: {
204 parent: {
205 uri: `at://${did}/app.bsky.feed.post/fake-parent`,
206 cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
207 },
208 root: {
209 uri: `at://${did}/app.bsky.feed.post/fake-parent`,
210 cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
211 },
212 },
213 }),
214 author: profile,
215 labels:
216 scenario[0] === 'label' && target[0] === 'post'
217 ? [
218 mock.label({
219 src: isSelfLabel ? did : undefined,
220 val: label[0],
221 uri: `at://${did}/app.bsky.feed.post/fake`,
222 }),
223 ]
224 : undefined,
225 })
226 const [item] = groupNotifications([notif])
227 item.subject = mock.postView({
228 record: notif.record as AppBskyFeedPost.Record,
229 author: profile,
230 labels: notif.labels,
231 })
232 return item
233 }, [scenario, label, target, profile, isSelfLabel, did])
234
235 const followNotif = React.useMemo(() => {
236 const notif = mock.followNotification({
237 author: profile,
238 subjectDid: currentAccount?.did || '',
239 })
240 const [item] = groupNotifications([notif])
241 return item
242 }, [profile, currentAccount])
243
244 const modOpts = React.useMemo(() => {
245 return {
246 userDid: isLoggedOut ? '' : isTargetMe ? did : 'did:web:alice.test',
247 prefs: {
248 adultContentEnabled: !noAdult,
249 labels: {
250 [label[0]]: visibility[0] as LabelPreference,
251 },
252 labelers: [
253 {
254 did: 'did:plc:fake-labeler',
255 labels: {[label[0]]: visibility[0] as LabelPreference},
256 },
257 ],
258 mutedWords: [],
259 hiddenPosts: [],
260 },
261 labelDefs: {
262 'did:plc:fake-labeler': [
263 interpretLabelValueDefinition(customLabelDef, 'did:plc:fake-labeler'),
264 ],
265 },
266 }
267 }, [label, visibility, noAdult, isLoggedOut, isTargetMe, did, customLabelDef])
268
269 const profileModeration = React.useMemo(() => {
270 return moderateProfile(profile, modOpts)
271 }, [profile, modOpts])
272 const postModeration = React.useMemo(() => {
273 return moderatePost(post, modOpts)
274 }, [post, modOpts])
275
276 return (
277 <Layout.Screen>
278 <moderationOptsOverrideContext.Provider value={modOpts}>
279 <ScrollView>
280 <CenteredView style={[t.atoms.bg, a.px_lg, a.py_lg]}>
281 <H1 style={[a.text_5xl, a.font_semi_bold, a.pb_lg]}>
282 Moderation states
283 </H1>
284
285 <Heading title="" subtitle="Scenario" />
286 <ToggleButton.Group
287 label="Scenario"
288 values={scenario}
289 onChange={setScenario}>
290 <ToggleButton.Button name="label" label="Label">
291 <ToggleButton.ButtonText>Label</ToggleButton.ButtonText>
292 </ToggleButton.Button>
293 <ToggleButton.Button name="block" label="Block">
294 <ToggleButton.ButtonText>Block</ToggleButton.ButtonText>
295 </ToggleButton.Button>
296 <ToggleButton.Button name="mute" label="Mute">
297 <ToggleButton.ButtonText>Mute</ToggleButton.ButtonText>
298 </ToggleButton.Button>
299 </ToggleButton.Group>
300
301 {scenario[0] === 'label' && (
302 <>
303 <View
304 style={[
305 a.border,
306 a.rounded_sm,
307 a.mt_lg,
308 a.mb_lg,
309 a.p_lg,
310 t.atoms.border_contrast_medium,
311 ]}>
312 <Toggle.Group
313 label="Toggle"
314 type="radio"
315 values={label}
316 onChange={setLabel}>
317 <View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
318 {LABEL_VALUES.map(labelValue => {
319 let targetFixed = target[0]
320 if (
321 targetFixed !== 'account' &&
322 targetFixed !== 'profile'
323 ) {
324 targetFixed = 'content'
325 }
326 const disabled =
327 isSelfLabel &&
328 LABELS[labelValue].flags.includes('no-self')
329 return (
330 <Toggle.Item
331 key={labelValue}
332 name={labelValue}
333 label={labelStrings[labelValue].name}
334 disabled={disabled}
335 style={disabled ? {opacity: 0.5} : undefined}>
336 <Toggle.Radio />
337 <Toggle.LabelText>{labelValue}</Toggle.LabelText>
338 </Toggle.Item>
339 )
340 })}
341 <Toggle.Item
342 name="custom"
343 label="Custom label"
344 disabled={isSelfLabel}
345 style={isSelfLabel ? {opacity: 0.5} : undefined}>
346 <Toggle.Radio />
347 <Toggle.LabelText>Custom label</Toggle.LabelText>
348 </Toggle.Item>
349 </View>
350 </Toggle.Group>
351
352 {label[0] === 'custom' ? (
353 <CustomLabelForm
354 def={customLabelDef}
355 setDef={setCustomLabelDef}
356 />
357 ) : (
358 <>
359 <View style={{height: 10}} />
360 <Divider />
361 </>
362 )}
363
364 <View style={{height: 10}} />
365
366 <SmallToggler label="Advanced">
367 <Toggle.Group
368 label="Toggle"
369 type="checkbox"
370 values={scenarioSwitches}
371 onChange={setScenarioSwitches}>
372 <View
373 style={[a.gap_md, a.flex_row, a.flex_wrap, a.pt_md]}>
374 <Toggle.Item name="targetMe" label="Target is me">
375 <Toggle.Checkbox />
376 <Toggle.LabelText>Target is me</Toggle.LabelText>
377 </Toggle.Item>
378 <Toggle.Item name="following" label="Following target">
379 <Toggle.Checkbox />
380 <Toggle.LabelText>Following target</Toggle.LabelText>
381 </Toggle.Item>
382 <Toggle.Item name="selfLabel" label="Self label">
383 <Toggle.Checkbox />
384 <Toggle.LabelText>Self label</Toggle.LabelText>
385 </Toggle.Item>
386 <Toggle.Item name="noAdult" label="Adult disabled">
387 <Toggle.Checkbox />
388 <Toggle.LabelText>Adult disabled</Toggle.LabelText>
389 </Toggle.Item>
390 <Toggle.Item name="loggedOut" label="Signed out">
391 <Toggle.Checkbox />
392 <Toggle.LabelText>Signed out</Toggle.LabelText>
393 </Toggle.Item>
394 </View>
395 </Toggle.Group>
396
397 {LABELS[label[0] as keyof typeof LABELS]?.configurable !==
398 false && (
399 <View style={[a.mt_md]}>
400 <Text
401 style={[
402 a.font_semi_bold,
403 a.text_xs,
404 t.atoms.text,
405 a.pb_sm,
406 ]}>
407 Preference
408 </Text>
409 <Toggle.Group
410 label="Preference"
411 type="radio"
412 values={visibility}
413 onChange={setVisiblity}>
414 <View
415 style={[
416 a.flex_row,
417 a.gap_md,
418 a.flex_wrap,
419 a.align_center,
420 ]}>
421 <Toggle.Item name="hide" label="Hide">
422 <Toggle.Radio />
423 <Toggle.LabelText>Hide</Toggle.LabelText>
424 </Toggle.Item>
425 <Toggle.Item name="warn" label="Warn">
426 <Toggle.Radio />
427 <Toggle.LabelText>Warn</Toggle.LabelText>
428 </Toggle.Item>
429 <Toggle.Item name="ignore" label="Ignore">
430 <Toggle.Radio />
431 <Toggle.LabelText>Ignore</Toggle.LabelText>
432 </Toggle.Item>
433 </View>
434 </Toggle.Group>
435 </View>
436 )}
437 </SmallToggler>
438 </View>
439
440 <View style={[a.flex_row, a.flex_wrap, a.gap_md]}>
441 <View>
442 <Text
443 style={[
444 a.font_semi_bold,
445 a.text_xs,
446 t.atoms.text,
447 a.pl_md,
448 a.pb_xs,
449 ]}>
450 Target
451 </Text>
452 <View
453 style={[
454 a.border,
455 enableSquareButtons ? a.rounded_sm : a.rounded_full,
456 a.px_md,
457 a.py_sm,
458 t.atoms.border_contrast_medium,
459 t.atoms.bg,
460 ]}>
461 <Toggle.Group
462 label="Target"
463 type="radio"
464 values={target}
465 onChange={setTarget}>
466 <View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
467 <Toggle.Item name="account" label="Account">
468 <Toggle.Radio />
469 <Toggle.LabelText>Account</Toggle.LabelText>
470 </Toggle.Item>
471 <Toggle.Item name="profile" label="Profile">
472 <Toggle.Radio />
473 <Toggle.LabelText>Profile</Toggle.LabelText>
474 </Toggle.Item>
475 <Toggle.Item name="post" label="Post">
476 <Toggle.Radio />
477 <Toggle.LabelText>Post</Toggle.LabelText>
478 </Toggle.Item>
479 <Toggle.Item name="embed" label="Embed">
480 <Toggle.Radio />
481 <Toggle.LabelText>Embed</Toggle.LabelText>
482 </Toggle.Item>
483 </View>
484 </Toggle.Group>
485 </View>
486 </View>
487 </View>
488 </>
489 )}
490
491 <Spacer />
492
493 <Heading title="" subtitle="Results" />
494
495 <ToggleButton.Group
496 label="Results"
497 values={view}
498 onChange={setView}>
499 <ToggleButton.Button name="post" label="Post">
500 <ToggleButton.ButtonText>Post</ToggleButton.ButtonText>
501 </ToggleButton.Button>
502 <ToggleButton.Button name="notifications" label="Notifications">
503 <ToggleButton.ButtonText>Notifications</ToggleButton.ButtonText>
504 </ToggleButton.Button>
505 <ToggleButton.Button name="account" label="Account">
506 <ToggleButton.ButtonText>Account</ToggleButton.ButtonText>
507 </ToggleButton.Button>
508 <ToggleButton.Button name="data" label="Data">
509 <ToggleButton.ButtonText>Data</ToggleButton.ButtonText>
510 </ToggleButton.Button>
511 </ToggleButton.Group>
512
513 <View
514 style={[
515 a.border,
516 a.rounded_sm,
517 a.mt_lg,
518 a.p_md,
519 t.atoms.border_contrast_medium,
520 ]}>
521 {view[0] === 'post' && (
522 <>
523 <Heading title="Post" subtitle="in feed" />
524 <MockPostFeedItem post={post} moderation={postModeration} />
525
526 <Heading title="Post" subtitle="viewed directly" />
527 <MockPostThreadItem post={post} moderationOpts={modOpts} />
528
529 <Heading title="Post" subtitle="reply in thread" />
530 <MockPostThreadItem
531 post={post}
532 moderationOpts={modOpts}
533 isReply
534 />
535 </>
536 )}
537
538 {view[0] === 'notifications' && (
539 <>
540 <Heading title="Notification" subtitle="quote or reply" />
541 <MockNotifItem notif={replyNotif} moderationOpts={modOpts} />
542 <View style={{height: 20}} />
543 <Heading title="Notification" subtitle="follow or like" />
544 <MockNotifItem notif={followNotif} moderationOpts={modOpts} />
545 </>
546 )}
547
548 {view[0] === 'account' && (
549 <>
550 <Heading title="Account" subtitle="in listing" />
551 <MockAccountCard
552 profile={profile}
553 moderation={profileModeration}
554 />
555
556 <Heading title="Account" subtitle="viewing directly" />
557 <MockAccountScreen
558 profile={profile}
559 moderation={profileModeration}
560 moderationOpts={modOpts}
561 />
562 </>
563 )}
564
565 {view[0] === 'data' && (
566 <>
567 <ModerationUIView
568 label="Profile Moderation UI"
569 mod={profileModeration}
570 />
571 <ModerationUIView
572 label="Post Moderation UI"
573 mod={postModeration}
574 />
575 <DataView
576 label={label[0]}
577 data={LABELS[label[0] as keyof typeof LABELS]}
578 />
579 <DataView
580 label="Profile Moderation Data"
581 data={profileModeration}
582 />
583 <DataView
584 label="Post Moderation Data"
585 data={postModeration}
586 />
587 </>
588 )}
589 </View>
590
591 <View style={{height: 400}} />
592 </CenteredView>
593 </ScrollView>
594 </moderationOptsOverrideContext.Provider>
595 </Layout.Screen>
596 )
597}
598
599function Heading({title, subtitle}: {title: string; subtitle?: string}) {
600 const t = useTheme()
601 return (
602 <H3 style={[a.text_3xl, a.font_semi_bold, a.pb_md]}>
603 {title}{' '}
604 {!!subtitle && (
605 <H3 style={[t.atoms.text_contrast_medium, a.text_lg]}>{subtitle}</H3>
606 )}
607 </H3>
608 )
609}
610
611function CustomLabelForm({
612 def,
613 setDef,
614}: {
615 def: ComAtprotoLabelDefs.LabelValueDefinition
616 setDef: React.Dispatch<
617 React.SetStateAction<ComAtprotoLabelDefs.LabelValueDefinition>
618 >
619}) {
620 const t = useTheme()
621 const enableSquareButtons = useEnableSquareButtons()
622 return (
623 <View
624 style={[
625 a.flex_row,
626 a.flex_wrap,
627 a.gap_md,
628 t.atoms.bg_contrast_25,
629 a.rounded_md,
630 a.p_md,
631 a.mt_md,
632 ]}>
633 <View>
634 <Text
635 style={[a.font_semi_bold, a.text_xs, t.atoms.text, a.pl_md, a.pb_xs]}>
636 Blurs
637 </Text>
638 <View
639 style={[
640 a.border,
641 enableSquareButtons ? a.rounded_sm : a.rounded_full,
642 a.px_md,
643 a.py_sm,
644 t.atoms.border_contrast_medium,
645 t.atoms.bg,
646 ]}>
647 <Toggle.Group
648 label="Blurs"
649 type="radio"
650 values={[def.blurs]}
651 onChange={values => setDef(v => ({...v, blurs: values[0]}))}>
652 <View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
653 <Toggle.Item name="content" label="Content">
654 <Toggle.Radio />
655 <Toggle.LabelText>Content</Toggle.LabelText>
656 </Toggle.Item>
657 <Toggle.Item name="media" label="Media">
658 <Toggle.Radio />
659 <Toggle.LabelText>Media</Toggle.LabelText>
660 </Toggle.Item>
661 <Toggle.Item name="none" label="None">
662 <Toggle.Radio />
663 <Toggle.LabelText>None</Toggle.LabelText>
664 </Toggle.Item>
665 </View>
666 </Toggle.Group>
667 </View>
668 </View>
669 <View>
670 <Text
671 style={[a.font_semi_bold, a.text_xs, t.atoms.text, a.pl_md, a.pb_xs]}>
672 Severity
673 </Text>
674 <View
675 style={[
676 a.border,
677 enableSquareButtons ? a.rounded_sm : a.rounded_full,
678 a.px_md,
679 a.py_sm,
680 t.atoms.border_contrast_medium,
681 t.atoms.bg,
682 ]}>
683 <Toggle.Group
684 label="Severity"
685 type="radio"
686 values={[def.severity]}
687 onChange={values => setDef(v => ({...v, severity: values[0]}))}>
688 <View style={[a.flex_row, a.gap_md, a.flex_wrap, a.align_center]}>
689 <Toggle.Item name="alert" label="Alert">
690 <Toggle.Radio />
691 <Toggle.LabelText>Alert</Toggle.LabelText>
692 </Toggle.Item>
693 <Toggle.Item name="inform" label="Inform">
694 <Toggle.Radio />
695 <Toggle.LabelText>Inform</Toggle.LabelText>
696 </Toggle.Item>
697 <Toggle.Item name="none" label="None">
698 <Toggle.Radio />
699 <Toggle.LabelText>None</Toggle.LabelText>
700 </Toggle.Item>
701 </View>
702 </Toggle.Group>
703 </View>
704 </View>
705 </View>
706 )
707}
708
709function Toggler({label, children}: React.PropsWithChildren<{label: string}>) {
710 const t = useTheme()
711 const [show, setShow] = React.useState(false)
712 return (
713 <View style={a.mb_md}>
714 <View
715 style={[
716 t.atoms.border_contrast_medium,
717 a.border,
718 a.rounded_sm,
719 a.p_xs,
720 ]}>
721 <Button
722 variant="solid"
723 color="secondary"
724 label="Toggle visibility"
725 size="small"
726 onPress={() => setShow(!show)}>
727 <ButtonText>{label}</ButtonText>
728 <ButtonIcon
729 icon={show ? ChevronTop : ChevronBottom}
730 position="right"
731 />
732 </Button>
733 {show && children}
734 </View>
735 </View>
736 )
737}
738
739function SmallToggler({
740 label,
741 children,
742}: React.PropsWithChildren<{label: string}>) {
743 const [show, setShow] = React.useState(false)
744 return (
745 <View>
746 <View style={[a.flex_row]}>
747 <Button
748 variant="ghost"
749 color="secondary"
750 label="Toggle visibility"
751 size="tiny"
752 onPress={() => setShow(!show)}>
753 <ButtonText>{label}</ButtonText>
754 <ButtonIcon
755 icon={show ? ChevronTop : ChevronBottom}
756 position="right"
757 />
758 </Button>
759 </View>
760 {show && children}
761 </View>
762 )
763}
764
765function DataView({label, data}: {label: string; data: any}) {
766 return (
767 <Toggler label={label}>
768 <Text style={[{fontFamily: 'monospace'}, a.p_md]}>
769 {JSON.stringify(data, null, 2)}
770 </Text>
771 </Toggler>
772 )
773}
774
775function ModerationUIView({
776 mod,
777 label,
778}: {
779 mod: ModerationDecision
780 label: string
781}) {
782 return (
783 <Toggler label={label}>
784 <View style={a.p_lg}>
785 {[
786 'profileList',
787 'profileView',
788 'avatar',
789 'banner',
790 'displayName',
791 'contentList',
792 'contentView',
793 'contentMedia',
794 ].map(key => {
795 const ui = mod.ui(key as keyof ModerationBehavior)
796 return (
797 <View key={key} style={[a.flex_row, a.gap_md]}>
798 <Text style={[a.font_semi_bold, {width: 100}]}>{key}</Text>
799 <Flag v={ui.filter} label="Filter" />
800 <Flag v={ui.blur} label="Blur" />
801 <Flag v={ui.alert} label="Alert" />
802 <Flag v={ui.inform} label="Inform" />
803 <Flag v={ui.noOverride} label="No-override" />
804 </View>
805 )
806 })}
807 </View>
808 </Toggler>
809 )
810}
811
812function Spacer() {
813 return <View style={{height: 30}} />
814}
815
816function MockPostFeedItem({
817 post,
818 moderation,
819}: {
820 post: AppBskyFeedDefs.PostView
821 moderation: ModerationDecision
822}) {
823 const t = useTheme()
824 if (moderation.ui('contentList').filter) {
825 return (
826 <P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md, a.mb_lg]}>
827 Filtered from the feed
828 </P>
829 )
830 }
831 return (
832 <PostFeedItem
833 post={post}
834 record={post.record as AppBskyFeedPost.Record}
835 moderation={moderation}
836 parentAuthor={undefined}
837 showReplyTo={false}
838 reason={undefined}
839 feedContext={''}
840 reqId={undefined}
841 rootPost={post}
842 />
843 )
844}
845
846function MockPostThreadItem({
847 post,
848 moderationOpts,
849 isReply,
850}: {
851 post: AppBskyFeedDefs.PostView
852 moderationOpts: ModerationOpts
853 isReply?: boolean
854}) {
855 const thread = threadPost({
856 uri: post.uri,
857 depth: isReply ? 1 : 0,
858 value: {
859 $type: 'app.bsky.unspecced.defs#threadItemPost',
860 post,
861 moreParents: false,
862 moreReplies: 0,
863 opThread: false,
864 hiddenByThreadgate: false,
865 mutedByViewer: false,
866 },
867 moderationOpts,
868 threadgateHiddenReplies: new Set<string>(),
869 })
870
871 return isReply ? (
872 <ThreadItemPost item={thread} />
873 ) : (
874 <ThreadItemAnchor item={thread} />
875 )
876}
877
878function MockNotifItem({
879 notif,
880 moderationOpts,
881}: {
882 notif: FeedNotification
883 moderationOpts: ModerationOpts
884}) {
885 const t = useTheme()
886 if (shouldFilterNotif(notif.notification, moderationOpts)) {
887 return (
888 <P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md]}>
889 Filtered from the feed
890 </P>
891 )
892 }
893 return (
894 <NotificationFeedItem
895 item={notif}
896 moderationOpts={moderationOpts}
897 highlightUnread
898 />
899 )
900}
901
902function MockAccountCard({
903 profile,
904 moderation,
905}: {
906 profile: AppBskyActorDefs.ProfileViewBasic
907 moderation: ModerationDecision
908}) {
909 const t = useTheme()
910 const moderationOpts = useModerationOpts()
911
912 if (!moderationOpts) return null
913
914 if (moderation.ui('profileList').filter) {
915 return (
916 <P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md, a.mb_lg]}>
917 Filtered from the listing
918 </P>
919 )
920 }
921
922 return <ProfileCard.Card profile={profile} moderationOpts={moderationOpts} />
923}
924
925function MockAccountScreen({
926 profile,
927 moderation,
928 moderationOpts,
929}: {
930 profile: AppBskyActorDefs.ProfileViewBasic
931 moderation: ModerationDecision
932 moderationOpts: ModerationOpts
933}) {
934 const t = useTheme()
935 const {_} = useLingui()
936 return (
937 <View style={[t.atoms.border_contrast_medium, a.border, a.mb_md]}>
938 <ScreenHider
939 style={{}}
940 screenDescription={_(msg`profile`)}
941 modui={moderation.ui('profileView')}>
942 <ProfileHeaderStandard
943 // @ts-ignore ProfileViewBasic is close enough -prf
944 profile={profile}
945 moderationOpts={moderationOpts}
946 // @ts-ignore ProfileViewBasic is close enough -esb
947 descriptionRT={new RichText({text: profile.description as string})}
948 />
949 </ScreenHider>
950 </View>
951 )
952}
953
954function Flag({v, label}: {v: boolean | undefined; label: string}) {
955 const t = useTheme()
956 return (
957 <View style={[a.flex_row, a.align_center, a.gap_xs]}>
958 <View
959 style={[
960 a.justify_center,
961 a.align_center,
962 a.rounded_xs,
963 a.border,
964 t.atoms.border_contrast_medium,
965 {
966 backgroundColor: t.palette.contrast_25,
967 width: 14,
968 height: 14,
969 },
970 ]}>
971 {v && <Check size="xs" fill={t.palette.contrast_900} />}
972 </View>
973 <P style={a.text_xs}>{label}</P>
974 </View>
975 )
976}