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