An ATproto social media client -- with an independent Appview.

Use Button instead of TextLink for show more button (#8480)

* use button instead of TextLink for show more

* Match post text size, provide interaction feedback

* Move to new Post components dir

* Prettier

---------

Co-authored-by: Eric Bailey <git@esb.lol>

authored by samuel.fm

Eric Bailey and committed by
GitHub
00469314 ed969151

+160 -125
+7 -3
src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx
··· 1 import React from 'react' 2 - import {ActivityIndicator, GestureResponderEvent, Pressable} from 'react-native' 3 import {Image} from 'expo-image' 4 - import {AppBskyEmbedExternal} from '@atproto/api' 5 import {msg} from '@lingui/macro' 6 import {useLingui} from '@lingui/react' 7 8 - import {EmbedPlayerParams} from '#/lib/strings/embed-player' 9 import {isIOS, isNative, isWeb} from '#/platform/detection' 10 import {useExternalEmbedsPrefs} from '#/state/preferences' 11 import {atoms as a, useTheme} from '#/alf'
··· 1 import React from 'react' 2 + import { 3 + ActivityIndicator, 4 + type GestureResponderEvent, 5 + Pressable, 6 + } from 'react-native' 7 import {Image} from 'expo-image' 8 + import {type AppBskyEmbedExternal} from '@atproto/api' 9 import {msg} from '@lingui/macro' 10 import {useLingui} from '@lingui/react' 11 12 + import {type EmbedPlayerParams} from '#/lib/strings/embed-player' 13 import {isIOS, isNative, isWeb} from '#/platform/detection' 14 import {useExternalEmbedsPrefs} from '#/state/preferences' 15 import {atoms as a, useTheme} from '#/alf'
+7 -4
src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx
··· 1 import React from 'react' 2 import { 3 ActivityIndicator, 4 - GestureResponderEvent, 5 Pressable, 6 StyleSheet, 7 useWindowDimensions, ··· 16 import {useSafeAreaInsets} from 'react-native-safe-area-context' 17 import {WebView} from 'react-native-webview' 18 import {Image} from 'expo-image' 19 - import {AppBskyEmbedExternal} from '@atproto/api' 20 import {msg} from '@lingui/macro' 21 import {useLingui} from '@lingui/react' 22 import {useNavigation} from '@react-navigation/native' 23 24 - import {NavigationProp} from '#/lib/routes/types' 25 - import {EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player' 26 import {isNative} from '#/platform/detection' 27 import {useExternalEmbedsPrefs} from '#/state/preferences' 28 import {EventStopper} from '#/view/com/util/EventStopper'
··· 1 import React from 'react' 2 import { 3 ActivityIndicator, 4 + type GestureResponderEvent, 5 Pressable, 6 StyleSheet, 7 useWindowDimensions, ··· 16 import {useSafeAreaInsets} from 'react-native-safe-area-context' 17 import {WebView} from 'react-native-webview' 18 import {Image} from 'expo-image' 19 + import {type AppBskyEmbedExternal} from '@atproto/api' 20 import {msg} from '@lingui/macro' 21 import {useLingui} from '@lingui/react' 22 import {useNavigation} from '@react-navigation/native' 23 24 + import {type NavigationProp} from '#/lib/routes/types' 25 + import { 26 + type EmbedPlayerParams, 27 + getPlayerAspect, 28 + } from '#/lib/strings/embed-player' 29 import {isNative} from '#/platform/detection' 30 import {useExternalEmbedsPrefs} from '#/state/preferences' 31 import {EventStopper} from '#/view/com/util/EventStopper'
+4 -4
src/components/Post/Embed/ExternalEmbed/Gif.tsx
··· 1 import React from 'react' 2 import { 3 Pressable, 4 - StyleProp, 5 StyleSheet, 6 TouchableOpacity, 7 View, 8 - ViewStyle, 9 } from 'react-native' 10 import {msg, Trans} from '@lingui/macro' 11 import {useLingui} from '@lingui/react' 12 13 import {HITSLOP_20} from '#/lib/constants' 14 - import {EmbedPlayerParams} from '#/lib/strings/embed-player' 15 import {isWeb} from '#/platform/detection' 16 import {useAutoplayDisabled} from '#/state/preferences' 17 import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' ··· 22 import {Text} from '#/components/Typography' 23 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 24 import {GifView} from '../../../../../modules/expo-bluesky-gif-view' 25 - import {GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types' 26 27 function PlaybackControls({ 28 onPress,
··· 1 import React from 'react' 2 import { 3 Pressable, 4 + type StyleProp, 5 StyleSheet, 6 TouchableOpacity, 7 View, 8 + type ViewStyle, 9 } from 'react-native' 10 import {msg, Trans} from '@lingui/macro' 11 import {useLingui} from '@lingui/react' 12 13 import {HITSLOP_20} from '#/lib/constants' 14 + import {type EmbedPlayerParams} from '#/lib/strings/embed-player' 15 import {isWeb} from '#/platform/detection' 16 import {useAutoplayDisabled} from '#/state/preferences' 17 import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' ··· 22 import {Text} from '#/components/Typography' 23 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 24 import {GifView} from '../../../../../modules/expo-bluesky-gif-view' 25 + import {type GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types' 26 27 function PlaybackControls({ 28 onPress,
+2 -2
src/components/Post/Embed/ListEmbed.tsx
··· 6 import {atoms as a, useTheme} from '#/alf' 7 import * as ListCard from '#/components/ListCard' 8 import {ContentHider} from '#/components/moderation/ContentHider' 9 - import {EmbedType} from '#/types/bsky/post' 10 - import {CommonProps} from './types' 11 12 export function ListEmbed({ 13 embed,
··· 6 import {atoms as a, useTheme} from '#/alf' 7 import * as ListCard from '#/components/ListCard' 8 import {ContentHider} from '#/components/moderation/ContentHider' 9 + import {type EmbedType} from '#/types/bsky/post' 10 + import {type CommonProps} from './types' 11 12 export function ListEmbed({ 13 embed,
+1 -1
src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.tsx
··· 1 - import {StyleProp, ViewStyle} from 'react-native' 2 import {View} from 'react-native' 3 import {msg, plural} from '@lingui/macro' 4 import {useLingui} from '@lingui/react'
··· 1 + import {type StyleProp, type ViewStyle} from 'react-native' 2 import {View} from 'react-native' 3 import {msg, plural} from '@lingui/macro' 4 import {useLingui} from '@lingui/react'
+2 -2
src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx
··· 1 import React, {useRef} from 'react' 2 - import {Pressable, StyleProp, View, ViewStyle} from 'react-native' 3 - import {AppBskyEmbedVideo} from '@atproto/api' 4 import {BlueskyVideoView} from '@haileyok/bluesky-video' 5 import {msg} from '@lingui/macro' 6 import {useLingui} from '@lingui/react'
··· 1 import React, {useRef} from 'react' 2 + import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native' 3 + import {type AppBskyEmbedVideo} from '@atproto/api' 4 import {BlueskyVideoView} from '@haileyok/bluesky-video' 5 import {msg} from '@lingui/macro' 6 import {useLingui} from '@lingui/react'
+1 -1
src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx
··· 1 - import React from 'react' 2 import {View} from 'react-native' 3 import {msg, Trans} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 6 import {atoms as a, useTheme} from '#/alf' 7 import {Button, ButtonText} from '#/components/Button'
··· 1 import {View} from 'react-native' 2 import {msg, Trans} from '@lingui/macro' 3 import {useLingui} from '@lingui/react' 4 + import type React from 'react' 5 6 import {atoms as a, useTheme} from '#/alf' 7 import {Button, ButtonText} from '#/components/Button'
+2 -2
src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.tsx
··· 1 - import React from 'react' 2 - import {SvgProps} from 'react-native-svg' 3 4 import {PressableWithHover} from '#/view/com/util/PressableWithHover' 5 import {atoms as a, useTheme, web} from '#/alf'
··· 1 + import {type SvgProps} from 'react-native-svg' 2 + import type React from 'react' 3 4 import {PressableWithHover} from '#/view/com/util/PressableWithHover' 5 import {atoms as a, useTheme, web} from '#/alf'
+2 -1
src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx
··· 1 - import React, {useCallback, useEffect, useRef, useState} from 'react' 2 import {View} from 'react-native' 3 import {msg} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 6 import {isFirefox, isTouchDevice} from '#/lib/browser' 7 import {clamp} from '#/lib/numbers'
··· 1 + import {useCallback, useEffect, useRef, useState} from 'react' 2 import {View} from 'react-native' 3 import {msg} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 + import type React from 'react' 6 7 import {isFirefox, isTouchDevice} from '#/lib/browser' 8 import {clamp} from '#/lib/numbers'
+2 -1
src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx
··· 1 - import React, {useCallback} from 'react' 2 import {View} from 'react-native' 3 import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 import {isSafari, isTouchDevice} from '#/lib/browser' 8 import {atoms as a} from '#/alf'
··· 1 + import {useCallback} from 'react' 2 import {View} from 'react-native' 3 import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 + import type React from 'react' 7 8 import {isSafari, isTouchDevice} from '#/lib/browser' 9 import {atoms as a} from '#/alf'
+1 -1
src/components/Post/Embed/VideoEmbed/index.tsx
··· 1 import React, {useCallback, useState} from 'react' 2 import {ActivityIndicator, View} from 'react-native' 3 import {ImageBackground} from 'expo-image' 4 - import {AppBskyEmbedVideo} from '@atproto/api' 5 import {msg, Trans} from '@lingui/macro' 6 import {useLingui} from '@lingui/react' 7
··· 1 import React, {useCallback, useState} from 'react' 2 import {ActivityIndicator, View} from 'react-native' 3 import {ImageBackground} from 'expo-image' 4 + import {type AppBskyEmbedVideo} from '@atproto/api' 5 import {msg, Trans} from '@lingui/macro' 6 import {useLingui} from '@lingui/react' 7
+3 -2
src/components/Post/Embed/VideoEmbed/index.web.tsx
··· 1 - import React, {useCallback, useEffect, useRef, useState} from 'react' 2 import {View} from 'react-native' 3 - import {AppBskyEmbedVideo} from '@atproto/api' 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 import {isFirefox} from '#/lib/browser' 8 import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
··· 1 + import {useCallback, useEffect, useRef, useState} from 'react' 2 import {View} from 'react-native' 3 + import {type AppBskyEmbedVideo} from '@atproto/api' 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 + import type React from 'react' 7 8 import {isFirefox} from '#/lib/browser' 9 import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
+56
src/components/Post/ShowMoreTextButton.tsx
···
··· 1 + import {useCallback, useMemo} from 'react' 2 + import {LayoutAnimation, type TextStyle} from 'react-native' 3 + import {msg, Trans} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + 6 + import {HITSLOP_10} from '#/lib/constants' 7 + import {atoms as a, flatten, type TextStyleProp, useTheme} from '#/alf' 8 + import {Button} from '#/components/Button' 9 + import {Text} from '#/components/Typography' 10 + 11 + export function ShowMoreTextButton({ 12 + onPress: onPressProp, 13 + style, 14 + }: TextStyleProp & {onPress: () => void}) { 15 + const t = useTheme() 16 + const {_} = useLingui() 17 + 18 + const onPress = useCallback(() => { 19 + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 20 + onPressProp() 21 + }, [onPressProp]) 22 + 23 + const textStyle = useMemo(() => { 24 + return flatten([a.leading_snug, a.text_sm, style]) as TextStyle & { 25 + fontSize: number 26 + lineHeight: number 27 + } 28 + }, [style]) 29 + 30 + return ( 31 + <Button 32 + label={_(msg`Expand post text`)} 33 + onPress={onPress} 34 + style={[ 35 + a.self_start, 36 + { 37 + paddingBottom: textStyle.fontSize / 3, 38 + }, 39 + ]} 40 + hitSlop={HITSLOP_10}> 41 + {({pressed, hovered}) => ( 42 + <Text 43 + style={[ 44 + textStyle, 45 + { 46 + color: t.palette.primary_500, 47 + opacity: pressed ? 0.6 : 1, 48 + textDecorationLine: hovered ? 'underline' : undefined, 49 + }, 50 + ]}> 51 + <Trans>Show More</Trans> 52 + </Text> 53 + )} 54 + </Button> 55 + ) 56 + }
+18 -22
src/screens/PostThread/components/ThreadItemPost.tsx
··· 6 AtUri, 7 RichText as RichTextAPI, 8 } from '@atproto/api' 9 - import {msg, Trans} from '@lingui/macro' 10 - import {useLingui} from '@lingui/react' 11 12 import {useActorStatus} from '#/lib/actor-status' 13 import {MAX_POST_LINES} from '#/lib/constants' 14 import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 15 - import {usePalette} from '#/lib/hooks/usePalette' 16 import {makeProfileLink} from '#/lib/routes/links' 17 import {countLines} from '#/lib/strings/helpers' 18 import { ··· 24 import {useSession} from '#/state/session' 25 import {type OnPostSuccessData} from '#/state/shell/composer' 26 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 27 - import {TextLink} from '#/view/com/util/Link' 28 import {PostMeta} from '#/view/com/util/PostMeta' 29 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 30 import { ··· 40 import {PostHider} from '#/components/moderation/PostHider' 41 import {type AppModerationCause} from '#/components/Pills' 42 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 43 import {PostControls} from '#/components/PostControls' 44 import {RichText} from '#/components/RichText' 45 import * as Skele from '#/components/Skeleton' ··· 187 postShadow: Shadow<AppBskyFeedDefs.PostView> 188 }) { 189 const t = useTheme() 190 - const pal = usePalette('default') 191 - const {_} = useLingui() 192 const {openComposer} = useOpenComposer() 193 const {currentAccount} = useSession() 194 ··· 304 additionalCauses={additionalPostAlerts} 305 /> 306 {richText?.text ? ( 307 - <RichText 308 - enableTags 309 - value={richText} 310 - style={[a.flex_1, a.text_md]} 311 - numberOfLines={limitLines ? MAX_POST_LINES : undefined} 312 - authorHandle={post.author.handle} 313 - shouldProxyLinks={true} 314 - /> 315 - ) : undefined} 316 - {limitLines ? ( 317 - <TextLink 318 - text={_(msg`Show More`)} 319 - style={pal.link} 320 - onPress={onPressShowMore} 321 - href="#" 322 - /> 323 ) : undefined} 324 {post.embed && ( 325 <View style={[a.pb_xs]}>
··· 6 AtUri, 7 RichText as RichTextAPI, 8 } from '@atproto/api' 9 + import {Trans} from '@lingui/macro' 10 11 import {useActorStatus} from '#/lib/actor-status' 12 import {MAX_POST_LINES} from '#/lib/constants' 13 import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 14 import {makeProfileLink} from '#/lib/routes/links' 15 import {countLines} from '#/lib/strings/helpers' 16 import { ··· 22 import {useSession} from '#/state/session' 23 import {type OnPostSuccessData} from '#/state/shell/composer' 24 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 25 import {PostMeta} from '#/view/com/util/PostMeta' 26 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 27 import { ··· 37 import {PostHider} from '#/components/moderation/PostHider' 38 import {type AppModerationCause} from '#/components/Pills' 39 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 40 + import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' 41 import {PostControls} from '#/components/PostControls' 42 import {RichText} from '#/components/RichText' 43 import * as Skele from '#/components/Skeleton' ··· 185 postShadow: Shadow<AppBskyFeedDefs.PostView> 186 }) { 187 const t = useTheme() 188 const {openComposer} = useOpenComposer() 189 const {currentAccount} = useSession() 190 ··· 300 additionalCauses={additionalPostAlerts} 301 /> 302 {richText?.text ? ( 303 + <> 304 + <RichText 305 + enableTags 306 + value={richText} 307 + style={[a.flex_1, a.text_md]} 308 + numberOfLines={limitLines ? MAX_POST_LINES : undefined} 309 + authorHandle={post.author.handle} 310 + shouldProxyLinks={true} 311 + /> 312 + {limitLines && ( 313 + <ShowMoreTextButton 314 + style={[a.text_md]} 315 + onPress={onPressShowMore} 316 + /> 317 + )} 318 + </> 319 ) : undefined} 320 {post.embed && ( 321 <View style={[a.pb_xs]}>
+16 -22
src/screens/PostThread/components/ThreadItemTreePost.tsx
··· 1 - import React, {memo, useMemo} from 'react' 2 import {View} from 'react-native' 3 import { 4 type AppBskyFeedDefs, ··· 6 AtUri, 7 RichText as RichTextAPI, 8 } from '@atproto/api' 9 - import {msg, Trans} from '@lingui/macro' 10 - import {useLingui} from '@lingui/react' 11 12 import {MAX_POST_LINES} from '#/lib/constants' 13 import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 14 - import {usePalette} from '#/lib/hooks/usePalette' 15 import {makeProfileLink} from '#/lib/routes/links' 16 import {countLines} from '#/lib/strings/helpers' 17 import { ··· 23 import {useSession} from '#/state/session' 24 import {type OnPostSuccessData} from '#/state/shell/composer' 25 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 26 - import {TextLink} from '#/view/com/util/Link' 27 import {PostMeta} from '#/view/com/util/PostMeta' 28 import { 29 OUTER_SPACE, ··· 39 import {PostHider} from '#/components/moderation/PostHider' 40 import {type AppModerationCause} from '#/components/Pills' 41 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 42 import {PostControls} from '#/components/PostControls' 43 import {RichText} from '#/components/RichText' 44 import * as Skele from '#/components/Skeleton' ··· 255 onPostSuccess?: (data: OnPostSuccessData) => void 256 threadgateRecord?: AppBskyFeedThreadgate.Record 257 }): React.ReactNode { 258 - const pal = usePalette('default') 259 - const {_} = useLingui() 260 const {openComposer} = useOpenComposer() 261 const {currentAccount} = useSession() 262 ··· 271 }), 272 [record], 273 ) 274 - const [limitLines, setLimitLines] = React.useState( 275 () => countLines(richText?.text) >= MAX_POST_LINES, 276 ) 277 const threadRootUri = record.reply?.root?.uri || post.uri 278 - const postHref = React.useMemo(() => { 279 const urip = new AtUri(post.uri) 280 return makeProfileLink(post.author, 'post', urip.rkey) 281 }, [post.uri, post.author]) 282 const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ 283 threadgateRecord, 284 }) 285 - const additionalPostAlerts: AppModerationCause[] = React.useMemo(() => { 286 const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri) 287 const isControlledByViewer = 288 new AtUri(threadRootUri).host === currentAccount?.did ··· 297 : [] 298 }, [post, currentAccount?.did, threadgateHiddenReplies, threadRootUri]) 299 300 - const onPressReply = React.useCallback(() => { 301 openComposer({ 302 replyTo: { 303 uri: post.uri, ··· 311 }) 312 }, [openComposer, post, record, onPostSuccess, moderation]) 313 314 - const onPressShowMore = React.useCallback(() => { 315 setLimitLines(false) 316 }, [setLimitLines]) 317 ··· 348 additionalCauses={additionalPostAlerts} 349 /> 350 {richText?.text ? ( 351 - <View> 352 <RichText 353 enableTags 354 value={richText} ··· 357 authorHandle={post.author.handle} 358 shouldProxyLinks={true} 359 /> 360 - </View> 361 - ) : undefined} 362 - {limitLines ? ( 363 - <TextLink 364 - text={_(msg`Show More`)} 365 - style={pal.link} 366 - onPress={onPressShowMore} 367 - href="#" 368 - /> 369 ) : undefined} 370 {post.embed && ( 371 <View style={[a.pb_xs]}>
··· 1 + import {memo, useCallback, useMemo, useState} from 'react' 2 import {View} from 'react-native' 3 import { 4 type AppBskyFeedDefs, ··· 6 AtUri, 7 RichText as RichTextAPI, 8 } from '@atproto/api' 9 + import {Trans} from '@lingui/macro' 10 11 import {MAX_POST_LINES} from '#/lib/constants' 12 import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 13 import {makeProfileLink} from '#/lib/routes/links' 14 import {countLines} from '#/lib/strings/helpers' 15 import { ··· 21 import {useSession} from '#/state/session' 22 import {type OnPostSuccessData} from '#/state/shell/composer' 23 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 24 import {PostMeta} from '#/view/com/util/PostMeta' 25 import { 26 OUTER_SPACE, ··· 36 import {PostHider} from '#/components/moderation/PostHider' 37 import {type AppModerationCause} from '#/components/Pills' 38 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 39 + import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' 40 import {PostControls} from '#/components/PostControls' 41 import {RichText} from '#/components/RichText' 42 import * as Skele from '#/components/Skeleton' ··· 253 onPostSuccess?: (data: OnPostSuccessData) => void 254 threadgateRecord?: AppBskyFeedThreadgate.Record 255 }): React.ReactNode { 256 const {openComposer} = useOpenComposer() 257 const {currentAccount} = useSession() 258 ··· 267 }), 268 [record], 269 ) 270 + const [limitLines, setLimitLines] = useState( 271 () => countLines(richText?.text) >= MAX_POST_LINES, 272 ) 273 const threadRootUri = record.reply?.root?.uri || post.uri 274 + const postHref = useMemo(() => { 275 const urip = new AtUri(post.uri) 276 return makeProfileLink(post.author, 'post', urip.rkey) 277 }, [post.uri, post.author]) 278 const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ 279 threadgateRecord, 280 }) 281 + const additionalPostAlerts: AppModerationCause[] = useMemo(() => { 282 const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri) 283 const isControlledByViewer = 284 new AtUri(threadRootUri).host === currentAccount?.did ··· 293 : [] 294 }, [post, currentAccount?.did, threadgateHiddenReplies, threadRootUri]) 295 296 + const onPressReply = useCallback(() => { 297 openComposer({ 298 replyTo: { 299 uri: post.uri, ··· 307 }) 308 }, [openComposer, post, record, onPostSuccess, moderation]) 309 310 + const onPressShowMore = useCallback(() => { 311 setLimitLines(false) 312 }, [setLimitLines]) 313 ··· 344 additionalCauses={additionalPostAlerts} 345 /> 346 {richText?.text ? ( 347 + <> 348 <RichText 349 enableTags 350 value={richText} ··· 353 authorHandle={post.author.handle} 354 shouldProxyLinks={true} 355 /> 356 + {limitLines && ( 357 + <ShowMoreTextButton 358 + style={[a.text_md]} 359 + onPress={onPressShowMore} 360 + /> 361 + )} 362 + </> 363 ) : undefined} 364 {post.embed && ( 365 <View style={[a.pb_xs]}>
+3 -3
src/screens/VideoFeed/components/Scrubber.tsx
··· 3 import { 4 Gesture, 5 GestureDetector, 6 - NativeGesture, 7 } from 'react-native-gesture-handler' 8 import Animated, { 9 interpolate, 10 runOnJS, 11 runOnUI, 12 - SharedValue, 13 useAnimatedReaction, 14 useAnimatedStyle, 15 useSharedValue, ··· 20 useSafeAreaInsets, 21 } from 'react-native-safe-area-context' 22 import {useEventListener} from 'expo' 23 - import {VideoPlayer} from 'expo-video' 24 25 import {tokens} from '#/alf' 26 import {atoms as a} from '#/alf'
··· 3 import { 4 Gesture, 5 GestureDetector, 6 + type NativeGesture, 7 } from 'react-native-gesture-handler' 8 import Animated, { 9 interpolate, 10 runOnJS, 11 runOnUI, 12 + type SharedValue, 13 useAnimatedReaction, 14 useAnimatedStyle, 15 useSharedValue, ··· 20 useSafeAreaInsets, 21 } from 'react-native-safe-area-context' 22 import {useEventListener} from 'expo' 23 + import {type VideoPlayer} from 'expo-video' 24 25 import {tokens} from '#/alf' 26 import {atoms as a} from '#/alf'
+3 -1
src/view/com/notifications/NotificationFeedItem.tsx
··· 30 import {useNavigation} from '@react-navigation/native' 31 import {useQueryClient} from '@tanstack/react-query' 32 33 import {useAnimatedValue} from '#/lib/hooks/useAnimatedValue' 34 import {usePalette} from '#/lib/hooks/usePalette' 35 import {makeProfileLink} from '#/lib/routes/links' ··· 918 {text?.length > 0 && ( 919 <Text 920 emoji 921 - style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}> 922 {text} 923 </Text> 924 )}
··· 30 import {useNavigation} from '@react-navigation/native' 31 import {useQueryClient} from '@tanstack/react-query' 32 33 + import {MAX_POST_LINES} from '#/lib/constants' 34 import {useAnimatedValue} from '#/lib/hooks/useAnimatedValue' 35 import {usePalette} from '#/lib/hooks/usePalette' 36 import {makeProfileLink} from '#/lib/routes/links' ··· 919 {text?.length > 0 && ( 920 <Text 921 emoji 922 + style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]} 923 + numberOfLines={MAX_POST_LINES}> 924 {text} 925 </Text> 926 )}
+8 -9
src/view/com/post-thread/PostThreadItem.tsx
··· 44 import {type PostSource} from '#/state/unstable-post-source' 45 import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' 46 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 47 - import {Link, TextLink} from '#/view/com/util/Link' 48 import {formatCount} from '#/view/com/util/numeric/format' 49 import {PostMeta} from '#/view/com/util/PostMeta' 50 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' ··· 62 import {PostHider} from '#/components/moderation/PostHider' 63 import {type AppModerationCause} from '#/components/Pills' 64 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 65 import {PostControls} from '#/components/PostControls' 66 import * as Prompt from '#/components/Prompt' 67 import {RichText} from '#/components/RichText' ··· 685 authorHandle={post.author.handle} 686 shouldProxyLinks={true} 687 /> 688 </View> 689 - ) : undefined} 690 - {limitLines ? ( 691 - <TextLink 692 - text={_(msg`Show More`)} 693 - style={pal.link} 694 - onPress={onPressShowMore} 695 - href="#" 696 - /> 697 ) : undefined} 698 {post.embed && ( 699 <View style={[a.pb_xs]}>
··· 44 import {type PostSource} from '#/state/unstable-post-source' 45 import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' 46 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 47 + import {Link} from '#/view/com/util/Link' 48 import {formatCount} from '#/view/com/util/numeric/format' 49 import {PostMeta} from '#/view/com/util/PostMeta' 50 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' ··· 62 import {PostHider} from '#/components/moderation/PostHider' 63 import {type AppModerationCause} from '#/components/Pills' 64 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 65 + import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' 66 import {PostControls} from '#/components/PostControls' 67 import * as Prompt from '#/components/Prompt' 68 import {RichText} from '#/components/RichText' ··· 686 authorHandle={post.author.handle} 687 shouldProxyLinks={true} 688 /> 689 + {limitLines && ( 690 + <ShowMoreTextButton 691 + style={[a.text_md]} 692 + onPress={onPressShowMore} 693 + /> 694 + )} 695 </View> 696 ) : undefined} 697 {post.embed && ( 698 <View style={[a.pb_xs]}>
+15 -24
src/view/com/post/Post.tsx
··· 1 - import React, {useMemo, useState} from 'react' 2 import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native' 3 import { 4 type AppBskyFeedDefs, ··· 9 RichText as RichTextAPI, 10 } from '@atproto/api' 11 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 12 - import {msg, Trans} from '@lingui/macro' 13 - import {useLingui} from '@lingui/react' 14 import {useQueryClient} from '@tanstack/react-query' 15 16 import {MAX_POST_LINES} from '#/lib/constants' ··· 27 import {useModerationOpts} from '#/state/preferences/moderation-opts' 28 import {precacheProfile} from '#/state/queries/profile' 29 import {useSession} from '#/state/session' 30 - import {Link, TextLink} from '#/view/com/util/Link' 31 import {PostMeta} from '#/view/com/util/PostMeta' 32 import {Text} from '#/view/com/util/text/Text' 33 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' ··· 37 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' 38 import {PostAlerts} from '#/components/moderation/PostAlerts' 39 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 40 import {PostControls} from '#/components/PostControls' 41 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 42 import {RichText} from '#/components/RichText' ··· 115 }) { 116 const queryClient = useQueryClient() 117 const pal = usePalette('default') 118 - const {_} = useLingui() 119 const {openComposer} = useOpenComposer() 120 const [limitLines, setLimitLines] = useState( 121 () => countLines(richText?.text) >= MAX_POST_LINES, ··· 128 replyAuthorDid = urip.hostname 129 } 130 131 - const onPressReply = React.useCallback(() => { 132 openComposer({ 133 replyTo: { 134 uri: post.uri, ··· 141 }) 142 }, [openComposer, post, record, moderation]) 143 144 - const onPressShowMore = React.useCallback(() => { 145 setLimitLines(false) 146 }, [setLimitLines]) 147 148 - const onBeforePress = React.useCallback(() => { 149 precacheProfile(queryClient, post.author) 150 }, [queryClient, post.author]) 151 152 const {currentAccount} = useSession() 153 const isMe = replyAuthorDid === currentAccount?.did 154 155 - const [hover, setHover] = React.useState(false) 156 return ( 157 <Link 158 href={itemHref} ··· 227 style={[a.py_xs]} 228 /> 229 {richText.text ? ( 230 - <View style={styles.postTextContainer}> 231 <RichText 232 enableTags 233 testID="postText" ··· 237 authorHandle={post.author.handle} 238 shouldProxyLinks={true} 239 /> 240 </View> 241 - ) : undefined} 242 - {limitLines ? ( 243 - <TextLink 244 - text={_(msg`Show More`)} 245 - style={pal.link} 246 - onPress={onPressShowMore} 247 - href="#" 248 - /> 249 ) : undefined} 250 {post.embed ? ( 251 <Embed ··· 289 }, 290 alert: { 291 marginBottom: 6, 292 - }, 293 - postTextContainer: { 294 - flexDirection: 'row', 295 - alignItems: 'center', 296 - flexWrap: 'wrap', 297 - overflow: 'hidden', 298 }, 299 replyLine: { 300 position: 'absolute',
··· 1 + import {useCallback, useMemo, useState} from 'react' 2 import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native' 3 import { 4 type AppBskyFeedDefs, ··· 9 RichText as RichTextAPI, 10 } from '@atproto/api' 11 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 12 + import {Trans} from '@lingui/macro' 13 import {useQueryClient} from '@tanstack/react-query' 14 15 import {MAX_POST_LINES} from '#/lib/constants' ··· 26 import {useModerationOpts} from '#/state/preferences/moderation-opts' 27 import {precacheProfile} from '#/state/queries/profile' 28 import {useSession} from '#/state/session' 29 + import {Link} from '#/view/com/util/Link' 30 import {PostMeta} from '#/view/com/util/PostMeta' 31 import {Text} from '#/view/com/util/text/Text' 32 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' ··· 36 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' 37 import {PostAlerts} from '#/components/moderation/PostAlerts' 38 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 39 + import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' 40 import {PostControls} from '#/components/PostControls' 41 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 42 import {RichText} from '#/components/RichText' ··· 115 }) { 116 const queryClient = useQueryClient() 117 const pal = usePalette('default') 118 const {openComposer} = useOpenComposer() 119 const [limitLines, setLimitLines] = useState( 120 () => countLines(richText?.text) >= MAX_POST_LINES, ··· 127 replyAuthorDid = urip.hostname 128 } 129 130 + const onPressReply = useCallback(() => { 131 openComposer({ 132 replyTo: { 133 uri: post.uri, ··· 140 }) 141 }, [openComposer, post, record, moderation]) 142 143 + const onPressShowMore = useCallback(() => { 144 setLimitLines(false) 145 }, [setLimitLines]) 146 147 + const onBeforePress = useCallback(() => { 148 precacheProfile(queryClient, post.author) 149 }, [queryClient, post.author]) 150 151 const {currentAccount} = useSession() 152 const isMe = replyAuthorDid === currentAccount?.did 153 154 + const [hover, setHover] = useState(false) 155 return ( 156 <Link 157 href={itemHref} ··· 226 style={[a.py_xs]} 227 /> 228 {richText.text ? ( 229 + <View> 230 <RichText 231 enableTags 232 testID="postText" ··· 236 authorHandle={post.author.handle} 237 shouldProxyLinks={true} 238 /> 239 + {limitLines && ( 240 + <ShowMoreTextButton 241 + style={[a.text_md]} 242 + onPress={onPressShowMore} 243 + /> 244 + )} 245 </View> 246 ) : undefined} 247 {post.embed ? ( 248 <Embed ··· 286 }, 287 alert: { 288 marginBottom: 6, 289 }, 290 replyLine: { 291 position: 'absolute',
+7 -20
src/view/com/posts/PostFeedItem.tsx
··· 41 setUnstablePostSource, 42 } from '#/state/unstable-post-source' 43 import {FeedNameText} from '#/view/com/util/FeedInfoText' 44 - import {Link, TextLink, TextLinkOnWebOnly} from '#/view/com/util/Link' 45 import {PostMeta} from '#/view/com/util/PostMeta' 46 import {Text} from '#/view/com/util/text/Text' 47 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' ··· 54 import {type AppModerationCause} from '#/components/Pills' 55 import {Embed} from '#/components/Post/Embed' 56 import {PostEmbedViewContext} from '#/components/Post/Embed/types' 57 import {PostControls} from '#/components/PostControls' 58 import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug' 59 import {ProfileHoverCard} from '#/components/ProfileHoverCard' ··· 501 post: AppBskyFeedDefs.PostView 502 threadgateRecord?: AppBskyFeedThreadgate.Record 503 }): React.ReactNode => { 504 - const pal = usePalette('default') 505 - const {_} = useLingui() 506 const {currentAccount} = useSession() 507 const [limitLines, setLimitLines] = useState( 508 () => countLines(richText.text) >= MAX_POST_LINES, ··· 547 additionalCauses={additionalPostAlerts} 548 /> 549 {richText.text ? ( 550 - <View style={styles.postTextContainer}> 551 <RichText 552 enableTags 553 testID="postText" ··· 557 authorHandle={postAuthor.handle} 558 shouldProxyLinks={true} 559 /> 560 - </View> 561 - ) : undefined} 562 - {limitLines ? ( 563 - <TextLink 564 - text={_(msg`Show More`)} 565 - style={pal.link} 566 - onPress={onPressShowMore} 567 - href="#" 568 - /> 569 ) : undefined} 570 {postEmbed ? ( 571 <View style={[a.pb_xs]}> ··· 688 alert: { 689 marginTop: 6, 690 marginBottom: 6, 691 - }, 692 - postTextContainer: { 693 - flexDirection: 'row', 694 - alignItems: 'center', 695 - flexWrap: 'wrap', 696 - paddingBottom: 2, 697 - overflow: 'hidden', 698 }, 699 contentHiderChild: { 700 marginTop: 6,
··· 41 setUnstablePostSource, 42 } from '#/state/unstable-post-source' 43 import {FeedNameText} from '#/view/com/util/FeedInfoText' 44 + import {Link, TextLinkOnWebOnly} from '#/view/com/util/Link' 45 import {PostMeta} from '#/view/com/util/PostMeta' 46 import {Text} from '#/view/com/util/text/Text' 47 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' ··· 54 import {type AppModerationCause} from '#/components/Pills' 55 import {Embed} from '#/components/Post/Embed' 56 import {PostEmbedViewContext} from '#/components/Post/Embed/types' 57 + import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' 58 import {PostControls} from '#/components/PostControls' 59 import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug' 60 import {ProfileHoverCard} from '#/components/ProfileHoverCard' ··· 502 post: AppBskyFeedDefs.PostView 503 threadgateRecord?: AppBskyFeedThreadgate.Record 504 }): React.ReactNode => { 505 const {currentAccount} = useSession() 506 const [limitLines, setLimitLines] = useState( 507 () => countLines(richText.text) >= MAX_POST_LINES, ··· 546 additionalCauses={additionalPostAlerts} 547 /> 548 {richText.text ? ( 549 + <> 550 <RichText 551 enableTags 552 testID="postText" ··· 556 authorHandle={postAuthor.handle} 557 shouldProxyLinks={true} 558 /> 559 + {limitLines && ( 560 + <ShowMoreTextButton style={[a.text_md]} onPress={onPressShowMore} /> 561 + )} 562 + </> 563 ) : undefined} 564 {postEmbed ? ( 565 <View style={[a.pb_xs]}> ··· 682 alert: { 683 marginTop: 6, 684 marginBottom: 6, 685 }, 686 contentHiderChild: { 687 marginTop: 6,