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