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