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