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