collapsible post text
This commit is contained in:
committed by
Eric Bailey
parent
6b10ccf3da
commit
c9953c72b0
@@ -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}
|
||||||
|
|||||||
@@ -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() && (
|
||||||
value={richText}
|
<ExpandableRichTextView
|
||||||
style={[a.text_sm]}
|
expanded={expanded}
|
||||||
authorHandle={post.author.handle}
|
setExpanded={setExpanded}
|
||||||
enableTags
|
value={richText}
|
||||||
/>
|
authorHandle={post.author.handle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user