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