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