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