diff options
Diffstat (limited to 'src/view/com')
-rw-r--r-- | src/view/com/composer/Composer.tsx | 2 | ||||
-rw-r--r-- | src/view/com/composer/text-input/TextInput.web.tsx | 10 | ||||
-rw-r--r-- | src/view/com/feeds/FeedPage.tsx | 2 | ||||
-rw-r--r-- | src/view/com/pager/Pager.tsx | 2 | ||||
-rw-r--r-- | src/view/com/pager/PagerWithHeader.tsx | 15 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 31 | ||||
-rw-r--r-- | src/view/com/post/Post.tsx | 8 | ||||
-rw-r--r-- | src/view/com/posts/Feed.tsx | 37 | ||||
-rw-r--r-- | src/view/com/posts/FeedErrorMessage.tsx | 11 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 8 | ||||
-rw-r--r-- | src/view/com/posts/FollowingEndOfFeed.tsx | 9 | ||||
-rw-r--r-- | src/view/com/profile/ProfileCard.tsx | 3 | ||||
-rw-r--r-- | src/view/com/profile/ProfileHeader.tsx | 32 | ||||
-rw-r--r-- | src/view/com/profile/ProfileHeaderSuggestedFollows.tsx | 8 | ||||
-rw-r--r-- | src/view/com/util/LoadingPlaceholder.tsx | 55 | ||||
-rw-r--r-- | src/view/com/util/moderation/ContentHider.tsx | 43 | ||||
-rw-r--r-- | src/view/com/util/moderation/PostHider.tsx | 126 | ||||
-rw-r--r-- | src/view/com/util/moderation/ProfileHeaderAlerts.tsx | 16 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/index.tsx | 15 |
19 files changed, 296 insertions, 137 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 6f058d39e..7336f3b95 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -201,7 +201,7 @@ export const ComposePost = observer(function ComposePost({ setError('') - if (richtext.text.trim().length === 0 && gallery.isEmpty) { + if (richtext.text.trim().length === 0 && gallery.isEmpty && !extLink) { setError('Did you want to say anything?') return } diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 4c31da338..206a3205b 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -116,6 +116,16 @@ export const TextInput = React.forwardRef(function TextInputImpl( autofocus: 'end', editable: true, injectCSS: true, + onCreate({editor: editorProp}) { + // HACK + // the 'enter' animation sometimes causes autofocus to fail + // (see Composer.web.tsx in shell) + // so we wait 200ms (the anim is 150ms) and then focus manually + // -prf + setTimeout(() => { + editorProp.chain().focus('end').run() + }, 200) + }, onUpdate({editor: editorProp}) { const json = editorProp.getJSON() diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index f06716fb0..f3f07a8bd 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -158,9 +158,9 @@ export function FeedPage({ <View testID={testID} style={s.h100pct}> <Feed testID={testID ? `${testID}-feed` : undefined} + enabled={isPageFocused} feed={feed} feedParams={feedParams} - enabled={isPageFocused} pollInterval={POLL_FREQ} scrollElRef={scrollElRef} onScroll={onMainScroll} diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx index d70087504..61c3609f2 100644 --- a/src/view/com/pager/Pager.tsx +++ b/src/view/com/pager/Pager.tsx @@ -81,12 +81,14 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>( if (scrollState.current === 'settling') { if (lastDirection.current === -1 && offset < lastOffset.current) { onPageSelecting?.(position) + setSelectedPage(position) lastDirection.current = 0 } else if ( lastDirection.current === 1 && offset > lastOffset.current ) { onPageSelecting?.(position + 1) + setSelectedPage(position + 1) lastDirection.current = 0 } } else { diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index 487c589e3..dcfc1eebb 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -69,13 +69,19 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( // capture the header bar sizing const onTabBarLayout = React.useCallback( (evt: LayoutChangeEvent) => { - setTabBarHeight(evt.nativeEvent.layout.height) + const height = evt.nativeEvent.layout.height + if (height > 0) { + setTabBarHeight(height) + } }, [setTabBarHeight], ) const onHeaderOnlyLayout = React.useCallback( (evt: LayoutChangeEvent) => { - setHeaderOnlyHeight(evt.nativeEvent.layout.height) + const height = evt.nativeEvent.layout.height + if (height > 0) { + setHeaderOnlyHeight(height) + } }, [setHeaderOnlyHeight], ) @@ -248,11 +254,14 @@ let PagerTabBar = ({ })) return ( <Animated.View + pointerEvents="box-none" style={[ isMobile ? styles.tabBarMobile : styles.tabBarDesktop, headerTransform, ]}> - <View onLayout={onHeaderOnlyLayout}>{renderHeader?.()}</View> + <View onLayout={onHeaderOnlyLayout} pointerEvents="box-none"> + {renderHeader?.()} + </View> <View onLayout={onTabBarLayout} style={{ diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index a4b7a4a9c..86ea4eb39 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -351,11 +351,14 @@ let PostThreadItemLoaded = ({ {post.embed && ( <ContentHider moderation={moderation.embed} + moderationDecisions={moderation.decisions} ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)} + ignoreQuoteDecisions style={s.mb10}> <PostEmbeds embed={post.embed} moderation={moderation.embed} + moderationDecisions={moderation.decisions} /> </ContentHider> )} @@ -414,7 +417,7 @@ let PostThreadItemLoaded = ({ </> ) } else { - const isThreadedChild = treeView && depth > 1 + const isThreadedChild = treeView && depth > 0 return ( <PostOuterWrapper post={post} @@ -426,7 +429,11 @@ let PostThreadItemLoaded = ({ testID={`postThreadItem-by-${post.author.handle}`} href={postHref} style={[pal.view]} - moderation={moderation.content}> + moderation={moderation.content} + iconSize={isThreadedChild ? 26 : 38} + iconStyles={ + isThreadedChild ? {marginRight: 4} : {marginLeft: 2, marginRight: 2} + }> <PostSandboxWarning /> <View @@ -491,10 +498,10 @@ let PostThreadItemLoaded = ({ timestamp={post.indexedAt} postHref={postHref} showAvatar={isThreadedChild} - avatarSize={26} + avatarSize={20} displayNameType="md-bold" displayNameStyle={isThreadedChild && s.ml2} - style={isThreadedChild && s.mb5} + style={isThreadedChild && s.mb2} /> <PostAlerts moderation={moderation.content} @@ -522,10 +529,14 @@ let PostThreadItemLoaded = ({ {post.embed && ( <ContentHider style={styles.contentHider} - moderation={moderation.embed}> + moderation={moderation.embed} + moderationDecisions={moderation.decisions} + ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)} + ignoreQuoteDecisions> <PostEmbeds embed={post.embed} moderation={moderation.embed} + moderationDecisions={moderation.decisions} /> </ContentHider> )} @@ -583,7 +594,7 @@ function PostOuterWrapper({ const {isMobile} = useWebMediaQueries() const pal = usePalette('default') const styles = useStyles() - if (treeView && depth > 1) { + if (treeView && depth > 0) { return ( <View style={[ @@ -592,9 +603,9 @@ function PostOuterWrapper({ styles.cursor, { flexDirection: 'row', - paddingLeft: 20, + paddingLeft: depth === 1 ? 10 : 20, borderTopWidth: depth === 1 ? 1 : 0, - paddingTop: depth === 1 ? 8 : 0, + paddingTop: depth === 1 ? 6 : 0, }, ]}> {Array.from(Array(depth - 1)).map((_, n: number) => ( @@ -603,8 +614,8 @@ function PostOuterWrapper({ style={{ borderLeftWidth: 2, borderLeftColor: pal.colors.border, - marginLeft: n === 0 ? 14 : isMobile ? 6 : 14, - paddingLeft: n === 0 ? 18 : isMobile ? 6 : 12, + marginLeft: isMobile ? 6 : 14, + paddingLeft: isMobile ? 6 : 12, }} /> ))} diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 2e8019e71..9b1bf7a49 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -196,8 +196,14 @@ function PostInner({ {post.embed ? ( <ContentHider moderation={moderation.embed} + moderationDecisions={moderation.decisions} + ignoreQuoteDecisions style={styles.contentHider}> - <PostEmbeds embed={post.embed} moderation={moderation.embed} /> + <PostEmbeds + embed={post.embed} + moderation={moderation.embed} + moderationDecisions={moderation.decisions} + /> </ContentHider> ) : null} </ContentHider> diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 393c1bc91..b9ca9abdc 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -26,6 +26,7 @@ import { pollLatest, } from '#/state/queries/post-feed' import {useModerationOpts} from '#/state/queries/preferences' +import {isWeb} from '#/platform/detection' const LOADING_ITEM = {_reactKey: '__loading__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} @@ -89,7 +90,7 @@ let Feed = ({ const isEmpty = !isFetching && !data?.pages[0]?.slices.length const checkForNew = React.useCallback(async () => { - if (!data?.pages[0] || isFetching || !onHasNew) { + if (!data?.pages[0] || isFetching || !onHasNew || !enabled) { return } try { @@ -99,7 +100,7 @@ let Feed = ({ } catch (e) { logger.error('Poll latest failed', {feed, error: String(e)}) } - }, [feed, data, isFetching, onHasNew]) + }, [feed, data, isFetching, onHasNew, enabled]) React.useEffect(() => { // we store the interval handler in a ref to avoid needless @@ -216,19 +217,25 @@ let Feed = ({ const shouldRenderEndOfFeed = !hasNextPage && !isEmpty && !isFetching && !isError && !!renderEndOfFeed - const FeedFooter = React.useCallback( - () => - isFetchingNextPage ? ( - <View style={styles.feedFooter}> - <ActivityIndicator /> - </View> - ) : shouldRenderEndOfFeed ? ( - renderEndOfFeed() - ) : ( - <View /> - ), - [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed], - ) + const FeedFooter = React.useCallback(() => { + /** + * A bit of padding at the bottom of the feed as you scroll and when you + * reach the end, so that content isn't cut off by the bottom of the + * screen. + */ + const offset = Math.max(headerOffset, 32) * (isWeb ? 1 : 2) + + return isFetchingNextPage ? ( + <View style={[styles.feedFooter]}> + <ActivityIndicator /> + <View style={{height: offset}} /> + </View> + ) : shouldRenderEndOfFeed ? ( + <View style={{minHeight: offset}}>{renderEndOfFeed()}</View> + ) : ( + <View style={{height: offset}} /> + ) + }, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset]) const scrollHandler = useAnimatedScrollHandler(onScroll || {}) return ( diff --git a/src/view/com/posts/FeedErrorMessage.tsx b/src/view/com/posts/FeedErrorMessage.tsx index 63d9d5956..f63bc1a88 100644 --- a/src/view/com/posts/FeedErrorMessage.tsx +++ b/src/view/com/posts/FeedErrorMessage.tsx @@ -25,6 +25,7 @@ export enum KnownError { FeedgenOffline = 'FeedgenOffline', FeedgenUnknown = 'FeedgenUnknown', FeedNSFPublic = 'FeedNSFPublic', + FeedTooManyRequests = 'FeedTooManyRequests', Unknown = 'Unknown', } @@ -100,6 +101,9 @@ function FeedgenErrorMessage({ [KnownError.FeedgenUnknown]: _l( msgLingui`Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue.`, ), + [KnownError.FeedTooManyRequests]: _l( + msgLingui`We're sorry, but this feed is currently receiving high traffic and is temporarily unavailable. Please try again later.`, + ), }[knownError]), [_l, knownError], ) @@ -203,6 +207,13 @@ function detectKnownError( ) { return KnownError.Block } + + // check status codes + if (error?.status === 429) { + return KnownError.FeedTooManyRequests + } + + // convert error to string and continue if (typeof error !== 'string') { error = error.toString() } diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index dfb0cfcf6..b6c509e92 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -320,9 +320,15 @@ let FeedItemInner = ({ <ContentHider testID="contentHider-embed" moderation={moderation.embed} + moderationDecisions={moderation.decisions} ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)} + ignoreQuoteDecisions style={styles.embed}> - <PostEmbeds embed={post.embed} moderation={moderation.embed} /> + <PostEmbeds + embed={post.embed} + moderation={moderation.embed} + moderationDecisions={moderation.decisions} + /> </ContentHider> ) : null} </ContentHider> diff --git a/src/view/com/posts/FollowingEndOfFeed.tsx b/src/view/com/posts/FollowingEndOfFeed.tsx index 48724d8b3..6630b9a83 100644 --- a/src/view/com/posts/FollowingEndOfFeed.tsx +++ b/src/view/com/posts/FollowingEndOfFeed.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {StyleSheet, View} from 'react-native' +import {StyleSheet, View, Dimensions} from 'react-native' import {useNavigation} from '@react-navigation/native' import { FontAwesomeIcon, @@ -36,7 +36,12 @@ export function FollowingEndOfFeed() { }, [navigation]) return ( - <View style={[styles.container, pal.border]}> + <View + style={[ + styles.container, + pal.border, + {minHeight: Dimensions.get('window').height * 0.75}, + ]}> <View style={styles.inner}> <Text type="xl-medium" style={[s.textCenter, pal.text]}> You've reached the end of your feed! Find some more accounts to diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index 279e00d75..21972f274 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -228,6 +228,7 @@ const styles = StyleSheet.create({ outer: { borderTopWidth: 1, paddingHorizontal: 6, + paddingVertical: 4, }, outerNoBorder: { borderTopWidth: 0, @@ -237,7 +238,7 @@ const styles = StyleSheet.create({ alignItems: 'center', }, layoutAvi: { - alignSelf: 'baseline', + alignSelf: 'flex-start', width: 54, paddingLeft: 4, paddingTop: 10, diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index c1de10aa5..16e98ddf2 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -412,10 +412,12 @@ let ProfileHeaderLoaded = ({ const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower') return ( - <View style={pal.view}> - <UserBanner banner={profile.banner} moderation={moderation.avatar} /> - <View style={styles.content}> - <View style={[styles.buttonsLine]}> + <View style={pal.view} pointerEvents="box-none"> + <View pointerEvents="none"> + <UserBanner banner={profile.banner} moderation={moderation.avatar} /> + </View> + <View style={styles.content} pointerEvents="box-none"> + <View style={[styles.buttonsLine]} pointerEvents="box-none"> {isMe ? ( <TouchableOpacity testID="profileHeaderEditProfileButton" @@ -468,7 +470,7 @@ let ProfileHeaderLoaded = ({ pal.text, { color: showSuggestedFollows - ? colors.white + ? pal.textInverted.color : pal.text.color, }, ]} @@ -525,7 +527,7 @@ let ProfileHeaderLoaded = ({ </NativeDropdown> ) : undefined} </View> - <View> + <View pointerEvents="none"> <Text testID="profileHeaderDisplayName" type="title-2xl" @@ -536,7 +538,7 @@ let ProfileHeaderLoaded = ({ )} </Text> </View> - <View style={styles.handleLine}> + <View style={styles.handleLine} pointerEvents="none"> {profile.viewer?.followedBy && !blockHide ? ( <View style={[styles.pill, pal.btn, s.mr5]}> <Text type="xs" style={[pal.text]}> @@ -557,7 +559,7 @@ let ProfileHeaderLoaded = ({ </View> {!blockHide && ( <> - <View style={styles.metricsLine}> + <View style={styles.metricsLine} pointerEvents="box-none"> <Link testID="profileHeaderFollowersButton" style={[s.flexRow, s.mr10]} @@ -604,12 +606,14 @@ let ProfileHeaderLoaded = ({ </Text> </View> {descriptionRT && !moderation.profile.blur ? ( - <RichText - testID="profileHeaderDescription" - style={[styles.description, pal.text]} - numberOfLines={15} - richText={descriptionRT} - /> + <View pointerEvents="none"> + <RichText + testID="profileHeaderDescription" + style={[styles.description, pal.text]} + numberOfLines={15} + richText={descriptionRT} + /> + </View> ) : undefined} </> )} diff --git a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx index f648c9801..1d550aa5c 100644 --- a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx +++ b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx @@ -70,15 +70,19 @@ export function ProfileHeaderSuggestedFollows({ }) return ( - <Animated.View style={[{overflow: 'hidden', opacity: 0}, animatedStyles]}> - <View style={{paddingVertical: OUTER_PADDING}}> + <Animated.View + pointerEvents="box-none" + style={[{overflow: 'hidden', opacity: 0}, animatedStyles]}> + <View style={{paddingVertical: OUTER_PADDING}} pointerEvents="box-none"> <View + pointerEvents="box-none" style={{ backgroundColor: pal.viewLight.backgroundColor, height: '100%', paddingTop: INNER_PADDING / 2, }}> <View + pointerEvents="box-none" style={{ flexDirection: 'row', justifyContent: 'space-between', diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx index 74e36ff7b..a07b33325 100644 --- a/src/view/com/util/LoadingPlaceholder.tsx +++ b/src/view/com/util/LoadingPlaceholder.tsx @@ -7,7 +7,7 @@ import { DimensionValue, } from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {HeartIcon} from 'lib/icons' +import {HeartIcon, HeartIconSolid} from 'lib/icons' import {s} from 'lib/styles' import {useTheme} from 'lib/ThemeContext' import {usePalette} from 'lib/hooks/usePalette' @@ -46,12 +46,22 @@ export function PostLoadingPlaceholder({ const pal = usePalette('default') return ( <View style={[styles.post, pal.view, style]}> - <LoadingPlaceholder width={52} height={52} style={styles.avatar} /> + <LoadingPlaceholder + width={52} + height={52} + style={[ + styles.avatar, + { + position: 'relative', + top: -6, + }, + ]} + /> <View style={[s.flex1]}> - <LoadingPlaceholder width={100} height={8} style={[s.mb10]} /> - <LoadingPlaceholder width={200} height={8} style={[s.mb5]} /> - <LoadingPlaceholder width={200} height={8} style={[s.mb5]} /> - <LoadingPlaceholder width={120} height={8} style={[s.mb10]} /> + <LoadingPlaceholder width={100} height={6} style={{marginBottom: 10}} /> + <LoadingPlaceholder width="95%" height={6} style={{marginBottom: 8}} /> + <LoadingPlaceholder width="95%" height={6} style={{marginBottom: 8}} /> + <LoadingPlaceholder width="80%" height={6} style={{marginBottom: 15}} /> <View style={s.flexRow}> <View style={s.flex1}> <FontAwesomeIcon @@ -90,6 +100,8 @@ export function PostFeedLoadingPlaceholder() { <PostLoadingPlaceholder /> <PostLoadingPlaceholder /> <PostLoadingPlaceholder /> + <PostLoadingPlaceholder /> + <PostLoadingPlaceholder /> </View> ) } @@ -102,11 +114,23 @@ export function NotificationLoadingPlaceholder({ const pal = usePalette('default') return ( <View style={[styles.notification, pal.view, style]}> - <View style={[s.flexRow, s.mb10]}> - <LoadingPlaceholder width={30} height={30} style={styles.smallAvatar} /> + <View style={{paddingLeft: 30, paddingRight: 10}}> + <HeartIconSolid + style={{color: pal.colors.backgroundLight} as ViewStyle} + size={30} + /> + </View> + <View style={{flex: 1}}> + <View style={[s.flexRow, s.mb10]}> + <LoadingPlaceholder + width={30} + height={30} + style={styles.smallAvatar} + /> + </View> + <LoadingPlaceholder width="90%" height={6} style={[s.mb5]} /> + <LoadingPlaceholder width="70%" height={6} style={[s.mb5]} /> </View> - <LoadingPlaceholder width={200} height={8} style={[s.mb5]} /> - <LoadingPlaceholder width={120} height={8} style={[s.mb5]} /> </View> ) } @@ -239,18 +263,19 @@ const styles = StyleSheet.create({ }, post: { flexDirection: 'row', - padding: 10, - margin: 1, + alignItems: 'flex-start', + paddingHorizontal: 10, + paddingTop: 20, + paddingBottom: 5, }, avatar: { borderRadius: 26, marginRight: 10, - marginLeft: 6, + marginLeft: 8, }, notification: { + flexDirection: 'row', padding: 10, - paddingLeft: 46, - margin: 1, }, profileCard: { flexDirection: 'row', diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx index a13aae2b5..b1ea76621 100644 --- a/src/view/com/util/moderation/ContentHider.tsx +++ b/src/view/com/util/moderation/ContentHider.tsx @@ -1,36 +1,44 @@ 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 {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {ModerationUI} from '@atproto/api' +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} 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 {isMobile} = useWebMediaQueries() const [override, setOverride] = React.useState(false) const {openModal} = useModalControls() - if (!moderation.blur || (ignoreMute && moderation.cause?.type === 'muted')) { + if ( + !moderation.blur || + (ignoreMute && moderation.cause?.type === 'muted') || + shouldIgnoreQuote(moderationDecisions, ignoreQuoteDecisions) + ) { return ( <View testID={testID} style={[styles.outer, style]}> {children} @@ -38,6 +46,7 @@ export function ContentHider({ ) } + const isMute = moderation.cause?.type === 'muted' const desc = describeModerationCause(moderation.cause, 'content') return ( <View testID={testID} style={[styles.outer, style]}> @@ -58,7 +67,6 @@ export function ContentHider({ accessibilityLabel="" style={[ styles.cover, - {paddingRight: isMobile ? 22 : 18}, moderation.noOverride ? {borderWidth: 1, borderColor: pal.colors.borderDark} : pal.viewLight, @@ -74,14 +82,22 @@ export function ContentHider({ accessibilityRole="button" accessibilityLabel={_(msg`Learn more about this warning`)} accessibilityHint=""> - <ShieldExclamation size={18} style={pal.text} /> + {isMute ? ( + <FontAwesomeIcon + icon={['far', 'eye-slash']} + size={18} + color={pal.colors.textLight} + /> + ) : ( + <ShieldExclamation size={18} style={pal.textLight} /> + )} </Pressable> - <Text type="lg" style={pal.text}> + <Text type="md" style={pal.text}> {desc.name} </Text> {!moderation.noOverride && ( <View style={styles.showBtn}> - <Text type="xl" style={pal.link}> + <Text type="lg" style={pal.link}> {override ? 'Hide' : 'Show'} </Text> </View> @@ -92,6 +108,16 @@ export function ContentHider({ ) } +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', @@ -104,6 +130,7 @@ const styles = StyleSheet.create({ marginTop: 4, paddingVertical: 14, paddingLeft: 14, + paddingRight: 18, }, showBtn: { marginLeft: 'auto', diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx index c2b857f54..bffb7ea1a 100644 --- a/src/view/com/util/moderation/PostHider.tsx +++ b/src/view/com/util/moderation/PostHider.tsx @@ -1,8 +1,8 @@ import React, {ComponentProps} from 'react' -import {StyleSheet, Pressable, View} from 'react-native' +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 {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {Link} from '../Link' import {Text} from '../text/Text' import {addStyle} from 'lib/styles' @@ -13,9 +13,8 @@ import {msg} from '@lingui/macro' import {useModalControls} from '#/state/modals' interface Props extends ComponentProps<typeof Link> { - // testID?: string - // href?: string - // style: StyleProp<ViewStyle> + iconSize: number + iconStyles: StyleProp<ViewStyle> moderation: ModerationUI } @@ -25,11 +24,12 @@ export function PostHider({ moderation, style, children, + iconSize, + iconStyles, ...props }: Props) { const pal = usePalette('default') const {_} = useLingui() - const {isMobile} = useWebMediaQueries() const [override, setOverride] = React.useState(false) const {openModal} = useModalControls() @@ -47,57 +47,74 @@ export function PostHider({ ) } + const isMute = moderation.cause?.type === 'muted' const desc = describeModerationCause(moderation.cause, 'content') - return ( - <> + return !override ? ( + <Pressable + onPress={() => { + if (!moderation.noOverride) { + setOverride(v => !v) + } + }} + accessibilityRole="button" + accessibilityHint={override ? 'Hide the content' : 'Show the content'} + accessibilityLabel="" + style={[ + styles.description, + override ? {paddingBottom: 0} : undefined, + pal.view, + ]}> <Pressable onPress={() => { - if (!moderation.noOverride) { - setOverride(v => !v) - } + openModal({ + name: 'moderation-details', + context: 'content', + moderation, + }) }} accessibilityRole="button" - accessibilityHint={override ? 'Hide the content' : 'Show the content'} - accessibilityLabel="" - style={[ - styles.description, - {paddingRight: isMobile ? 22 : 18}, - pal.viewLight, - ]}> - <Pressable - onPress={() => { - openModal({ - name: 'moderation-details', - context: 'content', - moderation, - }) - }} - accessibilityRole="button" - accessibilityLabel={_(msg`Learn more about this warning`)} - accessibilityHint=""> - <ShieldExclamation size={18} style={pal.text} /> - </Pressable> - <Text type="lg" style={[{flex: 1}, pal.text]} numberOfLines={1}> - {desc.name} - </Text> - {!moderation.noOverride && ( - <Text type="xl" style={[styles.showBtn, pal.link]}> - {override ? 'Hide' : 'Show'} - </Text> - )} - </Pressable> - {override && ( - <View style={[styles.childrenContainer, pal.border, pal.viewLight]}> - <Link - testID={testID} - style={addStyle(style, styles.child)} - href={href} - noFeedback> - {children} - </Link> + 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 ? 'Hide' : 'Show'} + </Text> )} - </> + </Pressable> + ) : ( + <Link + testID={testID} + style={addStyle(style, styles.child)} + href={href} + noFeedback> + {children} + </Link> ) } @@ -106,18 +123,15 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', gap: 4, - paddingVertical: 14, - paddingLeft: 18, + paddingVertical: 10, + paddingLeft: 6, + paddingRight: 18, marginTop: 1, }, showBtn: { marginLeft: 'auto', alignSelf: 'center', }, - childrenContainer: { - paddingHorizontal: 4, - paddingBottom: 6, - }, child: { borderWidth: 0, borderTopWidth: 0, diff --git a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx index d2675ca54..0f07b679b 100644 --- a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx +++ b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx @@ -10,6 +10,7 @@ import { } 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({ @@ -31,6 +32,7 @@ export function ProfileHeaderAlerts({ return ( <View style={styles.grid}> {causes.map(cause => { + const isMute = cause.type === 'muted' const desc = describeModerationCause(cause, 'account') return ( <Pressable @@ -47,11 +49,19 @@ export function ProfileHeaderAlerts({ accessibilityLabel={_(msg`Learn more about this warning`)} accessibilityHint="" style={[styles.container, pal.viewLight, style]}> - <ShieldExclamation style={pal.text} size={24} /> - <Text type="lg" style={[{flex: 1}, pal.text]}> + {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="lg" style={[pal.link, styles.learnMoreBtn]}> + <Text type="sm" style={[pal.link, styles.learnMoreBtn]}> <Trans>Learn More</Trans> </Text> </Pressable> diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index ca3bf1104..5c16a3b3e 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -16,6 +16,7 @@ import { AppBskyFeedDefs, AppBskyGraphDefs, ModerationUI, + PostModeration, } from '@atproto/api' import {Link} from '../Link' import {ImageLayoutGrid} from '../images/ImageLayoutGrid' @@ -28,8 +29,9 @@ import {getYoutubeVideoId} from 'lib/strings/url-helpers' import {MaybeQuoteEmbed} from './QuoteEmbed' import {AutoSizedImage} from '../images/AutoSizedImage' import {ListEmbed} from './ListEmbed' -import {isCauseALabelOnUri} from 'lib/moderation' +import {isCauseALabelOnUri, isQuoteBlurred} from 'lib/moderation' import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' +import {ContentHider} from '../moderation/ContentHider' type Embed = | AppBskyEmbedRecord.View @@ -41,10 +43,12 @@ type Embed = export function PostEmbeds({ embed, moderation, + moderationDecisions, style, }: { embed?: Embed moderation: ModerationUI + moderationDecisions?: PostModeration['decisions'] style?: StyleProp<ViewStyle> }) { const pal = usePalette('default') @@ -55,14 +59,17 @@ export function PostEmbeds({ // = if (AppBskyEmbedRecordWithMedia.isView(embed)) { const isModOnQuote = - AppBskyEmbedRecord.isViewRecord(embed.record.record) && - isCauseALabelOnUri(moderation.cause, embed.record.record.uri) + (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={[styles.stackContainer, style]}> <PostEmbeds embed={embed.media} moderation={mediaModeration} /> - <MaybeQuoteEmbed embed={embed.record} moderation={quoteModeration} /> + <ContentHider moderation={quoteModeration}> + <MaybeQuoteEmbed embed={embed.record} moderation={quoteModeration} /> + </ContentHider> </View> ) } |