diff options
Diffstat (limited to 'src/view/com/util')
-rw-r--r-- | src/view/com/util/LoadLatestBtn.tsx | 52 | ||||
-rw-r--r-- | src/view/com/util/LoadLatestBtn.web.tsx | 14 | ||||
-rw-r--r-- | src/view/com/util/PostMeta.tsx | 7 | ||||
-rw-r--r-- | src/view/com/util/PostMuted.tsx | 50 | ||||
-rw-r--r-- | src/view/com/util/UserAvatar.tsx | 47 | ||||
-rw-r--r-- | src/view/com/util/moderation/ContentHider.tsx | 109 | ||||
-rw-r--r-- | src/view/com/util/moderation/PostHider.tsx | 105 | ||||
-rw-r--r-- | src/view/com/util/moderation/ProfileHeaderLabels.tsx | 55 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/QuoteEmbed.tsx | 1 |
9 files changed, 354 insertions, 86 deletions
diff --git a/src/view/com/util/LoadLatestBtn.tsx b/src/view/com/util/LoadLatestBtn.tsx index fd05ecc9c..88b6dffd9 100644 --- a/src/view/com/util/LoadLatestBtn.tsx +++ b/src/view/com/util/LoadLatestBtn.tsx @@ -10,31 +10,33 @@ import {useStores} from 'state/index' const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20} -export const LoadLatestBtn = observer(({onPress}: {onPress: () => void}) => { - const store = useStores() - const safeAreaInsets = useSafeAreaInsets() - return ( - <TouchableOpacity - style={[ - styles.loadLatest, - !store.shell.minimalShellMode && { - bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30), - }, - ]} - onPress={onPress} - hitSlop={HITSLOP}> - <LinearGradient - colors={[gradients.blueLight.start, gradients.blueLight.end]} - start={{x: 0, y: 0}} - end={{x: 1, y: 1}} - style={styles.loadLatestInner}> - <Text type="md-bold" style={styles.loadLatestText}> - Load new posts - </Text> - </LinearGradient> - </TouchableOpacity> - ) -}) +export const LoadLatestBtn = observer( + ({onPress, label}: {onPress: () => void; label: string}) => { + const store = useStores() + const safeAreaInsets = useSafeAreaInsets() + return ( + <TouchableOpacity + style={[ + styles.loadLatest, + !store.shell.minimalShellMode && { + bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30), + }, + ]} + onPress={onPress} + hitSlop={HITSLOP}> + <LinearGradient + colors={[gradients.blueLight.start, gradients.blueLight.end]} + start={{x: 0, y: 0}} + end={{x: 1, y: 1}} + style={styles.loadLatestInner}> + <Text type="md-bold" style={styles.loadLatestText}> + Load new {label} + </Text> + </LinearGradient> + </TouchableOpacity> + ) + }, +) const styles = StyleSheet.create({ loadLatest: { diff --git a/src/view/com/util/LoadLatestBtn.web.tsx b/src/view/com/util/LoadLatestBtn.web.tsx index ba33f92a7..c85f44f30 100644 --- a/src/view/com/util/LoadLatestBtn.web.tsx +++ b/src/view/com/util/LoadLatestBtn.web.tsx @@ -6,7 +6,13 @@ import {UpIcon} from 'lib/icons' const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20} -export const LoadLatestBtn = ({onPress}: {onPress: () => void}) => { +export const LoadLatestBtn = ({ + onPress, + label, +}: { + onPress: () => void + label: string +}) => { const pal = usePalette('default') return ( <TouchableOpacity @@ -15,7 +21,7 @@ export const LoadLatestBtn = ({onPress}: {onPress: () => void}) => { hitSlop={HITSLOP}> <Text type="md-bold" style={pal.text}> <UpIcon size={16} strokeWidth={1} style={[pal.text, styles.icon]} /> - Load new posts + Load new {label} </Text> </TouchableOpacity> ) @@ -25,7 +31,9 @@ const styles = StyleSheet.create({ loadLatest: { flexDirection: 'row', position: 'absolute', - left: 'calc(50vw - 80px)', + left: '50vw', + // @ts-ignore web only -prf + transform: 'translateX(-50%)', top: 30, shadowColor: '#000', shadowOpacity: 0.2, diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index c46c16da0..d9dd11e05 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -15,6 +15,7 @@ interface PostMetaOpts { authorAvatar?: string authorHandle: string authorDisplayName: string | undefined + authorHasWarning: boolean postHref: string timestamp: string did?: string @@ -93,7 +94,11 @@ export const PostMeta = observer(function (opts: PostMetaOpts) { <View style={styles.meta}> {typeof opts.authorAvatar !== 'undefined' && ( <View style={[styles.metaItem, styles.avatar]}> - <UserAvatar avatar={opts.authorAvatar} size={16} /> + <UserAvatar + avatar={opts.authorAvatar} + size={16} + hasWarning={opts.authorHasWarning} + /> </View> )} <View style={[styles.metaItem, styles.maxWidth]}> diff --git a/src/view/com/util/PostMuted.tsx b/src/view/com/util/PostMuted.tsx deleted file mode 100644 index 539a71ecf..000000000 --- a/src/view/com/util/PostMuted.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {usePalette} from 'lib/hooks/usePalette' -import {Text} from './text/Text' - -export function PostMutedWrapper({ - isMuted, - children, -}: React.PropsWithChildren<{isMuted?: boolean}>) { - const pal = usePalette('default') - const [override, setOverride] = React.useState(false) - if (!isMuted || override) { - return <>{children}</> - } - return ( - <View style={[styles.container, pal.view, pal.border]}> - <FontAwesomeIcon - icon={['far', 'eye-slash']} - style={[styles.icon, pal.text]} - /> - <Text type="md" style={pal.textLight}> - Post from an account you muted. - </Text> - <TouchableOpacity - style={styles.showBtn} - onPress={() => setOverride(true)}> - <Text type="md" style={pal.link}> - Show post - </Text> - </TouchableOpacity> - </View> - ) -} - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 14, - paddingHorizontal: 18, - borderTopWidth: 1, - }, - icon: { - marginRight: 10, - }, - showBtn: { - marginLeft: 'auto', - }, -}) diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index ff741cd34..d18c2d697 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -44,10 +44,12 @@ function DefaultAvatar({size}: {size: number}) { export function UserAvatar({ size, avatar, + hasWarning, onSelectNewAvatar, }: { size: number avatar?: string | null + hasWarning?: boolean onSelectNewAvatar?: (img: PickedMedia | null) => void }) { const store = useStores() @@ -105,6 +107,22 @@ export function UserAvatar({ }, }, ] + + const warning = React.useMemo(() => { + if (!hasWarning) { + return <></> + } + return ( + <View style={[styles.warningIconContainer, pal.view]}> + <FontAwesomeIcon + icon="exclamation-circle" + style={styles.warningIcon} + size={Math.floor(size / 3)} + /> + </View> + ) + }, [hasWarning, size, pal]) + // onSelectNewAvatar is only passed as prop on the EditProfile component return onSelectNewAvatar ? ( <DropdownButton @@ -137,14 +155,20 @@ export function UserAvatar({ </View> </DropdownButton> ) : avatar ? ( - <HighPriorityImage - testID="userAvatarImage" - style={{width: size, height: size, borderRadius: Math.floor(size / 2)}} - resizeMode="stretch" - source={{uri: avatar}} - /> + <View style={{width: size, height: size}}> + <HighPriorityImage + testID="userAvatarImage" + style={{width: size, height: size, borderRadius: Math.floor(size / 2)}} + resizeMode="stretch" + source={{uri: avatar}} + /> + {warning} + </View> ) : ( - <DefaultAvatar size={size} /> + <View style={{width: size, height: size}}> + <DefaultAvatar size={size} /> + {warning} + </View> ) } @@ -165,4 +189,13 @@ const styles = StyleSheet.create({ height: 80, borderRadius: 40, }, + warningIconContainer: { + position: 'absolute', + right: 0, + bottom: 0, + borderRadius: 100, + }, + warningIcon: { + color: colors.red3, + }, }) diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx new file mode 100644 index 000000000..f65635d35 --- /dev/null +++ b/src/view/com/util/moderation/ContentHider.tsx @@ -0,0 +1,109 @@ +import React from 'react' +import { + StyleProp, + StyleSheet, + TouchableOpacity, + View, + ViewStyle, +} from 'react-native' +import {ComAtprotoLabelDefs} from '@atproto/api' +import {usePalette} from 'lib/hooks/usePalette' +import {useStores} from 'state/index' +import {Text} from '../text/Text' +import {addStyle} from 'lib/styles' + +export function ContentHider({ + testID, + isMuted, + labels, + style, + containerStyle, + children, +}: React.PropsWithChildren<{ + testID?: string + isMuted?: boolean + labels: ComAtprotoLabelDefs.Label[] | undefined + style?: StyleProp<ViewStyle> + containerStyle?: StyleProp<ViewStyle> +}>) { + const pal = usePalette('default') + const [override, setOverride] = React.useState(false) + const store = useStores() + const labelPref = store.preferences.getLabelPreference(labels) + + if (!isMuted && labelPref.pref === 'show') { + return ( + <View testID={testID} style={style}> + {children} + </View> + ) + } + + if (labelPref.pref === 'hide') { + return <></> + } + + return ( + <View style={[styles.container, pal.view, pal.border, containerStyle]}> + <View + style={[ + styles.description, + pal.viewLight, + override && styles.descriptionOpen, + ]}> + <Text type="md" style={pal.textLight}> + {isMuted ? ( + <>Post from an account you muted.</> + ) : ( + <>Warning: {labelPref.desc.title}</> + )} + </Text> + <TouchableOpacity + style={styles.showBtn} + onPress={() => setOverride(v => !v)}> + <Text type="md" style={pal.link}> + {override ? 'Hide' : 'Show'} + </Text> + </TouchableOpacity> + </View> + {override && ( + <View style={[styles.childrenContainer, pal.border]}> + <View testID={testID} style={addStyle(style, styles.child)}> + {children} + </View> + </View> + )} + </View> + ) +} + +const styles = StyleSheet.create({ + container: { + marginBottom: 10, + borderWidth: 1, + borderRadius: 12, + }, + description: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 14, + paddingLeft: 14, + paddingRight: 18, + borderRadius: 12, + }, + descriptionOpen: { + borderBottomLeftRadius: 0, + borderBottomRightRadius: 0, + }, + icon: { + marginRight: 10, + }, + showBtn: { + marginLeft: 'auto', + }, + childrenContainer: { + paddingHorizontal: 12, + paddingTop: 8, + }, + child: {}, +}) diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx new file mode 100644 index 000000000..bafc7aecf --- /dev/null +++ b/src/view/com/util/moderation/PostHider.tsx @@ -0,0 +1,105 @@ +import React from 'react' +import { + StyleProp, + StyleSheet, + TouchableOpacity, + View, + ViewStyle, +} from 'react-native' +import {ComAtprotoLabelDefs} 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 {useStores} from 'state/index' + +export function PostHider({ + testID, + href, + isMuted, + labels, + style, + children, +}: React.PropsWithChildren<{ + testID?: string + href: string + isMuted: boolean | undefined + labels: ComAtprotoLabelDefs.Label[] | undefined + style: StyleProp<ViewStyle> +}>) { + const store = useStores() + const pal = usePalette('default') + const [override, setOverride] = React.useState(false) + const bg = override ? pal.viewLight : pal.view + + const labelPref = store.preferences.getLabelPreference(labels) + if (labelPref.pref === 'hide') { + return <></> + } + + if (!isMuted) { + // NOTE: any further label enforcement should occur in ContentContainer + return ( + <Link testID={testID} style={style} href={href} noFeedback> + {children} + </Link> + ) + } + + return ( + <> + <View style={[styles.description, bg, pal.border]}> + <FontAwesomeIcon + icon={['far', 'eye-slash']} + style={[styles.icon, pal.text]} + /> + <Text type="md" style={pal.textLight}> + Post from an account you muted. + </Text> + <TouchableOpacity + style={styles.showBtn} + onPress={() => setOverride(v => !v)}> + <Text type="md" style={pal.link}> + {override ? 'Hide' : 'Show'} post + </Text> + </TouchableOpacity> + </View> + {override && ( + <View style={[styles.childrenContainer, pal.border, bg]}> + <Link + testID={testID} + style={addStyle(style, styles.child)} + href={href} + noFeedback> + {children} + </Link> + </View> + )} + </> + ) +} + +const styles = StyleSheet.create({ + description: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 14, + paddingHorizontal: 18, + borderTopWidth: 1, + }, + icon: { + marginRight: 10, + }, + showBtn: { + marginLeft: 'auto', + }, + childrenContainer: { + paddingHorizontal: 6, + paddingBottom: 6, + }, + child: { + borderWidth: 1, + borderRadius: 12, + }, +}) diff --git a/src/view/com/util/moderation/ProfileHeaderLabels.tsx b/src/view/com/util/moderation/ProfileHeaderLabels.tsx new file mode 100644 index 000000000..e099f09a7 --- /dev/null +++ b/src/view/com/util/moderation/ProfileHeaderLabels.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' +import {ComAtprotoLabelDefs} from '@atproto/api' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {Text} from '../text/Text' +import {usePalette} from 'lib/hooks/usePalette' +import {getLabelValueGroup} from 'lib/labeling/helpers' + +export function ProfileHeaderLabels({ + labels, +}: { + labels: ComAtprotoLabelDefs.Label[] | undefined +}) { + const palErr = usePalette('error') + if (!labels?.length) { + return null + } + return ( + <> + {labels.map((label, i) => { + const labelGroup = getLabelValueGroup(label?.val || '') + return ( + <View + key={`${label.val}-${i}`} + style={[styles.container, palErr.border, palErr.view]}> + <FontAwesomeIcon + icon="circle-exclamation" + style={palErr.text as FontAwesomeIconStyle} + size={20} + /> + <Text style={palErr.text}> + This account has been flagged for{' '} + {labelGroup.title.toLocaleLowerCase()}. + </Text> + </View> + ) + })} + </> + ) +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + gap: 10, + borderWidth: 1, + borderRadius: 6, + paddingHorizontal: 10, + paddingVertical: 8, + }, +}) diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx index 5a8be5a14..94e837238 100644 --- a/src/view/com/util/post-embeds/QuoteEmbed.tsx +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -42,6 +42,7 @@ export function QuoteEmbed({ authorAvatar={quote.author.avatar} authorHandle={quote.author.handle} authorDisplayName={quote.author.displayName} + authorHasWarning={false} postHref={itemHref} timestamp={quote.indexedAt} /> |