collapsible post text

This commit is contained in:
Samuel Newman
2025-01-15 22:22:35 +00:00
committed by Eric Bailey
parent 6b10ccf3da
commit c9953c72b0
2 changed files with 92 additions and 13 deletions
+9 -1
View File
@@ -19,7 +19,7 @@ import {Text, TextProps} from '#/components/Typography'
const WORD_WRAP = {wordWrap: 1} const WORD_WRAP = {wordWrap: 1}
export type RichTextProps = TextStyleProp & export type RichTextProps = TextStyleProp &
Pick<TextProps, 'selectable'> & { Pick<TextProps, 'selectable' | 'onLayout' | 'onTextLayout'> & {
value: RichTextAPI | string value: RichTextAPI | string
testID?: string testID?: string
numberOfLines?: number numberOfLines?: number
@@ -43,6 +43,8 @@ export function RichText({
onLinkPress, onLinkPress,
interactiveStyle, interactiveStyle,
emojiMultiplier = 1.85, emojiMultiplier = 1.85,
onLayout,
onTextLayout,
}: RichTextProps) { }: RichTextProps) {
const richText = React.useMemo( const richText = React.useMemo(
() => () =>
@@ -70,6 +72,8 @@ export function RichText({
selectable={selectable} selectable={selectable}
testID={testID} testID={testID}
style={[plainStyles, {fontSize}]} style={[plainStyles, {fontSize}]}
onLayout={onLayout}
onTextLayout={onTextLayout}
// @ts-ignore web only -prf // @ts-ignore web only -prf
dataSet={WORD_WRAP}> dataSet={WORD_WRAP}>
{text} {text}
@@ -83,6 +87,8 @@ export function RichText({
testID={testID} testID={testID}
style={plainStyles} style={plainStyles}
numberOfLines={numberOfLines} numberOfLines={numberOfLines}
onLayout={onLayout}
onTextLayout={onTextLayout}
// @ts-ignore web only -prf // @ts-ignore web only -prf
dataSet={WORD_WRAP}> dataSet={WORD_WRAP}>
{text} {text}
@@ -163,6 +169,8 @@ export function RichText({
testID={testID} testID={testID}
style={plainStyles} style={plainStyles}
numberOfLines={numberOfLines} numberOfLines={numberOfLines}
onLayout={onLayout}
onTextLayout={onTextLayout}
// @ts-ignore web only -prf // @ts-ignore web only -prf
dataSet={WORD_WRAP}> dataSet={WORD_WRAP}>
{els} {els}
+80 -9
View File
@@ -1,6 +1,8 @@
import {useCallback, useEffect, useMemo, useState} from 'react' import {useCallback, useEffect, useMemo, useState} from 'react'
import { import {
LayoutAnimation,
ListRenderItem, ListRenderItem,
ScrollView,
useWindowDimensions, useWindowDimensions,
View, View,
ViewToken, ViewToken,
@@ -19,7 +21,8 @@ import {
AtUri, AtUri,
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import { import {
useFocusEffect, useFocusEffect,
useIsFocused, useIsFocused,
@@ -27,7 +30,7 @@ import {
} from '@react-navigation/native' } from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {VIBES_FEED_URI} from '#/lib/constants' import {HITSLOP_20, VIBES_FEED_URI} from '#/lib/constants'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
@@ -40,7 +43,7 @@ import {List} from '#/view/com/util/List'
import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls' import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, ThemeProvider, useTheme} from '#/alf' import {atoms as a, ThemeProvider, useTheme} from '#/alf'
import {Button} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {ListFooter} from '#/components/Lists' import {ListFooter} from '#/components/Lists'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
@@ -272,6 +275,8 @@ function VibeOverlay({
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const t = useTheme() const t = useTheme()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const [expanded, setExpanded] = useState(false)
const pushToProfile = useNonReactiveCallback(() => { const pushToProfile = useNonReactiveCallback(() => {
navigation.navigate('Profile', {name: post.author.did}) navigation.navigate('Profile', {name: post.author.did})
}) })
@@ -288,7 +293,7 @@ function VibeOverlay({
const dragLeftGesture = Gesture.Pan() const dragLeftGesture = Gesture.Pan()
.activeOffsetX([0, 10]) .activeOffsetX([0, 10])
.failOffsetX([-10, 0]) .failOffsetX([-10, 0])
.failOffsetY([-10, 10]) .failOffsetY([-5, 5])
.maxPointers(1) .maxPointers(1)
.onEnd(evt => { .onEnd(evt => {
'worklet' 'worklet'
@@ -317,12 +322,16 @@ function VibeOverlay({
<View /> <View />
</Button> </Button>
<LinearGradient <LinearGradient
colors={['rgba(0,0,0,0)', 'rgba(0,0,0,0.8)']} colors={
expanded
? ['rgba(0,0,0,0)', 'rgba(0,0,0,0.6)', 'rgba(0,0,0,0.8)']
: ['rgba(0,0,0,0)', 'rgba(0,0,0,0.8)']
}
style={[a.w_full, a.px_xl, a.py_sm, a.gap_sm]}> style={[a.w_full, a.px_xl, a.py_sm, a.gap_sm]}>
<View style={[a.flex_row, a.gap_md, a.align_center]}> <View style={[a.flex_row, a.gap_md, a.align_center]}>
<PreviewableUserAvatar profile={post.author} size={32} /> <PreviewableUserAvatar profile={post.author} size={32} />
<View> <View>
<Text style={[a.text_md, a.font_heavy]} numberOfLines={1}> <Text style={[a.text_md, a.font_heavy]} emoji numberOfLines={1}>
{sanitizeDisplayName( {sanitizeDisplayName(
post.author.displayName || post.author.handle, post.author.displayName || post.author.handle,
)} )}
@@ -334,12 +343,14 @@ function VibeOverlay({
</Text> </Text>
</View> </View>
</View> </View>
<RichText {record?.text?.trim() && (
<ExpandableRichTextView
expanded={expanded}
setExpanded={setExpanded}
value={richText} value={richText}
style={[a.text_sm]}
authorHandle={post.author.handle} authorHandle={post.author.handle}
enableTags
/> />
)}
{postShadow !== POST_TOMBSTONE && record && ( {postShadow !== POST_TOMBSTONE && record && (
<PostCtrls <PostCtrls
richText={richText} richText={richText}
@@ -357,3 +368,63 @@ function VibeOverlay({
</GestureDetector> </GestureDetector>
) )
} }
function ExpandableRichTextView({
value,
authorHandle,
expanded,
setExpanded,
}: {
value: RichTextAPI
authorHandle?: string
expanded: boolean
setExpanded: React.Dispatch<React.SetStateAction<boolean>>
}) {
const {height: screenHeight} = useWindowDimensions()
const [constrained, setConstrained] = useState(false)
const [contentHeight, setContentHeight] = useState(0)
const {_} = useLingui()
return (
<ScrollView
scrollEnabled={expanded}
onContentSizeChange={(_w, h) => {
LayoutAnimation.configureNext({
duration: 500,
update: {type: 'spring', springDamping: 0.6},
})
setContentHeight(h)
}}
style={{height: Math.min(contentHeight, screenHeight * 0.5)}}
contentContainerStyle={[
a.gap_xs,
expanded ? [a.align_start] : a.flex_row,
]}>
<RichText
value={value}
style={[a.text_sm, a.flex_1]}
authorHandle={authorHandle}
enableTags
numberOfLines={expanded ? undefined : constrained ? 1 : 2}
onTextLayout={evt => {
if (!constrained && evt.nativeEvent.lines.length > 1) {
setConstrained(true)
}
}}
/>
{constrained && (
<Button
label={expanded ? _(msg`Read less`) : _(msg`Read more`)}
hitSlop={HITSLOP_20}
onPress={() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
setExpanded(prev => !prev)
}}>
<ButtonText>
{expanded ? <Trans>Read less</Trans> : <Trans>Read more</Trans>}
</ButtonText>
</Button>
)}
</ScrollView>
)
}