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