about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx10
-rw-r--r--src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx11
-rw-r--r--src/components/Post/Embed/ExternalEmbed/Gif.tsx8
-rw-r--r--src/components/Post/Embed/ListEmbed.tsx4
-rw-r--r--src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.tsx2
-rw-r--r--src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx4
-rw-r--r--src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx2
-rw-r--r--src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.tsx4
-rw-r--r--src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx3
-rw-r--r--src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx3
-rw-r--r--src/components/Post/Embed/VideoEmbed/index.tsx2
-rw-r--r--src/components/Post/Embed/VideoEmbed/index.web.tsx5
-rw-r--r--src/components/Post/ShowMoreTextButton.tsx56
-rw-r--r--src/screens/PostThread/components/ThreadItemPost.tsx40
-rw-r--r--src/screens/PostThread/components/ThreadItemTreePost.tsx38
-rw-r--r--src/screens/VideoFeed/components/Scrubber.tsx6
-rw-r--r--src/view/com/notifications/NotificationFeedItem.tsx4
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx17
-rw-r--r--src/view/com/post/Post.tsx39
-rw-r--r--src/view/com/posts/PostFeedItem.tsx27
20 files changed, 160 insertions, 125 deletions
diff --git a/src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx b/src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx
index 8a12f0374..0c8f30d2b 100644
--- a/src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx
+++ b/src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx
@@ -1,11 +1,15 @@
 import React from 'react'
-import {ActivityIndicator, GestureResponderEvent, Pressable} from 'react-native'
+import {
+  ActivityIndicator,
+  type GestureResponderEvent,
+  Pressable,
+} from 'react-native'
 import {Image} from 'expo-image'
-import {AppBskyEmbedExternal} from '@atproto/api'
+import {type AppBskyEmbedExternal} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {EmbedPlayerParams} from '#/lib/strings/embed-player'
+import {type EmbedPlayerParams} from '#/lib/strings/embed-player'
 import {isIOS, isNative, isWeb} from '#/platform/detection'
 import {useExternalEmbedsPrefs} from '#/state/preferences'
 import {atoms as a, useTheme} from '#/alf'
diff --git a/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx
index 7f6d53340..392cdd8a1 100644
--- a/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx
+++ b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx
@@ -1,7 +1,7 @@
 import React from 'react'
 import {
   ActivityIndicator,
-  GestureResponderEvent,
+  type GestureResponderEvent,
   Pressable,
   StyleSheet,
   useWindowDimensions,
@@ -16,13 +16,16 @@ import Animated, {
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import {WebView} from 'react-native-webview'
 import {Image} from 'expo-image'
-import {AppBskyEmbedExternal} from '@atproto/api'
+import {type AppBskyEmbedExternal} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
 
-import {NavigationProp} from '#/lib/routes/types'
-import {EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player'
+import {type NavigationProp} from '#/lib/routes/types'
+import {
+  type EmbedPlayerParams,
+  getPlayerAspect,
+} from '#/lib/strings/embed-player'
 import {isNative} from '#/platform/detection'
 import {useExternalEmbedsPrefs} from '#/state/preferences'
 import {EventStopper} from '#/view/com/util/EventStopper'
diff --git a/src/components/Post/Embed/ExternalEmbed/Gif.tsx b/src/components/Post/Embed/ExternalEmbed/Gif.tsx
index a839294f1..8e8499731 100644
--- a/src/components/Post/Embed/ExternalEmbed/Gif.tsx
+++ b/src/components/Post/Embed/ExternalEmbed/Gif.tsx
@@ -1,17 +1,17 @@
 import React from 'react'
 import {
   Pressable,
-  StyleProp,
+  type StyleProp,
   StyleSheet,
   TouchableOpacity,
   View,
-  ViewStyle,
+  type ViewStyle,
 } from 'react-native'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {HITSLOP_20} from '#/lib/constants'
-import {EmbedPlayerParams} from '#/lib/strings/embed-player'
+import {type EmbedPlayerParams} from '#/lib/strings/embed-player'
 import {isWeb} from '#/platform/detection'
 import {useAutoplayDisabled} from '#/state/preferences'
 import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
@@ -22,7 +22,7 @@ import * as Prompt from '#/components/Prompt'
 import {Text} from '#/components/Typography'
 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
 import {GifView} from '../../../../../modules/expo-bluesky-gif-view'
-import {GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types'
+import {type GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types'
 
 function PlaybackControls({
   onPress,
diff --git a/src/components/Post/Embed/ListEmbed.tsx b/src/components/Post/Embed/ListEmbed.tsx
index dc79a7579..82685d271 100644
--- a/src/components/Post/Embed/ListEmbed.tsx
+++ b/src/components/Post/Embed/ListEmbed.tsx
@@ -6,8 +6,8 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {atoms as a, useTheme} from '#/alf'
 import * as ListCard from '#/components/ListCard'
 import {ContentHider} from '#/components/moderation/ContentHider'
-import {EmbedType} from '#/types/bsky/post'
-import {CommonProps} from './types'
+import {type EmbedType} from '#/types/bsky/post'
+import {type CommonProps} from './types'
 
 export function ListEmbed({
   embed,
diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.tsx
index 95401309f..67af7618c 100644
--- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.tsx
+++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.tsx
@@ -1,4 +1,4 @@
-import {StyleProp, ViewStyle} from 'react-native'
+import {type StyleProp, type ViewStyle} from 'react-native'
 import {View} from 'react-native'
 import {msg, plural} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx
index 88879d45a..351e9f305 100644
--- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx
+++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx
@@ -1,6 +1,6 @@
 import React, {useRef} from 'react'
-import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
-import {AppBskyEmbedVideo} from '@atproto/api'
+import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
+import {type AppBskyEmbedVideo} from '@atproto/api'
 import {BlueskyVideoView} from '@haileyok/bluesky-video'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx
index 1b46163cc..37b44751d 100644
--- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx
+++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx
@@ -1,7 +1,7 @@
-import React from 'react'
 import {View} from 'react-native'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import type React from 'react'
 
 import {atoms as a, useTheme} from '#/alf'
 import {Button, ButtonText} from '#/components/Button'
diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.tsx
index 1b69a3e25..9b0c963ea 100644
--- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.tsx
+++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.tsx
@@ -1,5 +1,5 @@
-import React from 'react'
-import {SvgProps} from 'react-native-svg'
+import {type SvgProps} from 'react-native-svg'
+import type React from 'react'
 
 import {PressableWithHover} from '#/view/com/util/PressableWithHover'
 import {atoms as a, useTheme, web} from '#/alf'
diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx
index 96960bad4..d84a90fa6 100644
--- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx
+++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx
@@ -1,7 +1,8 @@
-import React, {useCallback, useEffect, useRef, useState} from 'react'
+import {useCallback, useEffect, useRef, useState} from 'react'
 import {View} from 'react-native'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import type React from 'react'
 
 import {isFirefox, isTouchDevice} from '#/lib/browser'
 import {clamp} from '#/lib/numbers'
diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx
index e0b688075..ec5f23fc0 100644
--- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx
+++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx
@@ -1,8 +1,9 @@
-import React, {useCallback} from 'react'
+import {useCallback} from 'react'
 import {View} from 'react-native'
 import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import type React from 'react'
 
 import {isSafari, isTouchDevice} from '#/lib/browser'
 import {atoms as a} from '#/alf'
diff --git a/src/components/Post/Embed/VideoEmbed/index.tsx b/src/components/Post/Embed/VideoEmbed/index.tsx
index fe29ecad6..8cb78ff70 100644
--- a/src/components/Post/Embed/VideoEmbed/index.tsx
+++ b/src/components/Post/Embed/VideoEmbed/index.tsx
@@ -1,7 +1,7 @@
 import React, {useCallback, useState} from 'react'
 import {ActivityIndicator, View} from 'react-native'
 import {ImageBackground} from 'expo-image'
-import {AppBskyEmbedVideo} from '@atproto/api'
+import {type AppBskyEmbedVideo} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
diff --git a/src/components/Post/Embed/VideoEmbed/index.web.tsx b/src/components/Post/Embed/VideoEmbed/index.web.tsx
index 53adc3b6a..7f601af47 100644
--- a/src/components/Post/Embed/VideoEmbed/index.web.tsx
+++ b/src/components/Post/Embed/VideoEmbed/index.web.tsx
@@ -1,8 +1,9 @@
-import React, {useCallback, useEffect, useRef, useState} from 'react'
+import {useCallback, useEffect, useRef, useState} from 'react'
 import {View} from 'react-native'
-import {AppBskyEmbedVideo} from '@atproto/api'
+import {type AppBskyEmbedVideo} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import type React from 'react'
 
 import {isFirefox} from '#/lib/browser'
 import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
diff --git a/src/components/Post/ShowMoreTextButton.tsx b/src/components/Post/ShowMoreTextButton.tsx
new file mode 100644
index 000000000..bc6db55b9
--- /dev/null
+++ b/src/components/Post/ShowMoreTextButton.tsx
@@ -0,0 +1,56 @@
+import {useCallback, useMemo} from 'react'
+import {LayoutAnimation, type TextStyle} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {HITSLOP_10} from '#/lib/constants'
+import {atoms as a, flatten, type TextStyleProp, useTheme} from '#/alf'
+import {Button} from '#/components/Button'
+import {Text} from '#/components/Typography'
+
+export function ShowMoreTextButton({
+  onPress: onPressProp,
+  style,
+}: TextStyleProp & {onPress: () => void}) {
+  const t = useTheme()
+  const {_} = useLingui()
+
+  const onPress = useCallback(() => {
+    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+    onPressProp()
+  }, [onPressProp])
+
+  const textStyle = useMemo(() => {
+    return flatten([a.leading_snug, a.text_sm, style]) as TextStyle & {
+      fontSize: number
+      lineHeight: number
+    }
+  }, [style])
+
+  return (
+    <Button
+      label={_(msg`Expand post text`)}
+      onPress={onPress}
+      style={[
+        a.self_start,
+        {
+          paddingBottom: textStyle.fontSize / 3,
+        },
+      ]}
+      hitSlop={HITSLOP_10}>
+      {({pressed, hovered}) => (
+        <Text
+          style={[
+            textStyle,
+            {
+              color: t.palette.primary_500,
+              opacity: pressed ? 0.6 : 1,
+              textDecorationLine: hovered ? 'underline' : undefined,
+            },
+          ]}>
+          <Trans>Show More</Trans>
+        </Text>
+      )}
+    </Button>
+  )
+}
diff --git a/src/screens/PostThread/components/ThreadItemPost.tsx b/src/screens/PostThread/components/ThreadItemPost.tsx
index 9393a6d1b..4337397f8 100644
--- a/src/screens/PostThread/components/ThreadItemPost.tsx
+++ b/src/screens/PostThread/components/ThreadItemPost.tsx
@@ -6,13 +6,11 @@ import {
   AtUri,
   RichText as RichTextAPI,
 } from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
+import {Trans} from '@lingui/macro'
 
 import {useActorStatus} from '#/lib/actor-status'
 import {MAX_POST_LINES} from '#/lib/constants'
 import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
-import {usePalette} from '#/lib/hooks/usePalette'
 import {makeProfileLink} from '#/lib/routes/links'
 import {countLines} from '#/lib/strings/helpers'
 import {
@@ -24,7 +22,6 @@ import {type ThreadItem} from '#/state/queries/usePostThread/types'
 import {useSession} from '#/state/session'
 import {type OnPostSuccessData} from '#/state/shell/composer'
 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
-import {TextLink} from '#/view/com/util/Link'
 import {PostMeta} from '#/view/com/util/PostMeta'
 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
 import {
@@ -40,6 +37,7 @@ import {PostAlerts} from '#/components/moderation/PostAlerts'
 import {PostHider} from '#/components/moderation/PostHider'
 import {type AppModerationCause} from '#/components/Pills'
 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
+import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
 import {PostControls} from '#/components/PostControls'
 import {RichText} from '#/components/RichText'
 import * as Skele from '#/components/Skeleton'
@@ -187,8 +185,6 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({
   postShadow: Shadow<AppBskyFeedDefs.PostView>
 }) {
   const t = useTheme()
-  const pal = usePalette('default')
-  const {_} = useLingui()
   const {openComposer} = useOpenComposer()
   const {currentAccount} = useSession()
 
@@ -304,22 +300,22 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({
                 additionalCauses={additionalPostAlerts}
               />
               {richText?.text ? (
-                <RichText
-                  enableTags
-                  value={richText}
-                  style={[a.flex_1, a.text_md]}
-                  numberOfLines={limitLines ? MAX_POST_LINES : undefined}
-                  authorHandle={post.author.handle}
-                  shouldProxyLinks={true}
-                />
-              ) : undefined}
-              {limitLines ? (
-                <TextLink
-                  text={_(msg`Show More`)}
-                  style={pal.link}
-                  onPress={onPressShowMore}
-                  href="#"
-                />
+                <>
+                  <RichText
+                    enableTags
+                    value={richText}
+                    style={[a.flex_1, a.text_md]}
+                    numberOfLines={limitLines ? MAX_POST_LINES : undefined}
+                    authorHandle={post.author.handle}
+                    shouldProxyLinks={true}
+                  />
+                  {limitLines && (
+                    <ShowMoreTextButton
+                      style={[a.text_md]}
+                      onPress={onPressShowMore}
+                    />
+                  )}
+                </>
               ) : undefined}
               {post.embed && (
                 <View style={[a.pb_xs]}>
diff --git a/src/screens/PostThread/components/ThreadItemTreePost.tsx b/src/screens/PostThread/components/ThreadItemTreePost.tsx
index ac659a6e0..a8ffb76f4 100644
--- a/src/screens/PostThread/components/ThreadItemTreePost.tsx
+++ b/src/screens/PostThread/components/ThreadItemTreePost.tsx
@@ -1,4 +1,4 @@
-import React, {memo, useMemo} from 'react'
+import {memo, useCallback, useMemo, useState} from 'react'
 import {View} from 'react-native'
 import {
   type AppBskyFeedDefs,
@@ -6,12 +6,10 @@ import {
   AtUri,
   RichText as RichTextAPI,
 } from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
+import {Trans} from '@lingui/macro'
 
 import {MAX_POST_LINES} from '#/lib/constants'
 import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
-import {usePalette} from '#/lib/hooks/usePalette'
 import {makeProfileLink} from '#/lib/routes/links'
 import {countLines} from '#/lib/strings/helpers'
 import {
@@ -23,7 +21,6 @@ import {type ThreadItem} from '#/state/queries/usePostThread/types'
 import {useSession} from '#/state/session'
 import {type OnPostSuccessData} from '#/state/shell/composer'
 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
-import {TextLink} from '#/view/com/util/Link'
 import {PostMeta} from '#/view/com/util/PostMeta'
 import {
   OUTER_SPACE,
@@ -39,6 +36,7 @@ import {PostAlerts} from '#/components/moderation/PostAlerts'
 import {PostHider} from '#/components/moderation/PostHider'
 import {type AppModerationCause} from '#/components/Pills'
 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
+import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
 import {PostControls} from '#/components/PostControls'
 import {RichText} from '#/components/RichText'
 import * as Skele from '#/components/Skeleton'
@@ -255,8 +253,6 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
   onPostSuccess?: (data: OnPostSuccessData) => void
   threadgateRecord?: AppBskyFeedThreadgate.Record
 }): React.ReactNode {
-  const pal = usePalette('default')
-  const {_} = useLingui()
   const {openComposer} = useOpenComposer()
   const {currentAccount} = useSession()
 
@@ -271,18 +267,18 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
       }),
     [record],
   )
-  const [limitLines, setLimitLines] = React.useState(
+  const [limitLines, setLimitLines] = useState(
     () => countLines(richText?.text) >= MAX_POST_LINES,
   )
   const threadRootUri = record.reply?.root?.uri || post.uri
-  const postHref = React.useMemo(() => {
+  const postHref = useMemo(() => {
     const urip = new AtUri(post.uri)
     return makeProfileLink(post.author, 'post', urip.rkey)
   }, [post.uri, post.author])
   const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
     threadgateRecord,
   })
-  const additionalPostAlerts: AppModerationCause[] = React.useMemo(() => {
+  const additionalPostAlerts: AppModerationCause[] = useMemo(() => {
     const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri)
     const isControlledByViewer =
       new AtUri(threadRootUri).host === currentAccount?.did
@@ -297,7 +293,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
       : []
   }, [post, currentAccount?.did, threadgateHiddenReplies, threadRootUri])
 
-  const onPressReply = React.useCallback(() => {
+  const onPressReply = useCallback(() => {
     openComposer({
       replyTo: {
         uri: post.uri,
@@ -311,7 +307,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
     })
   }, [openComposer, post, record, onPostSuccess, moderation])
 
-  const onPressShowMore = React.useCallback(() => {
+  const onPressShowMore = useCallback(() => {
     setLimitLines(false)
   }, [setLimitLines])
 
@@ -348,7 +344,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
                     additionalCauses={additionalPostAlerts}
                   />
                   {richText?.text ? (
-                    <View>
+                    <>
                       <RichText
                         enableTags
                         value={richText}
@@ -357,15 +353,13 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
                         authorHandle={post.author.handle}
                         shouldProxyLinks={true}
                       />
-                    </View>
-                  ) : undefined}
-                  {limitLines ? (
-                    <TextLink
-                      text={_(msg`Show More`)}
-                      style={pal.link}
-                      onPress={onPressShowMore}
-                      href="#"
-                    />
+                      {limitLines && (
+                        <ShowMoreTextButton
+                          style={[a.text_md]}
+                          onPress={onPressShowMore}
+                        />
+                      )}
+                    </>
                   ) : undefined}
                   {post.embed && (
                     <View style={[a.pb_xs]}>
diff --git a/src/screens/VideoFeed/components/Scrubber.tsx b/src/screens/VideoFeed/components/Scrubber.tsx
index 29cc4b278..69e68ec9e 100644
--- a/src/screens/VideoFeed/components/Scrubber.tsx
+++ b/src/screens/VideoFeed/components/Scrubber.tsx
@@ -3,13 +3,13 @@ import {View} from 'react-native'
 import {
   Gesture,
   GestureDetector,
-  NativeGesture,
+  type NativeGesture,
 } from 'react-native-gesture-handler'
 import Animated, {
   interpolate,
   runOnJS,
   runOnUI,
-  SharedValue,
+  type SharedValue,
   useAnimatedReaction,
   useAnimatedStyle,
   useSharedValue,
@@ -20,7 +20,7 @@ import {
   useSafeAreaInsets,
 } from 'react-native-safe-area-context'
 import {useEventListener} from 'expo'
-import {VideoPlayer} from 'expo-video'
+import {type VideoPlayer} from 'expo-video'
 
 import {tokens} from '#/alf'
 import {atoms as a} from '#/alf'
diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx
index 0a460e77b..85f67919a 100644
--- a/src/view/com/notifications/NotificationFeedItem.tsx
+++ b/src/view/com/notifications/NotificationFeedItem.tsx
@@ -30,6 +30,7 @@ import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
 import {useQueryClient} from '@tanstack/react-query'
 
+import {MAX_POST_LINES} from '#/lib/constants'
 import {useAnimatedValue} from '#/lib/hooks/useAnimatedValue'
 import {usePalette} from '#/lib/hooks/usePalette'
 import {makeProfileLink} from '#/lib/routes/links'
@@ -918,7 +919,8 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) {
         {text?.length > 0 && (
           <Text
             emoji
-            style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
+            style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}
+            numberOfLines={MAX_POST_LINES}>
             {text}
           </Text>
         )}
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 15f5539c9..592224ff5 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -44,7 +44,7 @@ import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replie
 import {type PostSource} from '#/state/unstable-post-source'
 import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
-import {Link, TextLink} from '#/view/com/util/Link'
+import {Link} from '#/view/com/util/Link'
 import {formatCount} from '#/view/com/util/numeric/format'
 import {PostMeta} from '#/view/com/util/PostMeta'
 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
@@ -62,6 +62,7 @@ import {PostAlerts} from '#/components/moderation/PostAlerts'
 import {PostHider} from '#/components/moderation/PostHider'
 import {type AppModerationCause} from '#/components/Pills'
 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
+import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
 import {PostControls} from '#/components/PostControls'
 import * as Prompt from '#/components/Prompt'
 import {RichText} from '#/components/RichText'
@@ -685,16 +686,14 @@ let PostThreadItemLoaded = ({
                     authorHandle={post.author.handle}
                     shouldProxyLinks={true}
                   />
+                  {limitLines && (
+                    <ShowMoreTextButton
+                      style={[a.text_md]}
+                      onPress={onPressShowMore}
+                    />
+                  )}
                 </View>
               ) : undefined}
-              {limitLines ? (
-                <TextLink
-                  text={_(msg`Show More`)}
-                  style={pal.link}
-                  onPress={onPressShowMore}
-                  href="#"
-                />
-              ) : undefined}
               {post.embed && (
                 <View style={[a.pb_xs]}>
                   <Embed
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 19017f30f..6079f5c10 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -1,4 +1,4 @@
-import React, {useMemo, useState} from 'react'
+import {useCallback, useMemo, useState} from 'react'
 import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native'
 import {
   type AppBskyFeedDefs,
@@ -9,8 +9,7 @@ import {
   RichText as RichTextAPI,
 } from '@atproto/api'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
+import {Trans} from '@lingui/macro'
 import {useQueryClient} from '@tanstack/react-query'
 
 import {MAX_POST_LINES} from '#/lib/constants'
@@ -27,7 +26,7 @@ import {
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {precacheProfile} from '#/state/queries/profile'
 import {useSession} from '#/state/session'
-import {Link, TextLink} from '#/view/com/util/Link'
+import {Link} from '#/view/com/util/Link'
 import {PostMeta} from '#/view/com/util/PostMeta'
 import {Text} from '#/view/com/util/text/Text'
 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
@@ -37,6 +36,7 @@ import {ContentHider} from '#/components/moderation/ContentHider'
 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
 import {PostAlerts} from '#/components/moderation/PostAlerts'
 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
+import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
 import {PostControls} from '#/components/PostControls'
 import {ProfileHoverCard} from '#/components/ProfileHoverCard'
 import {RichText} from '#/components/RichText'
@@ -115,7 +115,6 @@ function PostInner({
 }) {
   const queryClient = useQueryClient()
   const pal = usePalette('default')
-  const {_} = useLingui()
   const {openComposer} = useOpenComposer()
   const [limitLines, setLimitLines] = useState(
     () => countLines(richText?.text) >= MAX_POST_LINES,
@@ -128,7 +127,7 @@ function PostInner({
     replyAuthorDid = urip.hostname
   }
 
-  const onPressReply = React.useCallback(() => {
+  const onPressReply = useCallback(() => {
     openComposer({
       replyTo: {
         uri: post.uri,
@@ -141,18 +140,18 @@ function PostInner({
     })
   }, [openComposer, post, record, moderation])
 
-  const onPressShowMore = React.useCallback(() => {
+  const onPressShowMore = useCallback(() => {
     setLimitLines(false)
   }, [setLimitLines])
 
-  const onBeforePress = React.useCallback(() => {
+  const onBeforePress = useCallback(() => {
     precacheProfile(queryClient, post.author)
   }, [queryClient, post.author])
 
   const {currentAccount} = useSession()
   const isMe = replyAuthorDid === currentAccount?.did
 
-  const [hover, setHover] = React.useState(false)
+  const [hover, setHover] = useState(false)
   return (
     <Link
       href={itemHref}
@@ -227,7 +226,7 @@ function PostInner({
               style={[a.py_xs]}
             />
             {richText.text ? (
-              <View style={styles.postTextContainer}>
+              <View>
                 <RichText
                   enableTags
                   testID="postText"
@@ -237,16 +236,14 @@ function PostInner({
                   authorHandle={post.author.handle}
                   shouldProxyLinks={true}
                 />
+                {limitLines && (
+                  <ShowMoreTextButton
+                    style={[a.text_md]}
+                    onPress={onPressShowMore}
+                  />
+                )}
               </View>
             ) : undefined}
-            {limitLines ? (
-              <TextLink
-                text={_(msg`Show More`)}
-                style={pal.link}
-                onPress={onPressShowMore}
-                href="#"
-              />
-            ) : undefined}
             {post.embed ? (
               <Embed
                 embed={post.embed}
@@ -290,12 +287,6 @@ const styles = StyleSheet.create({
   alert: {
     marginBottom: 6,
   },
-  postTextContainer: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    flexWrap: 'wrap',
-    overflow: 'hidden',
-  },
   replyLine: {
     position: 'absolute',
     left: 36,
diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx
index 6755f013d..96d5df5e9 100644
--- a/src/view/com/posts/PostFeedItem.tsx
+++ b/src/view/com/posts/PostFeedItem.tsx
@@ -41,7 +41,7 @@ import {
   setUnstablePostSource,
 } from '#/state/unstable-post-source'
 import {FeedNameText} from '#/view/com/util/FeedInfoText'
-import {Link, TextLink, TextLinkOnWebOnly} from '#/view/com/util/Link'
+import {Link, TextLinkOnWebOnly} from '#/view/com/util/Link'
 import {PostMeta} from '#/view/com/util/PostMeta'
 import {Text} from '#/view/com/util/text/Text'
 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
@@ -54,6 +54,7 @@ import {PostAlerts} from '#/components/moderation/PostAlerts'
 import {type AppModerationCause} from '#/components/Pills'
 import {Embed} from '#/components/Post/Embed'
 import {PostEmbedViewContext} from '#/components/Post/Embed/types'
+import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
 import {PostControls} from '#/components/PostControls'
 import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
 import {ProfileHoverCard} from '#/components/ProfileHoverCard'
@@ -501,8 +502,6 @@ let PostContent = ({
   post: AppBskyFeedDefs.PostView
   threadgateRecord?: AppBskyFeedThreadgate.Record
 }): React.ReactNode => {
-  const pal = usePalette('default')
-  const {_} = useLingui()
   const {currentAccount} = useSession()
   const [limitLines, setLimitLines] = useState(
     () => countLines(richText.text) >= MAX_POST_LINES,
@@ -547,7 +546,7 @@ let PostContent = ({
         additionalCauses={additionalPostAlerts}
       />
       {richText.text ? (
-        <View style={styles.postTextContainer}>
+        <>
           <RichText
             enableTags
             testID="postText"
@@ -557,15 +556,10 @@ let PostContent = ({
             authorHandle={postAuthor.handle}
             shouldProxyLinks={true}
           />
-        </View>
-      ) : undefined}
-      {limitLines ? (
-        <TextLink
-          text={_(msg`Show More`)}
-          style={pal.link}
-          onPress={onPressShowMore}
-          href="#"
-        />
+          {limitLines && (
+            <ShowMoreTextButton style={[a.text_md]} onPress={onPressShowMore} />
+          )}
+        </>
       ) : undefined}
       {postEmbed ? (
         <View style={[a.pb_xs]}>
@@ -689,13 +683,6 @@ const styles = StyleSheet.create({
     marginTop: 6,
     marginBottom: 6,
   },
-  postTextContainer: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    flexWrap: 'wrap',
-    paddingBottom: 2,
-    overflow: 'hidden',
-  },
   contentHiderChild: {
     marginTop: 6,
   },