3p moderation services [WIP] (#2550)
* Add modservice screen and profile-header-card * Drop the guidelines for now * Remove ununsed constants * Add label & label group descriptions * Not found state * Reorg, add icon * Subheader * Header * Complete header * Clean up * Add all groups * Fix scroll view * Dialogs side quest * Remove log * Add (WIP) debug mod page * Dialog solution * Add note * Clean up and reorganize localized moderation strings * Memoize * Add example * Add first ReportDialog screen * Report dialog step 2 * Submit * Integrate updates * Move moderation screen * Migrate buttons * Migrate everything * Rough sketch * Fix types * Update atoms values * Abstract ModerationServiceCard * Hook up data to settings page * Handle subscription * Rough enablement * Rough enablement * Some validation, fixes * More work on the mod debug screen * Hook up data * Update invalidation * Hook up data to ReportDialog * Fix native error * Refactor/rewrite the entire moderation-application system * Fix toggles * Add copyright and other option to report * Handle reports on profile vs content * Little cleanup * Get post hiding back in gear * Better loading flow on Mod screen * Clean up Mod screen * Clean up ProfileMod screen * Handle muting correctly * Update enablement on ProfileMod screen * Improve Moderation screen and dialog * Styling, handle disabled labelers * Rework list of labels on own content * Use moderateNotification() * ReportDialog updates * Fix button overflow * Simplify the ProfileModerationService ui * Mod screen design * Move moderation card from the profile header to a tab * Small tweaks to the moderation screen * Enable toggle on mod page * Add notifs to debugmod and dont filter notifs from followed users * Add moderator-service profile view * Wire up more of the modservice data to profiles * A bunch of speculative non-working UI * Cleanup: delete old code * Update ModerationDetailsDialog * Update ReportDialog * Update LabelsOnMe dialog * Handle ReportDialog load better * Rename LabelsOnMeDialog, fix close * Experiment to put labeling under a tab of a normal profile * Moderator variation of profile * Remove dead code and start moving toward latest modsdk * Remove a bunch of now-dead label strings * Update ModDebug to be a bit more intuitive and support custom labels * Minor ui tweaks * Improve consistency of display name blurring * Fix profile-card warning rendering * More debugmod UI tuning * Update to use new labeler semantics * Delete some dead code and do some refactoring * Update profile to pull from labeler definition * Implement new label config controls (wip) * Tweak ui * Implement preference controls on labelers * Rework label pref ui * Get moderation screen working * Add asyncstorage query persistence * Implement label handling * Small cleanup * Implement Likes dialog * Fix: remove text outside of text element * Cleanup * Fix likes dialog on mobile * Implement the label appeal flow * Get report flow working again with temporarily fixed report options * Update onboarding * Enforce limit of ten labeler subscriptions * Fix type errors * Fix lint errors * Improve types of RQ * Some work on Likes dialog, needs discussion * Bit of ReportDialog cleanup * Replace non-single-path SVG * Update nudity descriptions * Update to use new sdk updates * Add adult-content-enabled behavior to label config * Use the default setting of custom labels * Handle global moderation label prefs with the global settings * Fix missing postAuthor * Fix empty moderation page * Add mutewords control back to Mod screen * Tweak adult setting styles * Remove deprecated global labels * Handle underage users on mod screen * Adjust font sizes * Swap in RichText * Like button improvements * Tweaks to Labeler profile * Design tweaks for mod pref dialog * Add tertiary button color * Switch moderation UIs to tertiary color * Update mutewords and hiddenposts to use the new sdk * Add test-environment mod authority * Switch 'gore' to 'graphic-media' * Move nudity out of the adult content control * Remove focus styles from buttons - let the browser behavior handle it * Fixes to the adult content age-gating in moderaiton * Ditch tertiary button color, lighten secondary button * Fix some colors * Remove focused overrides from toggles * Liked by screen * Rework the moderationlabelpref * Fix optimistic like * Cleanup * Change how onboarding handles adult content enabled/disabled * Add special handling of the mod authorities * Tweaks * Update the default labeler avatar to a shield * Add route to go server * Avoid dups due to bad config * Fix attrs * Fix: dont try to detect link/label mismatches on post meta * Correctly show the label behavior when adult content is disabled * Readd the local hiddenPosts handling * WIP * Fix bad merge * Conten hider design tweaks * Fix text string breakage * Adjust source text in ContentHider * Fix link bug * Design tweaks to ContentHider and ModDetailsDialog * Adjust spacing of inform badges * Adjust spacing of embeds in posts * Style tweaks to post/profile alerts * Labels on me and dialog * Remove bad focus styles from post dropdown * Better spacing solution * Tune moderation UIs * Moderation UI tweaks for mobile * Move labelers query on Mod screen * Update to use new SDK appLabelers semantics * Implement report submission * Replace the report modal entirely with the report dialog * Add @ to mod details dialog handle * Bump SDK package * Remove silly type * Add to AWS build CI * Fix ToggleButton overflow * Clean up ModServiceCard, rename to LabelingServiceCard * Hackfix to translate gore labels to graphic-media * Tune content hider sizing on web desktop * Handle self labels * Fix spacing below text-only posts * Fix: send appeals to the right labeler * Give mod page links interactive states * Fix references * Remove focus handling * Remove remnant * Remove the like count from the subscribed labeler listing * Bump @atproto/api@0.11.1 * Remove extra @ * Fix: persist labels to local storage to reduce coverage gaps * update dipendencies * revert dipendencies * Add some explainers on how blocking affects labelers * Tweak copy * Fix underline color in header * Fix profile menu * Handle card overflow * Remove metrics from header * Mute 'account' not 'user' * Show metrics if self * Show the labels tab on logged out view * Fix bad merge * Use purple theming on labelers * Tighten space on LabelerCard * Set staleTime to 6hrs for labeler details * Memoize the memoizers * Drop staleTime to 60s * Move label defs into a context to reduce recomputes * Submit view tweaks * Move labeler fetch below auth * Mitigation: hardcode the bluesky moderation labeler name * Bump sdk * Add missing translated string Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Add missing translated string Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Hailey's fix for incorrect profile tabs Co-authored-by: Hailey <me@haileyok.com> * Feedback * Fix borders, add bottom space * Hailey's fix pt 2 Co-authored-by: Hailey <me@haileyok.com> * Fix post tabs * Integrate feedback pt 1 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 2 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 3 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 4 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 5 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 6 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 7 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 8 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Format * Integrate new bday modal * Use public agent for getServices * Update casing --------- Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> Co-authored-by: Hailey <me@haileyok.com>
This commit is contained in:
parent
d5ebbeb3fc
commit
20d463ff2f
165 changed files with 7034 additions and 5009 deletions
923
src/view/screens/DebugMod.tsx
Normal file
923
src/view/screens/DebugMod.tsx
Normal file
|
@ -0,0 +1,923 @@
|
|||
import React from 'react'
|
||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
LABELS,
|
||||
mock,
|
||||
moderatePost,
|
||||
moderateProfile,
|
||||
ModerationOpts,
|
||||
AppBskyActorDefs,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
LabelPreference,
|
||||
ModerationDecision,
|
||||
ModerationBehavior,
|
||||
RichText,
|
||||
ComAtprotoLabelDefs,
|
||||
interpretLabelValueDefinition,
|
||||
} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {moderationOptsOverrideContext} from '#/state/queries/preferences'
|
||||
import {useSession} from '#/state/session'
|
||||
import {FeedNotification} from '#/state/queries/notifications/types'
|
||||
import {
|
||||
groupNotifications,
|
||||
shouldFilterNotif,
|
||||
} from '#/state/queries/notifications/util'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
||||
import {H1, H3, P, Text} from '#/components/Typography'
|
||||
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {
|
||||
ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottom,
|
||||
ChevronTop_Stroke2_Corner0_Rounded as ChevronTop,
|
||||
} from '#/components/icons/Chevron'
|
||||
import {ScreenHider} from '../../components/moderation/ScreenHider'
|
||||
import {ProfileHeaderStandard} from '#/screens/Profile/Header/ProfileHeaderStandard'
|
||||
import {ProfileCard} from '../com/profile/ProfileCard'
|
||||
import {FeedItem} from '../com/posts/FeedItem'
|
||||
import {FeedItem as NotifFeedItem} from '../com/notifications/FeedItem'
|
||||
import {PostThreadItem} from '../com/post-thread/PostThreadItem'
|
||||
import {Divider} from '#/components/Divider'
|
||||
|
||||
const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
|
||||
LABELS,
|
||||
) as (keyof typeof LABELS)[]
|
||||
|
||||
export const DebugModScreen = ({}: NativeStackScreenProps<
|
||||
CommonNavigatorParams,
|
||||
'DebugMod'
|
||||
>) => {
|
||||
const t = useTheme()
|
||||
const [scenario, setScenario] = React.useState<string[]>(['label'])
|
||||
const [scenarioSwitches, setScenarioSwitches] = React.useState<string[]>([])
|
||||
const [label, setLabel] = React.useState<string[]>([LABEL_VALUES[0]])
|
||||
const [target, setTarget] = React.useState<string[]>(['account'])
|
||||
const [visibility, setVisiblity] = React.useState<string[]>(['warn'])
|
||||
const [customLabelDef, setCustomLabelDef] =
|
||||
React.useState<ComAtprotoLabelDefs.LabelValueDefinition>({
|
||||
identifier: 'custom',
|
||||
blurs: 'content',
|
||||
severity: 'alert',
|
||||
defaultSetting: 'warn',
|
||||
locales: [
|
||||
{
|
||||
lang: 'en',
|
||||
name: 'Custom label',
|
||||
description: 'A custom label created in this test environment',
|
||||
},
|
||||
],
|
||||
})
|
||||
const [view, setView] = React.useState<string[]>(['post'])
|
||||
const labelStrings = useGlobalLabelStrings()
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
const isTargetMe =
|
||||
scenario[0] === 'label' && scenarioSwitches.includes('targetMe')
|
||||
const isSelfLabel =
|
||||
scenario[0] === 'label' && scenarioSwitches.includes('selfLabel')
|
||||
const noAdult =
|
||||
scenario[0] === 'label' && scenarioSwitches.includes('noAdult')
|
||||
const isLoggedOut =
|
||||
scenario[0] === 'label' && scenarioSwitches.includes('loggedOut')
|
||||
const isFollowing = scenarioSwitches.includes('following')
|
||||
|
||||
const did =
|
||||
isTargetMe && currentAccount ? currentAccount.did : 'did:web:bob.test'
|
||||
|
||||
const profile = React.useMemo(() => {
|
||||
const mockedProfile = mock.profileViewBasic({
|
||||
handle: `bob.test`,
|
||||
displayName: 'Bob Robertson',
|
||||
description: 'User with this as their bio',
|
||||
labels:
|
||||
scenario[0] === 'label' && target[0] === 'account'
|
||||
? [
|
||||
mock.label({
|
||||
src: isSelfLabel ? did : undefined,
|
||||
val: label[0],
|
||||
uri: `at://${did}/`,
|
||||
}),
|
||||
]
|
||||
: scenario[0] === 'label' && target[0] === 'profile'
|
||||
? [
|
||||
mock.label({
|
||||
src: isSelfLabel ? did : undefined,
|
||||
val: label[0],
|
||||
uri: `at://${did}/app.bsky.actor.profile/self`,
|
||||
}),
|
||||
]
|
||||
: undefined,
|
||||
viewer: mock.actorViewerState({
|
||||
following: isFollowing
|
||||
? `at://${currentAccount?.did || ''}/app.bsky.graph.follow/1234`
|
||||
: undefined,
|
||||
muted: scenario[0] === 'mute',
|
||||
mutedByList: undefined,
|
||||
blockedBy: undefined,
|
||||
blocking:
|
||||
scenario[0] === 'block'
|
||||
? `at://did:web:alice.test/app.bsky.actor.block/fake`
|
||||
: undefined,
|
||||
blockingByList: undefined,
|
||||
}),
|
||||
})
|
||||
mockedProfile.did = did
|
||||
mockedProfile.avatar = 'https://bsky.social/about/images/favicon-32x32.png'
|
||||
mockedProfile.banner =
|
||||
'https://bsky.social/about/images/social-card-default-gradient.png'
|
||||
return mockedProfile
|
||||
}, [scenario, target, label, isSelfLabel, did, isFollowing, currentAccount])
|
||||
|
||||
const post = React.useMemo(() => {
|
||||
return mock.postView({
|
||||
record: mock.post({
|
||||
text: "This is the body of the post. It's where the text goes. You get the idea.",
|
||||
}),
|
||||
author: profile,
|
||||
labels:
|
||||
scenario[0] === 'label' && target[0] === 'post'
|
||||
? [
|
||||
mock.label({
|
||||
src: isSelfLabel ? did : undefined,
|
||||
val: label[0],
|
||||
uri: `at://${did}/app.bsky.feed.post/fake`,
|
||||
}),
|
||||
]
|
||||
: undefined,
|
||||
embed:
|
||||
target[0] === 'embed'
|
||||
? mock.embedRecordView({
|
||||
record: mock.post({
|
||||
text: 'Embed',
|
||||
}),
|
||||
labels:
|
||||
scenario[0] === 'label' && target[0] === 'embed'
|
||||
? [
|
||||
mock.label({
|
||||
src: isSelfLabel ? did : undefined,
|
||||
val: label[0],
|
||||
uri: `at://${did}/app.bsky.feed.post/fake`,
|
||||
}),
|
||||
]
|
||||
: undefined,
|
||||
author: profile,
|
||||
})
|
||||
: {
|
||||
$type: 'app.bsky.embed.images#view',
|
||||
images: [
|
||||
{
|
||||
thumb:
|
||||
'https://bsky.social/about/images/social-card-default-gradient.png',
|
||||
fullsize:
|
||||
'https://bsky.social/about/images/social-card-default-gradient.png',
|
||||
alt: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}, [scenario, label, target, profile, isSelfLabel, did])
|
||||
|
||||
const replyNotif = React.useMemo(() => {
|
||||
const notif = mock.replyNotification({
|
||||
record: mock.post({
|
||||
text: "This is the body of the post. It's where the text goes. You get the idea.",
|
||||
reply: {
|
||||
parent: {
|
||||
uri: `at://${did}/app.bsky.feed.post/fake-parent`,
|
||||
cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
|
||||
},
|
||||
root: {
|
||||
uri: `at://${did}/app.bsky.feed.post/fake-parent`,
|
||||
cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
|
||||
},
|
||||
},
|
||||
}),
|
||||
author: profile,
|
||||
labels:
|
||||
scenario[0] === 'label' && target[0] === 'post'
|
||||
? [
|
||||
mock.label({
|
||||
src: isSelfLabel ? did : undefined,
|
||||
val: label[0],
|
||||
uri: `at://${did}/app.bsky.feed.post/fake`,
|
||||
}),
|
||||
]
|
||||
: undefined,
|
||||
})
|
||||
const [item] = groupNotifications([notif])
|
||||
item.subject = mock.postView({
|
||||
record: notif.record as AppBskyFeedPost.Record,
|
||||
author: profile,
|
||||
labels: notif.labels,
|
||||
})
|
||||
return item
|
||||
}, [scenario, label, target, profile, isSelfLabel, did])
|
||||
|
||||
const followNotif = React.useMemo(() => {
|
||||
const notif = mock.followNotification({
|
||||
author: profile,
|
||||
subjectDid: currentAccount?.did || '',
|
||||
})
|
||||
const [item] = groupNotifications([notif])
|
||||
return item
|
||||
}, [profile, currentAccount])
|
||||
|
||||
const modOpts = React.useMemo(() => {
|
||||
return {
|
||||
userDid: isLoggedOut ? '' : isTargetMe ? did : 'did:web:alice.test',
|
||||
prefs: {
|
||||
adultContentEnabled: !noAdult,
|
||||
labels: {
|
||||
[label[0]]: visibility[0] as LabelPreference,
|
||||
},
|
||||
labelers: [
|
||||
{
|
||||
did: 'did:plc:fake-labeler',
|
||||
labels: {[label[0]]: visibility[0] as LabelPreference},
|
||||
},
|
||||
],
|
||||
mutedWords: [],
|
||||
hiddenPosts: [],
|
||||
},
|
||||
labelDefs: {
|
||||
'did:plc:fake-labeler': [
|
||||
interpretLabelValueDefinition(customLabelDef, 'did:plc:fake-labeler'),
|
||||
],
|
||||
},
|
||||
}
|
||||
}, [label, visibility, noAdult, isLoggedOut, isTargetMe, did, customLabelDef])
|
||||
|
||||
const profileModeration = React.useMemo(() => {
|
||||
return moderateProfile(profile, modOpts)
|
||||
}, [profile, modOpts])
|
||||
const postModeration = React.useMemo(() => {
|
||||
return moderatePost(post, modOpts)
|
||||
}, [post, modOpts])
|
||||
|
||||
return (
|
||||
<moderationOptsOverrideContext.Provider value={modOpts}>
|
||||
<ScrollView>
|
||||
<CenteredView style={[t.atoms.bg, a.px_lg, a.py_lg]}>
|
||||
<H1 style={[a.text_5xl, a.font_bold, a.pb_lg]}>Moderation states</H1>
|
||||
|
||||
<Heading title="" subtitle="Scenario" />
|
||||
<ToggleButton.Group
|
||||
label="Scenario"
|
||||
values={scenario}
|
||||
onChange={setScenario}>
|
||||
<ToggleButton.Button name="label" label="Label">
|
||||
Label
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="block" label="Block">
|
||||
Block
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="mute" label="Mute">
|
||||
Mute
|
||||
</ToggleButton.Button>
|
||||
</ToggleButton.Group>
|
||||
|
||||
{scenario[0] === 'label' && (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
a.border,
|
||||
a.rounded_sm,
|
||||
a.mt_lg,
|
||||
a.mb_lg,
|
||||
a.p_lg,
|
||||
t.atoms.border_contrast_medium,
|
||||
]}>
|
||||
<Toggle.Group
|
||||
label="Toggle"
|
||||
type="radio"
|
||||
values={label}
|
||||
onChange={setLabel}>
|
||||
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
|
||||
{LABEL_VALUES.map(labelValue => {
|
||||
let targetFixed = target[0]
|
||||
if (
|
||||
targetFixed !== 'account' &&
|
||||
targetFixed !== 'profile'
|
||||
) {
|
||||
targetFixed = 'content'
|
||||
}
|
||||
const disabled =
|
||||
isSelfLabel &&
|
||||
LABELS[labelValue].flags.includes('no-self')
|
||||
return (
|
||||
<Toggle.Item
|
||||
key={labelValue}
|
||||
name={labelValue}
|
||||
label={labelStrings[labelValue].name}
|
||||
disabled={disabled}
|
||||
style={disabled ? {opacity: 0.5} : undefined}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>{labelValue}</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
)
|
||||
})}
|
||||
<Toggle.Item
|
||||
name="custom"
|
||||
label="Custom label"
|
||||
disabled={isSelfLabel}
|
||||
style={isSelfLabel ? {opacity: 0.5} : undefined}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Custom label</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
|
||||
{label[0] === 'custom' ? (
|
||||
<CustomLabelForm
|
||||
def={customLabelDef}
|
||||
setDef={setCustomLabelDef}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<View style={{height: 10}} />
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<View style={{height: 10}} />
|
||||
|
||||
<SmallToggler label="Advanced">
|
||||
<Toggle.Group
|
||||
label="Toggle"
|
||||
type="checkbox"
|
||||
values={scenarioSwitches}
|
||||
onChange={setScenarioSwitches}>
|
||||
<View style={[a.gap_md, a.flex_row, a.flex_wrap, a.pt_md]}>
|
||||
<Toggle.Item name="targetMe" label="Target is me">
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.Label>Target is me</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="following" label="Following target">
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.Label>Following target</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="selfLabel" label="Self label">
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.Label>Self label</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="noAdult" label="Adult disabled">
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.Label>Adult disabled</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="loggedOut" label="Logged out">
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.Label>Logged out</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
|
||||
{LABELS[label[0] as keyof typeof LABELS]?.configurable !==
|
||||
false && (
|
||||
<View style={[a.mt_md]}>
|
||||
<Text
|
||||
style={[a.font_bold, a.text_xs, t.atoms.text, a.pb_sm]}>
|
||||
Preference
|
||||
</Text>
|
||||
<Toggle.Group
|
||||
label="Preference"
|
||||
type="radio"
|
||||
values={visibility}
|
||||
onChange={setVisiblity}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.gap_md,
|
||||
a.flex_wrap,
|
||||
a.align_center,
|
||||
]}>
|
||||
<Toggle.Item name="hide" label="Hide">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Hide</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="warn" label="Warn">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Warn</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="ignore" label="Ignore">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Ignore</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
</View>
|
||||
)}
|
||||
</SmallToggler>
|
||||
</View>
|
||||
|
||||
<View style={[a.flex_row, a.flex_wrap, a.gap_md]}>
|
||||
<View>
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
a.text_xs,
|
||||
t.atoms.text,
|
||||
a.pl_md,
|
||||
a.pb_xs,
|
||||
]}>
|
||||
Target
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
a.border,
|
||||
a.rounded_full,
|
||||
a.px_md,
|
||||
a.py_sm,
|
||||
t.atoms.border_contrast_medium,
|
||||
t.atoms.bg,
|
||||
]}>
|
||||
<Toggle.Group
|
||||
label="Target"
|
||||
type="radio"
|
||||
values={target}
|
||||
onChange={setTarget}>
|
||||
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
|
||||
<Toggle.Item name="account" label="Account">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Account</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="profile" label="Profile">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Profile</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="post" label="Post">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Post</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="embed" label="Embed">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Embed</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Spacer />
|
||||
|
||||
<Heading title="" subtitle="Results" />
|
||||
|
||||
<ToggleButton.Group label="Results" values={view} onChange={setView}>
|
||||
<ToggleButton.Button name="post" label="Post">
|
||||
Post
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="notifications" label="Notifications">
|
||||
Notifications
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="account" label="Account">
|
||||
Account
|
||||
</ToggleButton.Button>
|
||||
<ToggleButton.Button name="data" label="Data">
|
||||
Data
|
||||
</ToggleButton.Button>
|
||||
</ToggleButton.Group>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.border,
|
||||
a.rounded_sm,
|
||||
a.mt_lg,
|
||||
a.p_md,
|
||||
t.atoms.border_contrast_medium,
|
||||
]}>
|
||||
{view[0] === 'post' && (
|
||||
<>
|
||||
<Heading title="Post" subtitle="in feed" />
|
||||
<MockPostFeedItem post={post} moderation={postModeration} />
|
||||
|
||||
<Heading title="Post" subtitle="viewed directly" />
|
||||
<MockPostThreadItem post={post} moderation={postModeration} />
|
||||
|
||||
<Heading title="Post" subtitle="reply in thread" />
|
||||
<MockPostThreadItem
|
||||
post={post}
|
||||
moderation={postModeration}
|
||||
reply
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{view[0] === 'notifications' && (
|
||||
<>
|
||||
<Heading title="Notification" subtitle="quote or reply" />
|
||||
<MockNotifItem notif={replyNotif} moderationOpts={modOpts} />
|
||||
<View style={{height: 20}} />
|
||||
<Heading title="Notification" subtitle="follow or like" />
|
||||
<MockNotifItem notif={followNotif} moderationOpts={modOpts} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{view[0] === 'account' && (
|
||||
<>
|
||||
<Heading title="Account" subtitle="in listing" />
|
||||
<MockAccountCard
|
||||
profile={profile}
|
||||
moderation={profileModeration}
|
||||
/>
|
||||
|
||||
<Heading title="Account" subtitle="viewing directly" />
|
||||
<MockAccountScreen
|
||||
profile={profile}
|
||||
moderation={profileModeration}
|
||||
moderationOpts={modOpts}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{view[0] === 'data' && (
|
||||
<>
|
||||
<ModerationUIView
|
||||
label="Profile Moderation UI"
|
||||
mod={profileModeration}
|
||||
/>
|
||||
<ModerationUIView
|
||||
label="Post Moderation UI"
|
||||
mod={postModeration}
|
||||
/>
|
||||
<DataView
|
||||
label={label[0]}
|
||||
data={LABELS[label[0] as keyof typeof LABELS]}
|
||||
/>
|
||||
<DataView
|
||||
label="Profile Moderation Data"
|
||||
data={profileModeration}
|
||||
/>
|
||||
<DataView label="Post Moderation Data" data={postModeration} />
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={{height: 400}} />
|
||||
</CenteredView>
|
||||
</ScrollView>
|
||||
</moderationOptsOverrideContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function Heading({title, subtitle}: {title: string; subtitle?: string}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<H3 style={[a.text_3xl, a.font_bold, a.pb_md]}>
|
||||
{title}{' '}
|
||||
{!!subtitle && (
|
||||
<H3 style={[t.atoms.text_contrast_medium, a.text_lg]}>{subtitle}</H3>
|
||||
)}
|
||||
</H3>
|
||||
)
|
||||
}
|
||||
|
||||
function CustomLabelForm({
|
||||
def,
|
||||
setDef,
|
||||
}: {
|
||||
def: ComAtprotoLabelDefs.LabelValueDefinition
|
||||
setDef: React.Dispatch<
|
||||
React.SetStateAction<ComAtprotoLabelDefs.LabelValueDefinition>
|
||||
>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.flex_wrap,
|
||||
a.gap_md,
|
||||
t.atoms.bg_contrast_25,
|
||||
a.rounded_md,
|
||||
a.p_md,
|
||||
a.mt_md,
|
||||
]}>
|
||||
<View>
|
||||
<Text style={[a.font_bold, a.text_xs, t.atoms.text, a.pl_md, a.pb_xs]}>
|
||||
Blurs
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
a.border,
|
||||
a.rounded_full,
|
||||
a.px_md,
|
||||
a.py_sm,
|
||||
t.atoms.border_contrast_medium,
|
||||
t.atoms.bg,
|
||||
]}>
|
||||
<Toggle.Group
|
||||
label="Blurs"
|
||||
type="radio"
|
||||
values={[def.blurs]}
|
||||
onChange={values => setDef(v => ({...v, blurs: values[0]}))}>
|
||||
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
|
||||
<Toggle.Item name="content" label="Content">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Content</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="media" label="Media">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Media</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="none" label="None">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>None</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
<Text style={[a.font_bold, a.text_xs, t.atoms.text, a.pl_md, a.pb_xs]}>
|
||||
Severity
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
a.border,
|
||||
a.rounded_full,
|
||||
a.px_md,
|
||||
a.py_sm,
|
||||
t.atoms.border_contrast_medium,
|
||||
t.atoms.bg,
|
||||
]}>
|
||||
<Toggle.Group
|
||||
label="Severity"
|
||||
type="radio"
|
||||
values={[def.severity]}
|
||||
onChange={values => setDef(v => ({...v, severity: values[0]}))}>
|
||||
<View style={[a.flex_row, a.gap_md, a.flex_wrap, a.align_center]}>
|
||||
<Toggle.Item name="alert" label="Alert">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Alert</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="inform" label="Inform">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>Inform</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="none" label="None">
|
||||
<Toggle.Radio />
|
||||
<Toggle.Label>None</Toggle.Label>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function Toggler({label, children}: React.PropsWithChildren<{label: string}>) {
|
||||
const t = useTheme()
|
||||
const [show, setShow] = React.useState(false)
|
||||
return (
|
||||
<View style={a.mb_md}>
|
||||
<View
|
||||
style={[
|
||||
t.atoms.border_contrast_medium,
|
||||
a.border,
|
||||
a.rounded_sm,
|
||||
a.p_xs,
|
||||
]}>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
label="Toggle visibility"
|
||||
size="small"
|
||||
onPress={() => setShow(!show)}>
|
||||
<ButtonText>{label}</ButtonText>
|
||||
<ButtonIcon
|
||||
icon={show ? ChevronTop : ChevronBottom}
|
||||
position="right"
|
||||
/>
|
||||
</Button>
|
||||
{show && children}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function SmallToggler({
|
||||
label,
|
||||
children,
|
||||
}: React.PropsWithChildren<{label: string}>) {
|
||||
const [show, setShow] = React.useState(false)
|
||||
return (
|
||||
<View>
|
||||
<View style={[a.flex_row]}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
label="Toggle visibility"
|
||||
size="tiny"
|
||||
onPress={() => setShow(!show)}>
|
||||
<ButtonText>{label}</ButtonText>
|
||||
<ButtonIcon
|
||||
icon={show ? ChevronTop : ChevronBottom}
|
||||
position="right"
|
||||
/>
|
||||
</Button>
|
||||
</View>
|
||||
{show && children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function DataView({label, data}: {label: string; data: any}) {
|
||||
return (
|
||||
<Toggler label={label}>
|
||||
<Text style={[{fontFamily: 'monospace'}, a.p_md]}>
|
||||
{JSON.stringify(data, null, 2)}
|
||||
</Text>
|
||||
</Toggler>
|
||||
)
|
||||
}
|
||||
|
||||
function ModerationUIView({
|
||||
mod,
|
||||
label,
|
||||
}: {
|
||||
mod: ModerationDecision
|
||||
label: string
|
||||
}) {
|
||||
return (
|
||||
<Toggler label={label}>
|
||||
<View style={a.p_lg}>
|
||||
{[
|
||||
'profileList',
|
||||
'profileView',
|
||||
'avatar',
|
||||
'banner',
|
||||
'displayName',
|
||||
'contentList',
|
||||
'contentView',
|
||||
'contentMedia',
|
||||
].map(key => {
|
||||
const ui = mod.ui(key as keyof ModerationBehavior)
|
||||
return (
|
||||
<View key={key} style={[a.flex_row, a.gap_md]}>
|
||||
<Text style={[a.font_bold, {width: 100}]}>{key}</Text>
|
||||
<Flag v={ui.filter} label="Filter" />
|
||||
<Flag v={ui.blur} label="Blur" />
|
||||
<Flag v={ui.alert} label="Alert" />
|
||||
<Flag v={ui.inform} label="Inform" />
|
||||
<Flag v={ui.noOverride} label="No-override" />
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
</Toggler>
|
||||
)
|
||||
}
|
||||
|
||||
function Spacer() {
|
||||
return <View style={{height: 30}} />
|
||||
}
|
||||
|
||||
function MockPostFeedItem({
|
||||
post,
|
||||
moderation,
|
||||
}: {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
moderation: ModerationDecision
|
||||
}) {
|
||||
const t = useTheme()
|
||||
if (moderation.ui('contentList').filter) {
|
||||
return (
|
||||
<P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md, a.mb_lg]}>
|
||||
Filtered from the feed
|
||||
</P>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<FeedItem
|
||||
post={post}
|
||||
record={post.record as AppBskyFeedPost.Record}
|
||||
moderation={moderation}
|
||||
reason={undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MockPostThreadItem({
|
||||
post,
|
||||
reply,
|
||||
}: {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
moderation: ModerationDecision
|
||||
reply?: boolean
|
||||
}) {
|
||||
return (
|
||||
<PostThreadItem
|
||||
// @ts-ignore
|
||||
post={post}
|
||||
record={post.record as AppBskyFeedPost.Record}
|
||||
depth={reply ? 1 : 0}
|
||||
isHighlightedPost={!reply}
|
||||
treeView={false}
|
||||
prevPost={undefined}
|
||||
nextPost={undefined}
|
||||
hasPrecedingItem={false}
|
||||
onPostReply={() => {}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MockNotifItem({
|
||||
notif,
|
||||
moderationOpts,
|
||||
}: {
|
||||
notif: FeedNotification
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const t = useTheme()
|
||||
if (shouldFilterNotif(notif.notification, moderationOpts)) {
|
||||
return (
|
||||
<P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md]}>
|
||||
Filtered from the feed
|
||||
</P>
|
||||
)
|
||||
}
|
||||
return <NotifFeedItem item={notif} moderationOpts={moderationOpts} />
|
||||
}
|
||||
|
||||
function MockAccountCard({
|
||||
profile,
|
||||
moderation,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
moderation: ModerationDecision
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
if (moderation.ui('profileList').filter) {
|
||||
return (
|
||||
<P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md, a.mb_lg]}>
|
||||
Filtered from the listing
|
||||
</P>
|
||||
)
|
||||
}
|
||||
|
||||
return <ProfileCard profile={profile} />
|
||||
}
|
||||
|
||||
function MockAccountScreen({
|
||||
profile,
|
||||
moderation,
|
||||
moderationOpts,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
moderation: ModerationDecision
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<View style={[t.atoms.border_contrast_medium, a.border, a.mb_md]}>
|
||||
<ScreenHider
|
||||
style={{}}
|
||||
screenDescription={_(msg`profile`)}
|
||||
modui={moderation.ui('profileView')}>
|
||||
<ProfileHeaderStandard
|
||||
// @ts-ignore ProfileViewBasic is close enough -prf
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
descriptionRT={new RichText({text: profile.description as string})}
|
||||
/>
|
||||
</ScreenHider>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function Flag({v, label}: {v: boolean | undefined; label: string}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.rounded_xs,
|
||||
a.border,
|
||||
t.atoms.border_contrast_medium,
|
||||
{
|
||||
backgroundColor: t.palette.contrast_25,
|
||||
width: 14,
|
||||
height: 14,
|
||||
},
|
||||
]}>
|
||||
{v && <Check size="xs" fill={t.palette.contrast_900} />}
|
||||
</View>
|
||||
<P style={a.text_xs}>{label}</P>
|
||||
</View>
|
||||
)
|
||||
}
|
|
@ -1,304 +0,0 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {ComAtprotoLabelDefs} from '@atproto/api'
|
||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {s} from 'lib/styles'
|
||||
import {CenteredView} from '../com/util/Views'
|
||||
import {ViewHeader} from '../com/util/ViewHeader'
|
||||
import {Link, TextLink} from '../com/util/Link'
|
||||
import {Text} from '../com/util/text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {ToggleButton} from '../com/util/forms/ToggleButton'
|
||||
import {useSession} from '#/state/session'
|
||||
import {
|
||||
useProfileQuery,
|
||||
useProfileUpdateMutation,
|
||||
} from '#/state/queries/profile'
|
||||
import {ScrollView} from '../com/util/Views'
|
||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>
|
||||
export function ModerationScreen({}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {screen, track} = useAnalytics()
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {openModal} = useModalControls()
|
||||
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
screen('Moderation')
|
||||
setMinimalShellMode(false)
|
||||
}, [screen, setMinimalShellMode]),
|
||||
)
|
||||
|
||||
const onPressContentFiltering = React.useCallback(() => {
|
||||
track('Moderation:ContentfilteringButtonClicked')
|
||||
openModal({name: 'content-filtering-settings'})
|
||||
}, [track, openModal])
|
||||
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
s.hContentRegion,
|
||||
pal.border,
|
||||
isTabletOrDesktop ? styles.desktopContainer : pal.viewLight,
|
||||
]}
|
||||
testID="moderationScreen">
|
||||
<ViewHeader title={_(msg`Moderation`)} showOnDesktop />
|
||||
<ScrollView contentContainerStyle={[styles.noBorder]}>
|
||||
<View style={styles.spacer} />
|
||||
<TouchableOpacity
|
||||
testID="contentFilteringBtn"
|
||||
style={[styles.linkCard, pal.view]}
|
||||
onPress={onPressContentFiltering}
|
||||
accessibilityRole="tab"
|
||||
accessibilityHint=""
|
||||
accessibilityLabel={_(msg`Open content filtering settings`)}>
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="eye"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
<Text type="lg" style={pal.text}>
|
||||
<Trans>Content filtering</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="mutedWordsBtn"
|
||||
style={[styles.linkCard, pal.view]}
|
||||
onPress={() => mutedWordsDialogControl.open()}
|
||||
accessibilityRole="tab"
|
||||
accessibilityHint=""
|
||||
accessibilityLabel={_(msg`Open muted words settings`)}>
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="filter"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
<Text type="lg" style={pal.text}>
|
||||
<Trans>Muted words & tags</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Link
|
||||
testID="moderationlistsBtn"
|
||||
style={[styles.linkCard, pal.view]}
|
||||
href="/moderation/modlists">
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="users-slash"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
<Text type="lg" style={pal.text}>
|
||||
<Trans>Moderation lists</Trans>
|
||||
</Text>
|
||||
</Link>
|
||||
<Link
|
||||
testID="mutedAccountsBtn"
|
||||
style={[styles.linkCard, pal.view]}
|
||||
href="/moderation/muted-accounts">
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="user-slash"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
<Text type="lg" style={pal.text}>
|
||||
<Trans>Muted accounts</Trans>
|
||||
</Text>
|
||||
</Link>
|
||||
<Link
|
||||
testID="blockedAccountsBtn"
|
||||
style={[styles.linkCard, pal.view]}
|
||||
href="/moderation/blocked-accounts">
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="ban"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
<Text type="lg" style={pal.text}>
|
||||
<Trans>Blocked accounts</Trans>
|
||||
</Text>
|
||||
</Link>
|
||||
<Text
|
||||
type="xl-bold"
|
||||
style={[
|
||||
pal.text,
|
||||
{
|
||||
paddingHorizontal: 18,
|
||||
paddingTop: 18,
|
||||
paddingBottom: 6,
|
||||
},
|
||||
]}>
|
||||
<Trans>Logged-out visibility</Trans>
|
||||
</Text>
|
||||
<PwiOptOut />
|
||||
</ScrollView>
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
|
||||
function PwiOptOut() {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
||||
const updateProfile = useProfileUpdateMutation()
|
||||
|
||||
const isOptedOut =
|
||||
profile?.labels?.some(l => l.val === '!no-unauthenticated') || false
|
||||
const canToggle = profile && !updateProfile.isPending
|
||||
|
||||
const onToggleOptOut = React.useCallback(() => {
|
||||
if (!profile) {
|
||||
return
|
||||
}
|
||||
let wasAdded = false
|
||||
updateProfile.mutate({
|
||||
profile,
|
||||
updates: existing => {
|
||||
// create labels attr if needed
|
||||
existing.labels = ComAtprotoLabelDefs.isSelfLabels(existing.labels)
|
||||
? existing.labels
|
||||
: {
|
||||
$type: 'com.atproto.label.defs#selfLabels',
|
||||
values: [],
|
||||
}
|
||||
|
||||
// toggle the label
|
||||
const hasLabel = existing.labels.values.some(
|
||||
l => l.val === '!no-unauthenticated',
|
||||
)
|
||||
if (hasLabel) {
|
||||
wasAdded = false
|
||||
existing.labels.values = existing.labels.values.filter(
|
||||
l => l.val !== '!no-unauthenticated',
|
||||
)
|
||||
} else {
|
||||
wasAdded = true
|
||||
existing.labels.values.push({val: '!no-unauthenticated'})
|
||||
}
|
||||
|
||||
// delete if no longer needed
|
||||
if (existing.labels.values.length === 0) {
|
||||
delete existing.labels
|
||||
}
|
||||
return existing
|
||||
},
|
||||
checkCommitted: res => {
|
||||
const exists = !!res.data.labels?.some(
|
||||
l => l.val === '!no-unauthenticated',
|
||||
)
|
||||
return exists === wasAdded
|
||||
},
|
||||
})
|
||||
}, [updateProfile, profile])
|
||||
|
||||
return (
|
||||
<View style={[pal.view, styles.toggleCard]}>
|
||||
<View
|
||||
style={{flexDirection: 'row', alignItems: 'center', paddingRight: 14}}>
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
label={_(
|
||||
msg`Discourage apps from showing my account to logged-out users`,
|
||||
)}
|
||||
labelType="lg"
|
||||
isSelected={isOptedOut}
|
||||
onPress={canToggle ? onToggleOptOut : undefined}
|
||||
style={[canToggle ? undefined : {opacity: 0.5}, {flex: 1}]}
|
||||
/>
|
||||
{updateProfile.isPending && <ActivityIndicator />}
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
gap: 10,
|
||||
paddingLeft: 66,
|
||||
paddingRight: 12,
|
||||
paddingBottom: 10,
|
||||
marginBottom: 64,
|
||||
}}>
|
||||
<Text style={pal.textLight}>
|
||||
<Trans>
|
||||
Bluesky will not show your profile and posts to logged-out users.
|
||||
Other apps may not honor this request. This does not make your
|
||||
account private.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.textLight, {fontWeight: '500'}]}>
|
||||
<Trans>
|
||||
Note: Bluesky is an open and public network. This setting only
|
||||
limits the visibility of your content on the Bluesky app and
|
||||
website, and other apps may not respect this setting. Your content
|
||||
may still be shown to logged-out users by other apps and websites.
|
||||
</Trans>
|
||||
</Text>
|
||||
<TextLink
|
||||
style={pal.link}
|
||||
href="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy"
|
||||
text={_(msg`Learn more about what is public on Bluesky.`)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
desktopContainer: {
|
||||
borderLeftWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
},
|
||||
spacer: {
|
||||
height: 6,
|
||||
},
|
||||
linkCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 18,
|
||||
marginBottom: 1,
|
||||
},
|
||||
toggleCard: {
|
||||
paddingVertical: 8,
|
||||
paddingTop: 2,
|
||||
paddingHorizontal: 6,
|
||||
marginBottom: 1,
|
||||
},
|
||||
iconContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 30,
|
||||
marginRight: 12,
|
||||
},
|
||||
noBorder: {
|
||||
borderBottomWidth: 0,
|
||||
borderRightWidth: 0,
|
||||
borderLeftWidth: 0,
|
||||
borderTopWidth: 0,
|
||||
},
|
||||
})
|
|
@ -1,5 +1,5 @@
|
|||
import React, {useMemo} from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {StyleSheet} from 'react-native'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
|
@ -7,48 +7,39 @@ import {
|
|||
ModerationOpts,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {CenteredView} from '../com/util/Views'
|
||||
import {ListRef} from '../com/util/List'
|
||||
import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
|
||||
import {Feed} from 'view/com/posts/Feed'
|
||||
import {ScreenHider} from '#/components/moderation/ScreenHider'
|
||||
import {ProfileLists} from '../com/lists/ProfileLists'
|
||||
import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
|
||||
import {ProfileHeader, ProfileHeaderLoading} from '../com/profile/ProfileHeader'
|
||||
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
|
||||
import {ErrorScreen} from '../com/util/error/ErrorScreen'
|
||||
import {EmptyState} from '../com/util/EmptyState'
|
||||
import {FAB} from '../com/util/fab/FAB'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {ComposeIcon2} from 'lib/icons'
|
||||
import {useSetTitle} from 'lib/hooks/useSetTitle'
|
||||
import {combinedDisplayName} from 'lib/strings/display-names'
|
||||
import {
|
||||
FeedDescriptor,
|
||||
resetProfilePostsQueries,
|
||||
} from '#/state/queries/post-feed'
|
||||
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
|
||||
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useSession, getAgent} from '#/state/session'
|
||||
import {useModerationOpts} from '#/state/queries/preferences'
|
||||
import {useProfileExtraInfoQuery} from '#/state/queries/profile-extra-info'
|
||||
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
||||
import {useLabelerInfoQuery} from '#/state/queries/labeler'
|
||||
import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {listenSoftReset} from '#/state/events'
|
||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {isInvalidHandle} from '#/lib/strings/handles'
|
||||
|
||||
import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
|
||||
import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
|
||||
import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
|
||||
|
||||
interface SectionRef {
|
||||
scrollToTop: () => void
|
||||
}
|
||||
|
@ -148,16 +139,24 @@ function ProfileScreenLoaded({
|
|||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {openComposer} = useComposerControls()
|
||||
const {screen, track} = useAnalytics()
|
||||
const {
|
||||
data: labelerInfo,
|
||||
error: labelerError,
|
||||
isLoading: isLabelerLoading,
|
||||
} = useLabelerInfoQuery({
|
||||
did: profile.did,
|
||||
enabled: !!profile.associated?.labeler,
|
||||
})
|
||||
const [currentPage, setCurrentPage] = React.useState(0)
|
||||
const {_} = useLingui()
|
||||
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
||||
const extraInfoQuery = useProfileExtraInfoQuery(profile.did)
|
||||
const postsSectionRef = React.useRef<SectionRef>(null)
|
||||
const repliesSectionRef = React.useRef<SectionRef>(null)
|
||||
const mediaSectionRef = React.useRef<SectionRef>(null)
|
||||
const likesSectionRef = React.useRef<SectionRef>(null)
|
||||
const feedsSectionRef = React.useRef<SectionRef>(null)
|
||||
const listsSectionRef = React.useRef<SectionRef>(null)
|
||||
const labelsSectionRef = React.useRef<SectionRef>(null)
|
||||
|
||||
useSetTitle(combinedDisplayName(profile))
|
||||
|
||||
|
@ -171,44 +170,75 @@ function ProfileScreenLoaded({
|
|||
)
|
||||
|
||||
const isMe = profile.did === currentAccount?.did
|
||||
const hasLabeler = !!profile.associated?.labeler
|
||||
const showFiltersTab = hasLabeler
|
||||
const showPostsTab = true
|
||||
const showRepliesTab = hasSession
|
||||
const showMediaTab = !hasLabeler
|
||||
const showLikesTab = isMe
|
||||
const showFeedsTab = hasSession && (isMe || extraInfoQuery.data?.hasFeedgens)
|
||||
const showListsTab = hasSession && (isMe || extraInfoQuery.data?.hasLists)
|
||||
const showFeedsTab =
|
||||
hasSession && (isMe || (profile.associated?.feedgens || 0) > 0)
|
||||
const showListsTab =
|
||||
hasSession && (isMe || (profile.associated?.lists || 0) > 0)
|
||||
|
||||
const sectionTitles = useMemo<string[]>(() => {
|
||||
return [
|
||||
_(msg`Posts`),
|
||||
showFiltersTab ? _(msg`Labels`) : undefined,
|
||||
showListsTab && hasLabeler ? _(msg`Lists`) : undefined,
|
||||
showPostsTab ? _(msg`Posts`) : undefined,
|
||||
showRepliesTab ? _(msg`Replies`) : undefined,
|
||||
_(msg`Media`),
|
||||
showMediaTab ? _(msg`Media`) : undefined,
|
||||
showLikesTab ? _(msg`Likes`) : undefined,
|
||||
showFeedsTab ? _(msg`Feeds`) : undefined,
|
||||
showListsTab ? _(msg`Lists`) : undefined,
|
||||
showListsTab && !hasLabeler ? _(msg`Lists`) : undefined,
|
||||
].filter(Boolean) as string[]
|
||||
}, [showRepliesTab, showLikesTab, showFeedsTab, showListsTab, _])
|
||||
}, [
|
||||
showPostsTab,
|
||||
showRepliesTab,
|
||||
showMediaTab,
|
||||
showLikesTab,
|
||||
showFeedsTab,
|
||||
showListsTab,
|
||||
showFiltersTab,
|
||||
hasLabeler,
|
||||
_,
|
||||
])
|
||||
|
||||
let nextIndex = 0
|
||||
const postsIndex = nextIndex++
|
||||
let filtersIndex: number | null = null
|
||||
let postsIndex: number | null = null
|
||||
let repliesIndex: number | null = null
|
||||
let mediaIndex: number | null = null
|
||||
let likesIndex: number | null = null
|
||||
let feedsIndex: number | null = null
|
||||
let listsIndex: number | null = null
|
||||
if (showFiltersTab) {
|
||||
filtersIndex = nextIndex++
|
||||
}
|
||||
if (showPostsTab) {
|
||||
postsIndex = nextIndex++
|
||||
}
|
||||
if (showRepliesTab) {
|
||||
repliesIndex = nextIndex++
|
||||
}
|
||||
const mediaIndex = nextIndex++
|
||||
let likesIndex: number | null = null
|
||||
if (showMediaTab) {
|
||||
mediaIndex = nextIndex++
|
||||
}
|
||||
if (showLikesTab) {
|
||||
likesIndex = nextIndex++
|
||||
}
|
||||
let feedsIndex: number | null = null
|
||||
if (showFeedsTab) {
|
||||
feedsIndex = nextIndex++
|
||||
}
|
||||
let listsIndex: number | null = null
|
||||
if (showListsTab) {
|
||||
listsIndex = nextIndex++
|
||||
}
|
||||
|
||||
const scrollSectionToTop = React.useCallback(
|
||||
(index: number) => {
|
||||
if (index === postsIndex) {
|
||||
if (index === filtersIndex) {
|
||||
labelsSectionRef.current?.scrollToTop()
|
||||
} else if (index === postsIndex) {
|
||||
postsSectionRef.current?.scrollToTop()
|
||||
} else if (index === repliesIndex) {
|
||||
repliesSectionRef.current?.scrollToTop()
|
||||
|
@ -222,7 +252,15 @@ function ProfileScreenLoaded({
|
|||
listsSectionRef.current?.scrollToTop()
|
||||
}
|
||||
},
|
||||
[postsIndex, repliesIndex, mediaIndex, likesIndex, feedsIndex, listsIndex],
|
||||
[
|
||||
filtersIndex,
|
||||
postsIndex,
|
||||
repliesIndex,
|
||||
mediaIndex,
|
||||
likesIndex,
|
||||
feedsIndex,
|
||||
listsIndex,
|
||||
],
|
||||
)
|
||||
|
||||
useFocusEffect(
|
||||
|
@ -278,6 +316,7 @@ function ProfileScreenLoaded({
|
|||
return (
|
||||
<ProfileHeader
|
||||
profile={profile}
|
||||
labeler={labelerInfo}
|
||||
descriptionRT={hasDescription ? descriptionRT : null}
|
||||
moderationOpts={moderationOpts}
|
||||
hideBackButton={hideBackButton}
|
||||
|
@ -286,6 +325,7 @@ function ProfileScreenLoaded({
|
|||
)
|
||||
}, [
|
||||
profile,
|
||||
labelerInfo,
|
||||
descriptionRT,
|
||||
hasDescription,
|
||||
moderationOpts,
|
||||
|
@ -297,8 +337,8 @@ function ProfileScreenLoaded({
|
|||
<ScreenHider
|
||||
testID="profileView"
|
||||
style={styles.container}
|
||||
screenDescription="profile"
|
||||
moderation={moderation.account}>
|
||||
screenDescription={_(msg`profile`)}
|
||||
modui={moderation.ui('profileView')}>
|
||||
<PagerWithHeader
|
||||
testID="profilePager"
|
||||
isHeaderReady={!showPlaceholder}
|
||||
|
@ -306,19 +346,45 @@ function ProfileScreenLoaded({
|
|||
onPageSelected={onPageSelected}
|
||||
onCurrentPageSelected={onCurrentPageSelected}
|
||||
renderHeader={renderHeader}>
|
||||
{({headerHeight, isFocused, scrollElRef}) => (
|
||||
<FeedSection
|
||||
ref={postsSectionRef}
|
||||
feed={`author|${profile.did}|posts_and_author_threads`}
|
||||
headerHeight={headerHeight}
|
||||
isFocused={isFocused}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
ignoreFilterFor={profile.did}
|
||||
/>
|
||||
)}
|
||||
{showFiltersTab
|
||||
? ({headerHeight, scrollElRef}) => (
|
||||
<ProfileLabelsSection
|
||||
ref={labelsSectionRef}
|
||||
labelerInfo={labelerInfo}
|
||||
labelerError={labelerError}
|
||||
isLabelerLoading={isLabelerLoading}
|
||||
moderationOpts={moderationOpts}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
headerHeight={headerHeight}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
{showListsTab && !!profile.associated?.labeler
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
<ProfileLists
|
||||
ref={listsSectionRef}
|
||||
did={profile.did}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
headerOffset={headerHeight}
|
||||
enabled={isFocused}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
{showPostsTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
<ProfileFeedSection
|
||||
ref={postsSectionRef}
|
||||
feed={`author|${profile.did}|posts_and_author_threads`}
|
||||
headerHeight={headerHeight}
|
||||
isFocused={isFocused}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
ignoreFilterFor={profile.did}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
{showRepliesTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
<FeedSection
|
||||
<ProfileFeedSection
|
||||
ref={repliesSectionRef}
|
||||
feed={`author|${profile.did}|posts_with_replies`}
|
||||
headerHeight={headerHeight}
|
||||
|
@ -328,19 +394,21 @@ function ProfileScreenLoaded({
|
|||
/>
|
||||
)
|
||||
: null}
|
||||
{({headerHeight, isFocused, scrollElRef}) => (
|
||||
<FeedSection
|
||||
ref={mediaSectionRef}
|
||||
feed={`author|${profile.did}|posts_with_media`}
|
||||
headerHeight={headerHeight}
|
||||
isFocused={isFocused}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
ignoreFilterFor={profile.did}
|
||||
/>
|
||||
)}
|
||||
{showMediaTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
<ProfileFeedSection
|
||||
ref={mediaSectionRef}
|
||||
feed={`author|${profile.did}|posts_with_media`}
|
||||
headerHeight={headerHeight}
|
||||
isFocused={isFocused}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
ignoreFilterFor={profile.did}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
{showLikesTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
<FeedSection
|
||||
<ProfileFeedSection
|
||||
ref={likesSectionRef}
|
||||
feed={`likes|${profile.did}`}
|
||||
headerHeight={headerHeight}
|
||||
|
@ -361,7 +429,7 @@ function ProfileScreenLoaded({
|
|||
/>
|
||||
)
|
||||
: null}
|
||||
{showListsTab
|
||||
{showListsTab && !profile.associated?.labeler
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
<ProfileLists
|
||||
ref={listsSectionRef}
|
||||
|
@ -387,77 +455,6 @@ function ProfileScreenLoaded({
|
|||
)
|
||||
}
|
||||
|
||||
interface FeedSectionProps {
|
||||
feed: FeedDescriptor
|
||||
headerHeight: number
|
||||
isFocused: boolean
|
||||
scrollElRef: ListRef
|
||||
ignoreFilterFor?: string
|
||||
}
|
||||
const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
|
||||
function FeedSectionImpl(
|
||||
{feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor},
|
||||
ref,
|
||||
) {
|
||||
const {_} = useLingui()
|
||||
const queryClient = useQueryClient()
|
||||
const [hasNew, setHasNew] = React.useState(false)
|
||||
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
|
||||
|
||||
const onScrollToTop = React.useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({
|
||||
animated: isNative,
|
||||
offset: -headerHeight,
|
||||
})
|
||||
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
|
||||
setHasNew(false)
|
||||
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
scrollToTop: onScrollToTop,
|
||||
}))
|
||||
|
||||
const renderPostsEmpty = React.useCallback(() => {
|
||||
return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} />
|
||||
}, [_])
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Feed
|
||||
testID="postsFeed"
|
||||
enabled={isFocused}
|
||||
feed={feed}
|
||||
scrollElRef={scrollElRef}
|
||||
onHasNew={setHasNew}
|
||||
onScrolledDownChange={setIsScrolledDown}
|
||||
renderEmptyState={renderPostsEmpty}
|
||||
headerOffset={headerHeight}
|
||||
renderEndOfFeed={ProfileEndOfFeed}
|
||||
ignoreFilterFor={ignoreFilterFor}
|
||||
/>
|
||||
{(isScrolledDown || hasNew) && (
|
||||
<LoadLatestBtn
|
||||
onPress={onScrollToTop}
|
||||
label={_(msg`Load new posts`)}
|
||||
showIndicator={hasNew}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
function ProfileEndOfFeed() {
|
||||
const pal = usePalette('default')
|
||||
|
||||
return (
|
||||
<View style={[pal.border, {paddingTop: 32, borderTopWidth: 1}]}>
|
||||
<Text style={[pal.textLight, pal.border, {textAlign: 'center'}]}>
|
||||
<Trans>End of feed</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function useRichText(text: string): [RichTextAPI, boolean] {
|
||||
const [prevText, setPrevText] = React.useState(text)
|
||||
const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
|
||||
|
|
|
@ -35,7 +35,7 @@ import {ComposeIcon2} from 'lib/icons'
|
|||
import {logger} from '#/logger'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
||||
import {useFeedSourceInfoQuery, FeedSourceFeedInfo} from '#/state/queries/feed'
|
||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||
import {
|
||||
|
@ -155,7 +155,7 @@ export function ProfileFeedScreenInner({
|
|||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
const {openModal} = useModalControls()
|
||||
const reportDialogControl = useReportDialogControl()
|
||||
const {openComposer} = useComposerControls()
|
||||
const {track} = useAnalytics()
|
||||
const feedSectionRef = React.useRef<SectionRef>(null)
|
||||
|
@ -253,13 +253,8 @@ export function ProfileFeedScreenInner({
|
|||
}, [feedInfo, track])
|
||||
|
||||
const onPressReport = React.useCallback(() => {
|
||||
if (!feedInfo) return
|
||||
openModal({
|
||||
name: 'report',
|
||||
uri: feedInfo.uri,
|
||||
cid: feedInfo.cid,
|
||||
})
|
||||
}, [openModal, feedInfo])
|
||||
reportDialogControl.open()
|
||||
}, [reportDialogControl])
|
||||
|
||||
const onCurrentPageSelected = React.useCallback(
|
||||
(index: number) => {
|
||||
|
@ -400,6 +395,14 @@ export function ProfileFeedScreenInner({
|
|||
|
||||
return (
|
||||
<View style={s.hContentRegion}>
|
||||
<ReportDialog
|
||||
control={reportDialogControl}
|
||||
params={{
|
||||
type: 'feedgen',
|
||||
uri: feedInfo.uri,
|
||||
cid: feedInfo.cid,
|
||||
}}
|
||||
/>
|
||||
<PagerWithHeader
|
||||
items={SECTION_TITLES}
|
||||
isHeaderReady={true}
|
||||
|
|
|
@ -39,6 +39,7 @@ import {Trans, msg} from '@lingui/macro'
|
|||
import {useLingui} from '@lingui/react'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||
import {
|
||||
useListQuery,
|
||||
|
@ -236,6 +237,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||
const {_} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {currentAccount} = useSession()
|
||||
const reportDialogControl = useReportDialogControl()
|
||||
const {openModal} = useModalControls()
|
||||
const listMuteMutation = useListMuteMutation()
|
||||
const listBlockMutation = useListBlockMutation()
|
||||
|
@ -370,12 +372,8 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||
])
|
||||
|
||||
const onPressReport = useCallback(() => {
|
||||
openModal({
|
||||
name: 'report',
|
||||
uri: list.uri,
|
||||
cid: list.cid,
|
||||
})
|
||||
}, [openModal, list])
|
||||
reportDialogControl.open()
|
||||
}, [reportDialogControl])
|
||||
|
||||
const onPressShare = useCallback(() => {
|
||||
const url = toShareUrl(`/profile/${list.creator.did}/lists/${rkey}`)
|
||||
|
@ -550,6 +548,14 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||
isOwner={list.creator.did === currentAccount?.did}
|
||||
creator={list.creator}
|
||||
avatarType="list">
|
||||
<ReportDialog
|
||||
control={reportDialogControl}
|
||||
params={{
|
||||
type: 'list',
|
||||
uri: list.uri,
|
||||
cid: list.cid,
|
||||
}}
|
||||
/>
|
||||
{isCurateList || isPinned ? (
|
||||
<Button
|
||||
testID={isPinned ? 'unpinBtn' : 'pinBtn'}
|
||||
|
|
|
@ -267,6 +267,10 @@ export function SettingsScreen({}: Props) {
|
|||
navigation.navigate('Debug')
|
||||
}, [navigation])
|
||||
|
||||
const onPressDebugModeration = React.useCallback(() => {
|
||||
navigation.navigate('DebugMod')
|
||||
}, [navigation])
|
||||
|
||||
const onPressSavedFeeds = React.useCallback(() => {
|
||||
navigation.navigate('SavedFeeds')
|
||||
}, [navigation])
|
||||
|
@ -821,6 +825,16 @@ export function SettingsScreen({}: Props) {
|
|||
<Trans>Storybook</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[pal.view, styles.linkCardNoIcon]}
|
||||
onPress={onPressDebugModeration}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Open storybook page`)}
|
||||
accessibilityHint={_(msg`Opens the storybook page`)}>
|
||||
<Text type="lg" style={pal.text}>
|
||||
<Trans>Debug Moderation</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[pal.view, styles.linkCardNoIcon]}
|
||||
onPress={onPressResetPreferences}
|
||||
|
|
|
@ -129,6 +129,15 @@ export function Buttons() {
|
|||
<ButtonIcon icon={Globe} position="left" />
|
||||
<ButtonText>Link out</ButtonText>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="gradient"
|
||||
color="gradient_sky"
|
||||
size="tiny"
|
||||
label="Link out">
|
||||
<ButtonIcon icon={Globe} position="left" />
|
||||
<ButtonText>Link out</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View style={[a.flex_row, a.gap_md, a.align_start]}>
|
||||
|
@ -148,6 +157,14 @@ export function Buttons() {
|
|||
label="Link out">
|
||||
<ButtonIcon icon={ChevronLeft} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="gradient"
|
||||
color="gradient_sunset"
|
||||
size="tiny"
|
||||
shape="round"
|
||||
label="Link out">
|
||||
<ButtonIcon icon={ChevronLeft} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
color="primary"
|
||||
|
@ -164,6 +181,14 @@ export function Buttons() {
|
|||
label="Link out">
|
||||
<ButtonIcon icon={ChevronLeft} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="primary"
|
||||
size="tiny"
|
||||
shape="round"
|
||||
label="Link out">
|
||||
<ButtonIcon icon={ChevronLeft} />
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View style={[a.flex_row, a.gap_md, a.align_start]}>
|
||||
|
@ -183,6 +208,14 @@ export function Buttons() {
|
|||
label="Link out">
|
||||
<ButtonIcon icon={ChevronLeft} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="gradient"
|
||||
color="gradient_sunset"
|
||||
size="tiny"
|
||||
shape="square"
|
||||
label="Link out">
|
||||
<ButtonIcon icon={ChevronLeft} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
color="primary"
|
||||
|
@ -199,6 +232,14 @@ export function Buttons() {
|
|||
label="Link out">
|
||||
<ButtonIcon icon={ChevronLeft} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="primary"
|
||||
size="tiny"
|
||||
shape="square"
|
||||
label="Link out">
|
||||
<ButtonIcon icon={ChevronLeft} />
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
|
|
@ -67,6 +67,7 @@ export function Storybook() {
|
|||
</Button>
|
||||
</View>
|
||||
|
||||
<Dialogs />
|
||||
<ThemeProvider theme="light">
|
||||
<Theming />
|
||||
</ThemeProvider>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue