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}