diff options
Diffstat (limited to 'src/view/com/util')
-rw-r--r-- | src/view/com/util/BottomSheetCustomBackdrop.tsx | 7 | ||||
-rw-r--r-- | src/view/com/util/ErrorBoundary.tsx | 23 | ||||
-rw-r--r-- | src/view/com/util/Link.tsx | 8 | ||||
-rw-r--r-- | src/view/com/util/PostMeta.tsx | 10 | ||||
-rw-r--r-- | src/view/com/util/UserAvatar.tsx | 33 | ||||
-rw-r--r-- | src/view/com/util/UserBanner.tsx | 12 | ||||
-rw-r--r-- | src/view/com/util/forms/PostDropdownBtn.tsx | 46 | ||||
-rw-r--r-- | src/view/com/util/moderation/ContentHider.tsx | 145 | ||||
-rw-r--r-- | src/view/com/util/moderation/LabelInfo.tsx | 61 | ||||
-rw-r--r-- | src/view/com/util/moderation/PostAlerts.tsx | 67 | ||||
-rw-r--r-- | src/view/com/util/moderation/PostHider.tsx | 142 | ||||
-rw-r--r-- | src/view/com/util/moderation/ProfileHeaderAlerts.tsx | 89 | ||||
-rw-r--r-- | src/view/com/util/moderation/ScreenHider.tsx | 180 | ||||
-rw-r--r-- | src/view/com/util/post-ctrls/PostCtrls.tsx | 3 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/QuoteEmbed.tsx | 129 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/index.tsx | 106 |
16 files changed, 222 insertions, 839 deletions
diff --git a/src/view/com/util/BottomSheetCustomBackdrop.tsx b/src/view/com/util/BottomSheetCustomBackdrop.tsx index ed5a2f165..ab6570252 100644 --- a/src/view/com/util/BottomSheetCustomBackdrop.tsx +++ b/src/view/com/util/BottomSheetCustomBackdrop.tsx @@ -6,12 +6,15 @@ import Animated, { interpolate, useAnimatedStyle, } from 'react-native-reanimated' -import {t} from '@lingui/macro' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' export function createCustomBackdrop( onClose?: (() => void) | undefined, ): React.FC<BottomSheetBackdropProps> { const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => { + const {_} = useLingui() + // animated variables const opacity = useAnimatedStyle(() => ({ opacity: interpolate( @@ -30,7 +33,7 @@ export function createCustomBackdrop( return ( <TouchableWithoutFeedback onPress={onClose} - accessibilityLabel={t`Close bottom drawer`} + accessibilityLabel={_(msg`Close bottom drawer`)} accessibilityHint="" onAccessibilityEscape={() => { if (onClose !== undefined) { diff --git a/src/view/com/util/ErrorBoundary.tsx b/src/view/com/util/ErrorBoundary.tsx index 5ec1d0014..22fdd606e 100644 --- a/src/view/com/util/ErrorBoundary.tsx +++ b/src/view/com/util/ErrorBoundary.tsx @@ -1,8 +1,9 @@ import React, {Component, ErrorInfo, ReactNode} from 'react' import {ErrorScreen} from './error/ErrorScreen' import {CenteredView} from './Views' -import {t} from '@lingui/macro' +import {msg} from '@lingui/macro' import {logger} from '#/logger' +import {useLingui} from '@lingui/react' interface Props { children?: ReactNode @@ -31,11 +32,7 @@ export class ErrorBoundary extends Component<Props, State> { if (this.state.hasError) { return ( <CenteredView style={{height: '100%', flex: 1}}> - <ErrorScreen - title={t`Oh no!`} - message={t`There was an unexpected issue in the application. Please let us know if this happened to you!`} - details={this.state.error.toString()} - /> + <TranslatedErrorScreen details={this.state.error.toString()} /> </CenteredView> ) } @@ -43,3 +40,17 @@ export class ErrorBoundary extends Component<Props, State> { return this.props.children } } + +function TranslatedErrorScreen({details}: {details?: string}) { + const {_} = useLingui() + + return ( + <ErrorScreen + title={_(msg`Oh no!`)} + message={_( + msg`There was an unexpected issue in the application. Please let us know if this happened to you!`, + )} + details={details} + /> + ) +} diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 7468111b5..b6c512b09 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -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} /> ) diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 3795dcf13..53dc20e71 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -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} diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 413237397..8656c3f51 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -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,33 @@ let DefaultAvatar = ({ </Svg> ) } + if (type === 'labeler') { + return ( + <Svg + testID="userAvatarFallback" + width={size} + height={size} + viewBox="0 0 32 32" + fill="none" + stroke="none"> + <Rect + x="0" + y="0" + width="32" + height="32" + rx="3" + 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 +161,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, diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index a5ddfee8a..4fb3726cd 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -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, + }, }) diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 8fc3d9ea6..70fbb907f 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -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`)} diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx deleted file mode 100644 index cd2545290..000000000 --- a/src/view/com/util/moderation/ContentHider.tsx +++ /dev/null @@ -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', - }, -}) diff --git a/src/view/com/util/moderation/LabelInfo.tsx b/src/view/com/util/moderation/LabelInfo.tsx deleted file mode 100644 index 970338752..000000000 --- a/src/view/com/util/moderation/LabelInfo.tsx +++ /dev/null @@ -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> - ) -} diff --git a/src/view/com/util/moderation/PostAlerts.tsx b/src/view/com/util/moderation/PostAlerts.tsx deleted file mode 100644 index bc5bf9b32..000000000 --- a/src/view/com/util/moderation/PostAlerts.tsx +++ /dev/null @@ -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', - }, -}) diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx deleted file mode 100644 index ede62e988..000000000 --- a/src/view/com/util/moderation/PostHider.tsx +++ /dev/null @@ -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, - }, -}) diff --git a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx deleted file mode 100644 index 0f07b679b..000000000 --- a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx +++ /dev/null @@ -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', - }, -}) diff --git a/src/view/com/util/moderation/ScreenHider.tsx b/src/view/com/util/moderation/ScreenHider.tsx deleted file mode 100644 index 86f0cbf7b..000000000 --- a/src/view/com/util/moderation/ScreenHider.tsx +++ /dev/null @@ -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, - }, -}) diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index c96954a11..3fa347a6d 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -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} /> diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx index 35b091269..2b1c3e617 100644 --- a/src/view/com/util/post-embeds/QuoteEmbed.tsx +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -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: { diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 7e235babb..47091fbb0 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -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> ) } |