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