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