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