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
|
@ -47,6 +47,7 @@ interface Props extends ComponentProps<typeof TouchableOpacity> {
|
|||
anchorNoUnderline?: boolean
|
||||
navigationAction?: 'push' | 'replace' | 'navigate'
|
||||
onPointerEnter?: () => void
|
||||
onBeforePress?: () => void
|
||||
}
|
||||
|
||||
export const Link = memo(function Link({
|
||||
|
@ -60,6 +61,7 @@ export const Link = memo(function Link({
|
|||
accessible,
|
||||
anchorNoUnderline,
|
||||
navigationAction,
|
||||
onBeforePress,
|
||||
...props
|
||||
}: Props) {
|
||||
const t = useTheme()
|
||||
|
@ -70,6 +72,7 @@ export const Link = memo(function Link({
|
|||
|
||||
const onPress = React.useCallback(
|
||||
(e?: Event) => {
|
||||
onBeforePress?.()
|
||||
if (typeof href === 'string') {
|
||||
return onPressInner(
|
||||
closeModal,
|
||||
|
@ -81,7 +84,7 @@ export const Link = memo(function Link({
|
|||
)
|
||||
}
|
||||
},
|
||||
[closeModal, navigation, navigationAction, href, openLink],
|
||||
[closeModal, navigation, navigationAction, href, openLink, onBeforePress],
|
||||
)
|
||||
|
||||
if (noFeedback) {
|
||||
|
@ -262,6 +265,7 @@ interface TextLinkOnWebOnlyProps extends TextProps {
|
|||
accessibilityHint?: string
|
||||
title?: string
|
||||
navigationAction?: 'push' | 'replace' | 'navigate'
|
||||
disableMismatchWarning?: boolean
|
||||
onPointerEnter?: () => void
|
||||
}
|
||||
export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
|
||||
|
@ -273,6 +277,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
|
|||
numberOfLines,
|
||||
lineHeight,
|
||||
navigationAction,
|
||||
disableMismatchWarning,
|
||||
...props
|
||||
}: TextLinkOnWebOnlyProps) {
|
||||
if (isWeb) {
|
||||
|
@ -287,6 +292,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
|
|||
lineHeight={lineHeight}
|
||||
title={props.title}
|
||||
navigationAction={navigationAction}
|
||||
disableMismatchWarning={disableMismatchWarning}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -11,7 +11,7 @@ import {sanitizeHandle} from 'lib/strings/handles'
|
|||
import {isAndroid, isWeb} from 'platform/detection'
|
||||
import {TimeElapsed} from './TimeElapsed'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {ModerationDecision, ModerationUI} from '@atproto/api'
|
||||
import {usePrefetchProfileQuery} from '#/state/queries/profile'
|
||||
|
||||
interface PostMetaOpts {
|
||||
|
@ -21,6 +21,7 @@ interface PostMetaOpts {
|
|||
handle: string
|
||||
displayName?: string | undefined
|
||||
}
|
||||
moderation: ModerationDecision | undefined
|
||||
authorHasWarning: boolean
|
||||
postHref: string
|
||||
timestamp: string
|
||||
|
@ -55,9 +56,14 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
|||
style={[pal.text, opts.displayNameStyle]}
|
||||
numberOfLines={1}
|
||||
lineHeight={1.2}
|
||||
disableMismatchWarning
|
||||
text={
|
||||
<>
|
||||
{sanitizeDisplayName(displayName)}
|
||||
{sanitizeDisplayName(
|
||||
displayName,
|
||||
opts.moderation?.ui('displayName'),
|
||||
)}
|
||||
|
||||
<Text
|
||||
type="md"
|
||||
numberOfLines={1}
|
||||
|
|
|
@ -24,9 +24,9 @@ import {
|
|||
} from '#/components/icons/Camera'
|
||||
import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive'
|
||||
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||
import {useTheme} from '#/alf'
|
||||
import {useTheme, tokens} from '#/alf'
|
||||
|
||||
export type UserAvatarType = 'user' | 'algo' | 'list'
|
||||
export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler'
|
||||
|
||||
interface BaseUserAvatarProps {
|
||||
type?: UserAvatarType
|
||||
|
@ -101,6 +101,29 @@ let DefaultAvatar = ({
|
|||
</Svg>
|
||||
)
|
||||
}
|
||||
if (type === 'labeler') {
|
||||
return (
|
||||
<Svg
|
||||
testID="userAvatarFallback"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
stroke="none">
|
||||
<Path
|
||||
d="M28 0H4C1.79086 0 0 1.79086 0 4V28C0 30.2091 1.79086 32 4 32H28C30.2091 32 32 30.2091 32 28V4C32 1.79086 30.2091 0 28 0Z"
|
||||
fill={tokens.color.temp_purple}
|
||||
/>
|
||||
<Path
|
||||
d="M24 9.75L16 7L8 9.75V15.9123C8 20.8848 12 23 16 25.1579C20 23 24 20.8848 24 15.9123V9.75Z"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Svg
|
||||
testID="userAvatarFallback"
|
||||
|
@ -134,7 +157,7 @@ let UserAvatar = ({
|
|||
const backgroundColor = pal.colors.backgroundLight
|
||||
|
||||
const aviStyle = useMemo(() => {
|
||||
if (type === 'algo' || type === 'list') {
|
||||
if (type === 'algo' || type === 'list' || type === 'labeler') {
|
||||
return {
|
||||
width: size,
|
||||
height: size,
|
||||
|
|
|
@ -7,7 +7,7 @@ import {msg, Trans} from '@lingui/macro'
|
|||
|
||||
import {colors} from 'lib/styles'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {useTheme as useAlfTheme} from '#/alf'
|
||||
import {useTheme as useAlfTheme, tokens} from '#/alf'
|
||||
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
||||
import {
|
||||
usePhotoLibraryPermission,
|
||||
|
@ -26,10 +26,12 @@ import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/ico
|
|||
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||
|
||||
export function UserBanner({
|
||||
type,
|
||||
banner,
|
||||
moderation,
|
||||
onSelectNewBanner,
|
||||
}: {
|
||||
type?: 'labeler' | 'default'
|
||||
banner?: string | null
|
||||
moderation?: ModerationUI
|
||||
onSelectNewBanner?: (img: RNImage | null) => void
|
||||
|
@ -167,7 +169,10 @@ export function UserBanner({
|
|||
) : (
|
||||
<View
|
||||
testID="userBannerFallback"
|
||||
style={[styles.bannerImage, styles.defaultBanner]}
|
||||
style={[
|
||||
styles.bannerImage,
|
||||
type === 'labeler' ? styles.labelerBanner : styles.defaultBanner,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -191,4 +196,7 @@ const styles = StyleSheet.create({
|
|||
defaultBanner: {
|
||||
backgroundColor: '#0070ff',
|
||||
},
|
||||
labelerBanner: {
|
||||
backgroundColor: tokens.color.temp_purple,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -16,7 +16,6 @@ import * as Toast from '../Toast'
|
|||
import {EventStopper} from '../EventStopper'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {getCurrentRoute} from 'lib/routes/helpers'
|
||||
|
@ -33,6 +32,7 @@ import {useSession} from '#/state/session'
|
|||
import {isWeb} from '#/platform/detection'
|
||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
||||
|
||||
import {atoms as a, useTheme as useAlf} from '#/alf'
|
||||
import * as Menu from '#/components/Menu'
|
||||
|
@ -45,7 +45,6 @@ import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/
|
|||
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
|
||||
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
|
||||
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
|
||||
let PostDropdownBtn = ({
|
||||
testID,
|
||||
|
@ -55,7 +54,6 @@ let PostDropdownBtn = ({
|
|||
record,
|
||||
richText,
|
||||
style,
|
||||
showAppealLabelItem,
|
||||
hitSlop,
|
||||
}: {
|
||||
testID: string
|
||||
|
@ -65,7 +63,6 @@ let PostDropdownBtn = ({
|
|||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
style?: StyleProp<ViewStyle>
|
||||
showAppealLabelItem?: boolean
|
||||
hitSlop?: PressableProps['hitSlop']
|
||||
}): React.ReactNode => {
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
|
@ -73,7 +70,6 @@ let PostDropdownBtn = ({
|
|||
const alf = useAlf()
|
||||
const {_} = useLingui()
|
||||
const defaultCtrlColor = theme.palette.default.postCtrl
|
||||
const {openModal} = useModalControls()
|
||||
const langPrefs = useLanguagePrefs()
|
||||
const mutedThreads = useMutedThreads()
|
||||
const toggleThreadMute = useToggleThreadMute()
|
||||
|
@ -83,6 +79,7 @@ let PostDropdownBtn = ({
|
|||
const openLink = useOpenLink()
|
||||
const navigation = useNavigation()
|
||||
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
|
||||
const reportDialogControl = useReportDialogControl()
|
||||
const deletePromptControl = useDialogControl()
|
||||
const hidePromptControl = useDialogControl()
|
||||
const loggedOutWarningPromptControl = useDialogControl()
|
||||
|
@ -293,13 +290,7 @@ let PostDropdownBtn = ({
|
|||
<Menu.Item
|
||||
testID="postDropdownReportBtn"
|
||||
label={_(msg`Report post`)}
|
||||
onPress={() => {
|
||||
openModal({
|
||||
name: 'report',
|
||||
uri: postUri,
|
||||
cid: postCid,
|
||||
})
|
||||
}}>
|
||||
onPress={() => reportDialogControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Warning} position="right" />
|
||||
</Menu.Item>
|
||||
|
@ -314,28 +305,6 @@ let PostDropdownBtn = ({
|
|||
<Menu.ItemIcon icon={Trash} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{showAppealLabelItem && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownAppealBtn"
|
||||
label={_(msg`Appeal content warning`)}
|
||||
onPress={() => {
|
||||
openModal({
|
||||
name: 'appeal-label',
|
||||
uri: postUri,
|
||||
cid: postCid,
|
||||
})
|
||||
}}>
|
||||
<Menu.ItemText>
|
||||
{_(msg`Appeal content warning`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={CircleInfo} position="right" />
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
</Menu.Group>
|
||||
</Menu.Outer>
|
||||
</Menu.Root>
|
||||
|
@ -359,6 +328,15 @@ let PostDropdownBtn = ({
|
|||
confirmButtonCta={_(msg`Hide`)}
|
||||
/>
|
||||
|
||||
<ReportDialog
|
||||
control={reportDialogControl}
|
||||
params={{
|
||||
type: 'post',
|
||||
uri: postUri,
|
||||
cid: postCid,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={loggedOutWarningPromptControl}
|
||||
title={_(msg`Note about sharing`)}
|
||||
|
|
|
@ -1,145 +0,0 @@
|
|||
import React from 'react'
|
||||
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {ModerationUI, PostModeration} from '@atproto/api'
|
||||
import {Text} from '../text/Text'
|
||||
import {ShieldExclamation} from 'lib/icons'
|
||||
import {describeModerationCause} from 'lib/moderation'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {isPostMediaBlurred} from 'lib/moderation'
|
||||
|
||||
export function ContentHider({
|
||||
testID,
|
||||
moderation,
|
||||
moderationDecisions,
|
||||
ignoreMute,
|
||||
ignoreQuoteDecisions,
|
||||
style,
|
||||
childContainerStyle,
|
||||
children,
|
||||
}: React.PropsWithChildren<{
|
||||
testID?: string
|
||||
moderation: ModerationUI
|
||||
moderationDecisions?: PostModeration['decisions']
|
||||
ignoreMute?: boolean
|
||||
ignoreQuoteDecisions?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
childContainerStyle?: StyleProp<ViewStyle>
|
||||
}>) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const [override, setOverride] = React.useState(false)
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
if (
|
||||
!moderation.blur ||
|
||||
(ignoreMute && moderation.cause?.type === 'muted') ||
|
||||
shouldIgnoreQuote(moderationDecisions, ignoreQuoteDecisions)
|
||||
) {
|
||||
return (
|
||||
<View testID={testID} style={[styles.outer, style]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const isMute = ['muted', 'muted-word'].includes(moderation.cause?.type || '')
|
||||
const desc = describeModerationCause(moderation.cause, 'content')
|
||||
return (
|
||||
<View testID={testID} style={[styles.outer, style]}>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (!moderation.noOverride) {
|
||||
setOverride(v => !v)
|
||||
} else {
|
||||
openModal({
|
||||
name: 'moderation-details',
|
||||
context: 'content',
|
||||
moderation,
|
||||
})
|
||||
}
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityHint={
|
||||
override ? _(msg`Hide the content`) : _(msg`Show the content`)
|
||||
}
|
||||
accessibilityLabel=""
|
||||
style={[
|
||||
styles.cover,
|
||||
moderation.noOverride
|
||||
? {borderWidth: 1, borderColor: pal.colors.borderDark}
|
||||
: pal.viewLight,
|
||||
]}>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
openModal({
|
||||
name: 'moderation-details',
|
||||
context: 'content',
|
||||
moderation,
|
||||
})
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Learn more about this warning`)}
|
||||
accessibilityHint="">
|
||||
{isMute ? (
|
||||
<FontAwesomeIcon
|
||||
icon={['far', 'eye-slash']}
|
||||
size={18}
|
||||
color={pal.colors.textLight}
|
||||
/>
|
||||
) : (
|
||||
<ShieldExclamation size={18} style={pal.textLight} />
|
||||
)}
|
||||
</Pressable>
|
||||
<Text type="md" style={[pal.text, {flex: 1}]} numberOfLines={2}>
|
||||
{desc.name}
|
||||
</Text>
|
||||
<View style={styles.showBtn}>
|
||||
<Text type="lg" style={pal.link}>
|
||||
{moderation.noOverride ? (
|
||||
<Trans>Learn more</Trans>
|
||||
) : override ? (
|
||||
<Trans>Hide</Trans>
|
||||
) : (
|
||||
<Trans>Show</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
{override && <View style={childContainerStyle}>{children}</View>}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function shouldIgnoreQuote(
|
||||
decisions: PostModeration['decisions'] | undefined,
|
||||
ignore: boolean | undefined,
|
||||
): boolean {
|
||||
if (!decisions || !ignore) {
|
||||
return false
|
||||
}
|
||||
return !isPostMediaBlurred(decisions)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outer: {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
cover: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
borderRadius: 8,
|
||||
marginTop: 4,
|
||||
paddingVertical: 14,
|
||||
paddingLeft: 14,
|
||||
paddingRight: 18,
|
||||
},
|
||||
showBtn: {
|
||||
marginLeft: 'auto',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
})
|
|
@ -1,61 +0,0 @@
|
|||
import React from 'react'
|
||||
import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {ComAtprotoLabelDefs} from '@atproto/api'
|
||||
import {Text} from '../text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export function LabelInfo({
|
||||
details,
|
||||
labels,
|
||||
style,
|
||||
}: {
|
||||
details: {did: string} | {uri: string; cid: string}
|
||||
labels: ComAtprotoLabelDefs.Label[] | undefined
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
if (!labels) {
|
||||
return null
|
||||
}
|
||||
labels = labels.filter(l => !l.val.startsWith('!'))
|
||||
if (!labels.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
pal.viewLight,
|
||||
{
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 8,
|
||||
},
|
||||
style,
|
||||
]}>
|
||||
<Text type="sm" style={pal.text}>
|
||||
<Trans>
|
||||
A content warning has been applied to this{' '}
|
||||
{'did' in details ? 'account' : 'post'}.
|
||||
</Trans>{' '}
|
||||
</Text>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Appeal this decision`)}
|
||||
accessibilityHint=""
|
||||
onPress={() => openModal({name: 'appeal-label', ...details})}>
|
||||
<Text type="sm" style={pal.link}>
|
||||
<Trans>Appeal this decision.</Trans>
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
)
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
import React from 'react'
|
||||
import {Pressable, StyleProp, StyleSheet, ViewStyle} from 'react-native'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {Text} from '../text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {ShieldExclamation} from 'lib/icons'
|
||||
import {describeModerationCause} from 'lib/moderation'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export function PostAlerts({
|
||||
moderation,
|
||||
style,
|
||||
}: {
|
||||
moderation: ModerationUI
|
||||
includeMute?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
const shouldAlert = !!moderation.cause && moderation.alert
|
||||
if (!shouldAlert) {
|
||||
return null
|
||||
}
|
||||
|
||||
const desc = describeModerationCause(moderation.cause, 'content')
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
openModal({
|
||||
name: 'moderation-details',
|
||||
context: 'content',
|
||||
moderation,
|
||||
})
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Learn more about this warning`)}
|
||||
accessibilityHint=""
|
||||
style={[styles.container, pal.viewLight, style]}>
|
||||
<ShieldExclamation style={pal.text} size={16} />
|
||||
<Text type="lg" style={[pal.text]}>
|
||||
{desc.name}{' '}
|
||||
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
|
||||
<Trans>Learn More</Trans>
|
||||
</Text>
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingVertical: 8,
|
||||
paddingLeft: 14,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 8,
|
||||
},
|
||||
learnMoreBtn: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
})
|
|
@ -1,142 +0,0 @@
|
|||
import React, {ComponentProps} from 'react'
|
||||
import {StyleSheet, Pressable, View, ViewStyle, StyleProp} from 'react-native'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {Link} from '../Link'
|
||||
import {Text} from '../text/Text'
|
||||
import {addStyle} from 'lib/styles'
|
||||
import {describeModerationCause} from 'lib/moderation'
|
||||
import {ShieldExclamation} from 'lib/icons'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
interface Props extends ComponentProps<typeof Link> {
|
||||
iconSize: number
|
||||
iconStyles: StyleProp<ViewStyle>
|
||||
moderation: ModerationUI
|
||||
}
|
||||
|
||||
export function PostHider({
|
||||
testID,
|
||||
href,
|
||||
moderation,
|
||||
style,
|
||||
children,
|
||||
iconSize,
|
||||
iconStyles,
|
||||
...props
|
||||
}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const [override, setOverride] = React.useState(false)
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
if (!moderation.blur) {
|
||||
return (
|
||||
<Link
|
||||
testID={testID}
|
||||
style={style}
|
||||
href={href}
|
||||
noFeedback
|
||||
accessible={false}
|
||||
{...props}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const isMute = ['muted', 'muted-word'].includes(moderation.cause?.type || '')
|
||||
const desc = describeModerationCause(moderation.cause, 'content')
|
||||
return !override ? (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (!moderation.noOverride) {
|
||||
setOverride(v => !v)
|
||||
}
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityHint={
|
||||
override ? _(msg`Hide the content`) : _(msg`Show the content`)
|
||||
}
|
||||
accessibilityLabel=""
|
||||
style={[
|
||||
styles.description,
|
||||
override ? {paddingBottom: 0} : undefined,
|
||||
pal.view,
|
||||
]}>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
openModal({
|
||||
name: 'moderation-details',
|
||||
context: 'content',
|
||||
moderation,
|
||||
})
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Learn more about this warning`)}
|
||||
accessibilityHint="">
|
||||
<View
|
||||
style={[
|
||||
pal.viewLight,
|
||||
{
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
borderRadius: iconSize,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
iconStyles,
|
||||
]}>
|
||||
{isMute ? (
|
||||
<FontAwesomeIcon
|
||||
icon={['far', 'eye-slash']}
|
||||
size={14}
|
||||
color={pal.colors.textLight}
|
||||
/>
|
||||
) : (
|
||||
<ShieldExclamation size={14} style={pal.textLight} />
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
<Text type="sm" style={[{flex: 1}, pal.textLight]} numberOfLines={1}>
|
||||
{desc.name}
|
||||
</Text>
|
||||
{!moderation.noOverride && (
|
||||
<Text type="sm" style={[styles.showBtn, pal.link]}>
|
||||
{override ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
) : (
|
||||
<Link
|
||||
testID={testID}
|
||||
style={addStyle(style, styles.child)}
|
||||
href={href}
|
||||
noFeedback>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
description: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingVertical: 10,
|
||||
paddingLeft: 6,
|
||||
paddingRight: 18,
|
||||
marginTop: 1,
|
||||
},
|
||||
showBtn: {
|
||||
marginLeft: 'auto',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
child: {
|
||||
borderWidth: 0,
|
||||
borderTopWidth: 0,
|
||||
borderRadius: 8,
|
||||
},
|
||||
})
|
|
@ -1,89 +0,0 @@
|
|||
import React from 'react'
|
||||
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {ProfileModeration} from '@atproto/api'
|
||||
import {Text} from '../text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {ShieldExclamation} from 'lib/icons'
|
||||
import {
|
||||
describeModerationCause,
|
||||
getProfileModerationCauses,
|
||||
} from 'lib/moderation'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export function ProfileHeaderAlerts({
|
||||
moderation,
|
||||
style,
|
||||
}: {
|
||||
moderation: ProfileModeration
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
const causes = getProfileModerationCauses(moderation)
|
||||
if (!causes.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.grid}>
|
||||
{causes.map(cause => {
|
||||
const isMute = cause.type === 'muted'
|
||||
const desc = describeModerationCause(cause, 'account')
|
||||
return (
|
||||
<Pressable
|
||||
testID="profileHeaderAlert"
|
||||
key={desc.name}
|
||||
onPress={() => {
|
||||
openModal({
|
||||
name: 'moderation-details',
|
||||
context: 'content',
|
||||
moderation: {cause},
|
||||
})
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Learn more about this warning`)}
|
||||
accessibilityHint=""
|
||||
style={[styles.container, pal.viewLight, style]}>
|
||||
{isMute ? (
|
||||
<FontAwesomeIcon
|
||||
icon={['far', 'eye-slash']}
|
||||
size={14}
|
||||
color={pal.colors.textLight}
|
||||
/>
|
||||
) : (
|
||||
<ShieldExclamation style={pal.text} size={18} />
|
||||
)}
|
||||
<Text type="sm" style={[{flex: 1}, pal.text]}>
|
||||
{desc.name}
|
||||
</Text>
|
||||
<Text type="sm" style={[pal.link, styles.learnMoreBtn]}>
|
||||
<Trans>Learn More</Trans>
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
grid: {
|
||||
gap: 4,
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 8,
|
||||
},
|
||||
learnMoreBtn: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
})
|
|
@ -1,180 +0,0 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
TouchableWithoutFeedback,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {Text} from '../text/Text'
|
||||
import {Button} from '../forms/Button'
|
||||
import {describeModerationCause} from 'lib/moderation'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {s} from '#/lib/styles'
|
||||
import {CenteredView} from '../Views'
|
||||
|
||||
export function ScreenHider({
|
||||
testID,
|
||||
screenDescription,
|
||||
moderation,
|
||||
style,
|
||||
containerStyle,
|
||||
children,
|
||||
}: React.PropsWithChildren<{
|
||||
testID?: string
|
||||
screenDescription: string
|
||||
moderation: ModerationUI
|
||||
style?: StyleProp<ViewStyle>
|
||||
containerStyle?: StyleProp<ViewStyle>
|
||||
}>) {
|
||||
const pal = usePalette('default')
|
||||
const palInverted = usePalette('inverted')
|
||||
const {_} = useLingui()
|
||||
const [override, setOverride] = React.useState(false)
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
if (!moderation.blur || override) {
|
||||
return (
|
||||
<View testID={testID} style={style}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const isNoPwi =
|
||||
moderation.cause?.type === 'label' &&
|
||||
moderation.cause?.labelDef.id === '!no-unauthenticated'
|
||||
const desc = describeModerationCause(moderation.cause, 'account')
|
||||
return (
|
||||
<CenteredView
|
||||
style={[styles.container, pal.view, containerStyle]}
|
||||
sideBorders>
|
||||
<View style={styles.iconContainer}>
|
||||
<View style={[styles.icon, palInverted.view]}>
|
||||
<FontAwesomeIcon
|
||||
icon={isNoPwi ? ['far', 'eye-slash'] : 'exclamation'}
|
||||
style={pal.textInverted as FontAwesomeIconStyle}
|
||||
size={24}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Text type="title-2xl" style={[styles.title, pal.text]}>
|
||||
{isNoPwi ? (
|
||||
<Trans>Sign-in Required</Trans>
|
||||
) : (
|
||||
<Trans>Content Warning</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Text type="2xl" style={[styles.description, pal.textLight]}>
|
||||
{isNoPwi ? (
|
||||
<Trans>
|
||||
This account has requested that users sign in to view their profile.
|
||||
</Trans>
|
||||
) : (
|
||||
<>
|
||||
<Trans>This {screenDescription} has been flagged:</Trans>
|
||||
<Text type="2xl-medium" style={[pal.text, s.ml5]}>
|
||||
{desc.name}.
|
||||
</Text>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={() => {
|
||||
openModal({
|
||||
name: 'moderation-details',
|
||||
context: 'account',
|
||||
moderation,
|
||||
})
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Learn more about this warning`)}
|
||||
accessibilityHint="">
|
||||
<Text type="2xl" style={pal.link}>
|
||||
<Trans>Learn More</Trans>
|
||||
</Text>
|
||||
</TouchableWithoutFeedback>
|
||||
</>
|
||||
)}{' '}
|
||||
</Text>
|
||||
{isMobile && <View style={styles.spacer} />}
|
||||
<View style={styles.btnContainer}>
|
||||
<Button
|
||||
type="inverted"
|
||||
onPress={() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Home')
|
||||
}
|
||||
}}
|
||||
style={styles.btn}>
|
||||
<Text type="button-lg" style={pal.textInverted}>
|
||||
<Trans>Go back</Trans>
|
||||
</Text>
|
||||
</Button>
|
||||
{!moderation.noOverride && (
|
||||
<Button
|
||||
type="default"
|
||||
onPress={() => setOverride(v => !v)}
|
||||
style={styles.btn}>
|
||||
<Text type="button-lg" style={pal.text}>
|
||||
<Trans>Show anyway</Trans>
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
spacer: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingTop: 100,
|
||||
paddingBottom: 150,
|
||||
},
|
||||
iconContainer: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
icon: {
|
||||
borderRadius: 25,
|
||||
width: 50,
|
||||
height: 50,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
description: {
|
||||
marginBottom: 10,
|
||||
paddingHorizontal: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
btnContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
marginVertical: 10,
|
||||
gap: 10,
|
||||
},
|
||||
btn: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 14,
|
||||
},
|
||||
})
|
|
@ -41,7 +41,6 @@ let PostCtrls = ({
|
|||
post,
|
||||
record,
|
||||
richText,
|
||||
showAppealLabelItem,
|
||||
style,
|
||||
onPressReply,
|
||||
logContext,
|
||||
|
@ -50,7 +49,6 @@ let PostCtrls = ({
|
|||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
showAppealLabelItem?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
onPressReply: () => void
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
||||
|
@ -232,7 +230,6 @@ let PostCtrls = ({
|
|||
postUri={post.uri}
|
||||
record={record}
|
||||
richText={richText}
|
||||
showAppealLabelItem={showAppealLabelItem}
|
||||
style={styles.btnPad}
|
||||
hitSlop={big ? HITSLOP_20 : HITSLOP_10}
|
||||
/>
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import React from 'react'
|
||||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {
|
||||
AppBskyFeedDefs,
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyFeedPost,
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
ModerationUI,
|
||||
AppBskyEmbedExternal,
|
||||
RichText as RichTextAPI,
|
||||
moderatePost,
|
||||
ModerationDecision,
|
||||
} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {PostMeta} from '../PostMeta'
|
||||
|
@ -16,20 +18,20 @@ import {Text} from '../text/Text'
|
|||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {ComposerOptsQuote} from 'state/shell/composer'
|
||||
import {PostEmbeds} from '.'
|
||||
import {PostAlerts} from '../moderation/PostAlerts'
|
||||
import {PostAlerts} from '../../../../components/moderation/PostAlerts'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {InfoCircleIcon} from 'lib/icons'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {useModerationOpts} from '#/state/queries/preferences'
|
||||
import {ContentHider} from '../../../../components/moderation/ContentHider'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
export function MaybeQuoteEmbed({
|
||||
embed,
|
||||
moderation,
|
||||
style,
|
||||
}: {
|
||||
embed: AppBskyEmbedRecord.View
|
||||
moderation: ModerationUI
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
|
@ -39,17 +41,9 @@ export function MaybeQuoteEmbed({
|
|||
AppBskyFeedPost.validateRecord(embed.record.value).success
|
||||
) {
|
||||
return (
|
||||
<QuoteEmbed
|
||||
quote={{
|
||||
author: embed.record.author,
|
||||
cid: embed.record.cid,
|
||||
uri: embed.record.uri,
|
||||
indexedAt: embed.record.indexedAt,
|
||||
text: embed.record.value.text,
|
||||
facets: embed.record.value.facets,
|
||||
embeds: embed.record.embeds,
|
||||
}}
|
||||
moderation={moderation}
|
||||
<QuoteEmbedModerated
|
||||
viewRecord={embed.record}
|
||||
postRecord={embed.record.value}
|
||||
style={style}
|
||||
/>
|
||||
)
|
||||
|
@ -75,19 +69,49 @@ export function MaybeQuoteEmbed({
|
|||
return null
|
||||
}
|
||||
|
||||
function QuoteEmbedModerated({
|
||||
viewRecord,
|
||||
postRecord,
|
||||
style,
|
||||
}: {
|
||||
viewRecord: AppBskyEmbedRecord.ViewRecord
|
||||
postRecord: AppBskyFeedPost.Record
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
const moderation = React.useMemo(() => {
|
||||
return moderationOpts
|
||||
? moderatePost(viewRecordToPostView(viewRecord), moderationOpts)
|
||||
: undefined
|
||||
}, [viewRecord, moderationOpts])
|
||||
|
||||
const quote = {
|
||||
author: viewRecord.author,
|
||||
cid: viewRecord.cid,
|
||||
uri: viewRecord.uri,
|
||||
indexedAt: viewRecord.indexedAt,
|
||||
text: postRecord.text,
|
||||
facets: postRecord.facets,
|
||||
embeds: viewRecord.embeds,
|
||||
}
|
||||
|
||||
return <QuoteEmbed quote={quote} moderation={moderation} style={style} />
|
||||
}
|
||||
|
||||
export function QuoteEmbed({
|
||||
quote,
|
||||
moderation,
|
||||
style,
|
||||
}: {
|
||||
quote: ComposerOptsQuote
|
||||
moderation?: ModerationUI
|
||||
moderation?: ModerationDecision
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const itemUrip = new AtUri(quote.uri)
|
||||
const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey)
|
||||
const itemTitle = `Post by ${quote.author.handle}`
|
||||
|
||||
const richText = React.useMemo(
|
||||
() =>
|
||||
quote.text.trim()
|
||||
|
@ -95,6 +119,7 @@ export function QuoteEmbed({
|
|||
: undefined,
|
||||
[quote.text, quote.facets],
|
||||
)
|
||||
|
||||
const embed = React.useMemo(() => {
|
||||
const e = quote.embeds?.[0]
|
||||
|
||||
|
@ -108,40 +133,52 @@ export function QuoteEmbed({
|
|||
return e.media
|
||||
}
|
||||
}, [quote.embeds])
|
||||
|
||||
return (
|
||||
<Link
|
||||
style={[styles.container, pal.borderDark, style]}
|
||||
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
|
||||
href={itemHref}
|
||||
title={itemTitle}>
|
||||
<View pointerEvents="none">
|
||||
<PostMeta
|
||||
author={quote.author}
|
||||
showAvatar
|
||||
authorHasWarning={false}
|
||||
postHref={itemHref}
|
||||
timestamp={quote.indexedAt}
|
||||
/>
|
||||
</View>
|
||||
{moderation ? (
|
||||
<PostAlerts moderation={moderation} style={styles.alert} />
|
||||
) : null}
|
||||
{richText ? (
|
||||
<RichText
|
||||
enableTags
|
||||
value={richText}
|
||||
style={[a.text_md]}
|
||||
numberOfLines={20}
|
||||
disableLinks
|
||||
authorHandle={quote.author.handle}
|
||||
/>
|
||||
) : null}
|
||||
{embed && <PostEmbeds embed={embed} moderation={{}} />}
|
||||
</Link>
|
||||
<ContentHider modui={moderation?.ui('contentList')}>
|
||||
<Link
|
||||
style={[styles.container, pal.borderDark, style]}
|
||||
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
|
||||
href={itemHref}
|
||||
title={itemTitle}>
|
||||
<View pointerEvents="none">
|
||||
<PostMeta
|
||||
author={quote.author}
|
||||
moderation={moderation}
|
||||
showAvatar
|
||||
authorHasWarning={false}
|
||||
postHref={itemHref}
|
||||
timestamp={quote.indexedAt}
|
||||
/>
|
||||
</View>
|
||||
{moderation ? (
|
||||
<PostAlerts modui={moderation.ui('contentView')} style={[a.py_xs]} />
|
||||
) : null}
|
||||
{richText ? (
|
||||
<RichText
|
||||
value={richText}
|
||||
style={[a.text_md]}
|
||||
numberOfLines={20}
|
||||
disableLinks
|
||||
/>
|
||||
) : null}
|
||||
{embed && <PostEmbeds embed={embed} moderation={moderation} />}
|
||||
</Link>
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuoteEmbed
|
||||
function viewRecordToPostView(
|
||||
viewRecord: AppBskyEmbedRecord.ViewRecord,
|
||||
): AppBskyFeedDefs.PostView {
|
||||
const {value, embeds, ...rest} = viewRecord
|
||||
return {
|
||||
...rest,
|
||||
$type: 'app.bsky.feed.defs#postView',
|
||||
record: value,
|
||||
embed: embeds?.[0],
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
|
|
@ -15,8 +15,7 @@ import {
|
|||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyGraphDefs,
|
||||
ModerationUI,
|
||||
PostModeration,
|
||||
ModerationDecision,
|
||||
} from '@atproto/api'
|
||||
import {Link} from '../Link'
|
||||
import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
|
||||
|
@ -26,9 +25,8 @@ import {ExternalLinkEmbed} from './ExternalLinkEmbed'
|
|||
import {MaybeQuoteEmbed} from './QuoteEmbed'
|
||||
import {AutoSizedImage} from '../images/AutoSizedImage'
|
||||
import {ListEmbed} from './ListEmbed'
|
||||
import {isCauseALabelOnUri, isQuoteBlurred} from 'lib/moderation'
|
||||
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
|
||||
import {ContentHider} from '../moderation/ContentHider'
|
||||
import {ContentHider} from '../../../../components/moderation/ContentHider'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
|
||||
|
@ -42,12 +40,10 @@ type Embed =
|
|||
export function PostEmbeds({
|
||||
embed,
|
||||
moderation,
|
||||
moderationDecisions,
|
||||
style,
|
||||
}: {
|
||||
embed?: Embed
|
||||
moderation: ModerationUI
|
||||
moderationDecisions?: PostModeration['decisions']
|
||||
moderation?: ModerationDecision
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
|
@ -66,18 +62,10 @@ export function PostEmbeds({
|
|||
// quote post with media
|
||||
// =
|
||||
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
|
||||
const isModOnQuote =
|
||||
(AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
|
||||
isCauseALabelOnUri(moderation.cause, embed.record.record.uri)) ||
|
||||
(moderationDecisions && isQuoteBlurred(moderationDecisions))
|
||||
const mediaModeration = isModOnQuote ? {} : moderation
|
||||
const quoteModeration = isModOnQuote ? moderation : {}
|
||||
return (
|
||||
<View style={style}>
|
||||
<PostEmbeds embed={embed.media} moderation={mediaModeration} />
|
||||
<ContentHider moderation={quoteModeration}>
|
||||
<MaybeQuoteEmbed embed={embed.record} moderation={quoteModeration} />
|
||||
</ContentHider>
|
||||
<PostEmbeds embed={embed.media} moderation={moderation} />
|
||||
<MaybeQuoteEmbed embed={embed.record} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
@ -86,6 +74,7 @@ export function PostEmbeds({
|
|||
// custom feed embed (i.e. generator view)
|
||||
// =
|
||||
if (AppBskyFeedDefs.isGeneratorView(embed.record)) {
|
||||
// TODO moderation
|
||||
return (
|
||||
<FeedSourceCard
|
||||
feedUri={embed.record.uri}
|
||||
|
@ -97,16 +86,13 @@ export function PostEmbeds({
|
|||
|
||||
// list embed
|
||||
if (AppBskyGraphDefs.isListView(embed.record)) {
|
||||
// TODO moderation
|
||||
return <ListEmbed item={embed.record} />
|
||||
}
|
||||
|
||||
// quote post
|
||||
// =
|
||||
return (
|
||||
<ContentHider moderation={moderation}>
|
||||
<MaybeQuoteEmbed embed={embed} style={style} moderation={moderation} />
|
||||
</ContentHider>
|
||||
)
|
||||
return <MaybeQuoteEmbed embed={embed} style={style} />
|
||||
}
|
||||
|
||||
// image embed
|
||||
|
@ -132,35 +118,41 @@ export function PostEmbeds({
|
|||
if (images.length === 1) {
|
||||
const {alt, thumb, aspectRatio} = images[0]
|
||||
return (
|
||||
<View style={[styles.imagesContainer, style]}>
|
||||
<AutoSizedImage
|
||||
alt={alt}
|
||||
uri={thumb}
|
||||
dimensionsHint={aspectRatio}
|
||||
onPress={() => _openLightbox(0)}
|
||||
onPressIn={() => onPressIn(0)}
|
||||
style={[styles.singleImage]}>
|
||||
{alt === '' ? null : (
|
||||
<View style={styles.altContainer}>
|
||||
<Text style={styles.alt} accessible={false}>
|
||||
ALT
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</AutoSizedImage>
|
||||
</View>
|
||||
<ContentHider modui={moderation?.ui('contentMedia')}>
|
||||
<View style={[styles.imagesContainer, style]}>
|
||||
<AutoSizedImage
|
||||
alt={alt}
|
||||
uri={thumb}
|
||||
dimensionsHint={aspectRatio}
|
||||
onPress={() => _openLightbox(0)}
|
||||
onPressIn={() => onPressIn(0)}
|
||||
style={[styles.singleImage]}>
|
||||
{alt === '' ? null : (
|
||||
<View style={styles.altContainer}>
|
||||
<Text style={styles.alt} accessible={false}>
|
||||
ALT
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</AutoSizedImage>
|
||||
</View>
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.imagesContainer, style]}>
|
||||
<ImageLayoutGrid
|
||||
images={embed.images}
|
||||
onPress={_openLightbox}
|
||||
onPressIn={onPressIn}
|
||||
style={embed.images.length === 1 ? [styles.singleImage] : undefined}
|
||||
/>
|
||||
</View>
|
||||
<ContentHider modui={moderation?.ui('contentMedia')}>
|
||||
<View style={[styles.imagesContainer, style]}>
|
||||
<ImageLayoutGrid
|
||||
images={embed.images}
|
||||
onPress={_openLightbox}
|
||||
onPressIn={onPressIn}
|
||||
style={
|
||||
embed.images.length === 1 ? [styles.singleImage] : undefined
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -171,15 +163,17 @@ export function PostEmbeds({
|
|||
const link = embed.external
|
||||
|
||||
return (
|
||||
<Link
|
||||
asAnchor
|
||||
anchorNoUnderline
|
||||
href={link.uri}
|
||||
style={[styles.extOuter, pal.view, pal.borderDark, style]}
|
||||
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
|
||||
onLongPress={onShareExternal}>
|
||||
<ExternalLinkEmbed link={link} />
|
||||
</Link>
|
||||
<ContentHider modui={moderation?.ui('contentMedia')}>
|
||||
<Link
|
||||
asAnchor
|
||||
anchorNoUnderline
|
||||
href={link.uri}
|
||||
style={[styles.extOuter, pal.view, pal.borderDark, style]}
|
||||
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
|
||||
onLongPress={onShareExternal}>
|
||||
<ExternalLinkEmbed link={link} />
|
||||
</Link>
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue