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