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