experimental gesture view
This commit is contained in:
@@ -0,0 +1,397 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {ColorValue, Dimensions, StyleSheet, View} from 'react-native'
|
||||||
|
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
||||||
|
import Animated, {
|
||||||
|
clamp,
|
||||||
|
interpolate,
|
||||||
|
interpolateColor,
|
||||||
|
runOnJS,
|
||||||
|
useAnimatedReaction,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useDerivedValue,
|
||||||
|
useSharedValue,
|
||||||
|
withSequence,
|
||||||
|
withTiming,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
|
||||||
|
import {useHaptics} from 'lib/haptics'
|
||||||
|
import absoluteFill = StyleSheet.absoluteFill
|
||||||
|
|
||||||
|
interface GestureAction {
|
||||||
|
color: ColorValue
|
||||||
|
action: () => void
|
||||||
|
threshold: number
|
||||||
|
icon: React.ElementType
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GestureActions {
|
||||||
|
leftFirst?: GestureAction
|
||||||
|
leftSecond?: GestureAction
|
||||||
|
rightFirst?: GestureAction
|
||||||
|
rightSecond?: GestureAction
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_WIDTH = Dimensions.get('screen').width
|
||||||
|
const ICON_SIZE = 42
|
||||||
|
|
||||||
|
export function GestureActionView({
|
||||||
|
children,
|
||||||
|
actions,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
actions: GestureActions
|
||||||
|
}) {
|
||||||
|
if (
|
||||||
|
(actions.leftSecond && !actions.leftFirst) ||
|
||||||
|
(actions.rightSecond && !actions.rightFirst)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'You must provide the first action before the second action',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [activeAction, setActiveAction] = React.useState<
|
||||||
|
'leftFirst' | 'leftSecond' | 'rightFirst' | 'rightSecond' | null
|
||||||
|
>(null)
|
||||||
|
|
||||||
|
const haptic = useHaptics()
|
||||||
|
|
||||||
|
const transX = useSharedValue(0)
|
||||||
|
const clampedTransX = useDerivedValue(() => {
|
||||||
|
const min = actions.leftFirst ? -MAX_WIDTH : 0
|
||||||
|
const max = actions.rightFirst ? MAX_WIDTH : 0
|
||||||
|
return clamp(transX.value, min, max)
|
||||||
|
})
|
||||||
|
const iconScale = useSharedValue(1)
|
||||||
|
|
||||||
|
const isActive = useSharedValue(false)
|
||||||
|
const hitFirst = useSharedValue(false)
|
||||||
|
const hitSecond = useSharedValue(false)
|
||||||
|
|
||||||
|
const runPopAnimation = () => {
|
||||||
|
'worklet'
|
||||||
|
iconScale.value = withSequence(
|
||||||
|
withTiming(1.2, {duration: 175}),
|
||||||
|
withTiming(1, {duration: 100}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
useAnimatedReaction(
|
||||||
|
() => transX,
|
||||||
|
() => {
|
||||||
|
if (transX.value === 0) {
|
||||||
|
runOnJS(setActiveAction)(null)
|
||||||
|
} else if (transX.value < 0) {
|
||||||
|
if (
|
||||||
|
actions.leftSecond &&
|
||||||
|
transX.value <= -actions.leftSecond.threshold
|
||||||
|
) {
|
||||||
|
if (activeAction !== 'leftSecond') {
|
||||||
|
runOnJS(setActiveAction)('leftSecond')
|
||||||
|
}
|
||||||
|
} else if (activeAction !== 'leftFirst') {
|
||||||
|
runOnJS(setActiveAction)('leftFirst')
|
||||||
|
}
|
||||||
|
} else if (transX.value > 0) {
|
||||||
|
if (
|
||||||
|
actions.rightSecond &&
|
||||||
|
transX.value > actions.rightSecond.threshold
|
||||||
|
) {
|
||||||
|
if (activeAction !== 'rightSecond') {
|
||||||
|
runOnJS(setActiveAction)('rightSecond')
|
||||||
|
}
|
||||||
|
} else if (activeAction !== 'rightFirst') {
|
||||||
|
runOnJS(setActiveAction)('rightFirst')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const panGesture = Gesture.Pan()
|
||||||
|
.activeOffsetX([-10, 10])
|
||||||
|
// Absurdly high value so it doesn't interfere with the pan gestures above (i.e., scroll)
|
||||||
|
// reanimated doesn't offer great support for disabling y/x axes :/
|
||||||
|
.activeOffsetY([-200, 200])
|
||||||
|
.onStart(() => {
|
||||||
|
isActive.value = true
|
||||||
|
})
|
||||||
|
.onChange(e => {
|
||||||
|
transX.value = e.translationX
|
||||||
|
|
||||||
|
if (e.translationX < 0) {
|
||||||
|
// Left side
|
||||||
|
if (actions.leftSecond) {
|
||||||
|
if (
|
||||||
|
e.translationX <= -actions.leftSecond.threshold &&
|
||||||
|
!hitSecond.value
|
||||||
|
) {
|
||||||
|
runPopAnimation()
|
||||||
|
runOnJS(haptic)()
|
||||||
|
hitSecond.value = true
|
||||||
|
} else if (
|
||||||
|
hitSecond.value &&
|
||||||
|
e.translationX > -actions.leftSecond.threshold
|
||||||
|
) {
|
||||||
|
runPopAnimation()
|
||||||
|
hitSecond.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hitSecond.value && actions.leftFirst) {
|
||||||
|
if (
|
||||||
|
e.translationX <= -actions.leftFirst.threshold &&
|
||||||
|
!hitFirst.value
|
||||||
|
) {
|
||||||
|
runPopAnimation()
|
||||||
|
runOnJS(haptic)()
|
||||||
|
hitFirst.value = true
|
||||||
|
} else if (
|
||||||
|
hitFirst.value &&
|
||||||
|
e.translationX > -actions.leftFirst.threshold
|
||||||
|
) {
|
||||||
|
hitFirst.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (e.translationX > 0) {
|
||||||
|
// Right side
|
||||||
|
if (actions.rightSecond) {
|
||||||
|
if (
|
||||||
|
e.translationX >= actions.rightSecond.threshold &&
|
||||||
|
!hitSecond.value
|
||||||
|
) {
|
||||||
|
runPopAnimation()
|
||||||
|
runOnJS(haptic)()
|
||||||
|
hitSecond.value = true
|
||||||
|
} else if (
|
||||||
|
hitSecond.value &&
|
||||||
|
e.translationX < actions.rightSecond.threshold
|
||||||
|
) {
|
||||||
|
runPopAnimation()
|
||||||
|
hitSecond.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hitSecond.value && actions.rightFirst) {
|
||||||
|
if (
|
||||||
|
e.translationX >= actions.rightFirst.threshold &&
|
||||||
|
!hitFirst.value
|
||||||
|
) {
|
||||||
|
runPopAnimation()
|
||||||
|
runOnJS(haptic)()
|
||||||
|
hitFirst.value = true
|
||||||
|
} else if (
|
||||||
|
hitFirst.value &&
|
||||||
|
e.translationX < actions.rightFirst.threshold
|
||||||
|
) {
|
||||||
|
hitFirst.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onEnd(e => {
|
||||||
|
if (e.translationX < 0) {
|
||||||
|
if (hitSecond.value && actions.leftSecond) {
|
||||||
|
runOnJS(actions.leftSecond.action)()
|
||||||
|
} else if (hitFirst.value && actions.leftFirst) {
|
||||||
|
runOnJS(actions.leftFirst.action)()
|
||||||
|
}
|
||||||
|
} else if (e.translationX > 0) {
|
||||||
|
if (hitSecond.value && actions.rightSecond) {
|
||||||
|
runOnJS(actions.rightSecond.action)()
|
||||||
|
} else if (hitSecond.value && actions.rightFirst) {
|
||||||
|
runOnJS(actions.rightFirst.action)()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transX.value = withTiming(0, {duration: 200})
|
||||||
|
hitFirst.value = false
|
||||||
|
hitSecond.value = false
|
||||||
|
isActive.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
const composedGesture = Gesture.Simultaneous(panGesture)
|
||||||
|
|
||||||
|
const animatedSliderStyle = useAnimatedStyle(() => {
|
||||||
|
return {
|
||||||
|
transform: [{translateX: clampedTransX.value}],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const leftSideInterpolation = React.useMemo(() => {
|
||||||
|
return createInterpolation({
|
||||||
|
firstColor: actions.leftFirst?.color,
|
||||||
|
secondColor: actions.leftSecond?.color,
|
||||||
|
firstThreshold: actions.leftFirst?.threshold,
|
||||||
|
secondThreshold: actions.leftSecond?.threshold,
|
||||||
|
side: 'left',
|
||||||
|
})
|
||||||
|
}, [actions.leftFirst, actions.leftSecond])
|
||||||
|
|
||||||
|
const rightSideInterpolation = React.useMemo(() => {
|
||||||
|
return createInterpolation({
|
||||||
|
firstColor: actions.rightFirst?.color,
|
||||||
|
secondColor: actions.rightSecond?.color,
|
||||||
|
firstThreshold: actions.rightFirst?.threshold,
|
||||||
|
secondThreshold: actions.rightSecond?.threshold,
|
||||||
|
side: 'right',
|
||||||
|
})
|
||||||
|
}, [actions.rightFirst, actions.rightSecond])
|
||||||
|
|
||||||
|
const interpolation = React.useMemo<{
|
||||||
|
inputRange: number[]
|
||||||
|
outputRange: ColorValue[]
|
||||||
|
}>(() => {
|
||||||
|
if (!actions.leftFirst) {
|
||||||
|
return rightSideInterpolation!
|
||||||
|
} else if (!actions.rightFirst) {
|
||||||
|
return leftSideInterpolation!
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
inputRange: [
|
||||||
|
...leftSideInterpolation.inputRange,
|
||||||
|
...rightSideInterpolation.inputRange,
|
||||||
|
],
|
||||||
|
outputRange: [
|
||||||
|
...leftSideInterpolation.outputRange,
|
||||||
|
...rightSideInterpolation.outputRange,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
leftSideInterpolation,
|
||||||
|
rightSideInterpolation,
|
||||||
|
actions.leftFirst,
|
||||||
|
actions.rightFirst,
|
||||||
|
])
|
||||||
|
|
||||||
|
const animatedBackgroundStyle = useAnimatedStyle(() => {
|
||||||
|
return {
|
||||||
|
backgroundColor: interpolateColor(
|
||||||
|
clampedTransX.value,
|
||||||
|
interpolation.inputRange,
|
||||||
|
// @ts-expect-error - Weird type expected by reanimated, but this is okay
|
||||||
|
interpolation.outputRange,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const animatedIconStyle = useAnimatedStyle(() => {
|
||||||
|
const absTransX = Math.abs(clampedTransX.value)
|
||||||
|
return {
|
||||||
|
opacity: interpolate(absTransX, [0, 75], [0.15, 1]),
|
||||||
|
transform: [{scale: iconScale.value}],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GestureDetector gesture={composedGesture}>
|
||||||
|
<View>
|
||||||
|
<Animated.View style={[absoluteFill, animatedBackgroundStyle]}>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
marginHorizontal: 12,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems:
|
||||||
|
activeAction === 'leftFirst' || activeAction === 'leftSecond'
|
||||||
|
? 'flex-end'
|
||||||
|
: 'flex-start',
|
||||||
|
}}>
|
||||||
|
<Animated.View style={[animatedIconStyle]}>
|
||||||
|
{activeAction === 'leftFirst' && actions.leftFirst?.icon ? (
|
||||||
|
<actions.leftFirst.icon
|
||||||
|
height={ICON_SIZE}
|
||||||
|
width={ICON_SIZE}
|
||||||
|
style={{
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : activeAction === 'leftSecond' && actions.leftSecond?.icon ? (
|
||||||
|
<actions.leftSecond.icon
|
||||||
|
height={ICON_SIZE}
|
||||||
|
width={ICON_SIZE}
|
||||||
|
style={{color: 'white'}}
|
||||||
|
/>
|
||||||
|
) : activeAction === 'rightFirst' && actions.rightFirst?.icon ? (
|
||||||
|
<actions.rightFirst.icon
|
||||||
|
height={ICON_SIZE}
|
||||||
|
width={ICON_SIZE}
|
||||||
|
style={{color: 'white'}}
|
||||||
|
/>
|
||||||
|
) : activeAction === 'rightSecond' &&
|
||||||
|
actions.rightSecond?.icon ? (
|
||||||
|
<actions.rightSecond.icon
|
||||||
|
height={ICON_SIZE}
|
||||||
|
width={ICON_SIZE}
|
||||||
|
style={{color: 'white'}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
<Animated.View style={animatedSliderStyle}>{children}</Animated.View>
|
||||||
|
</View>
|
||||||
|
</GestureDetector>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInterpolation({
|
||||||
|
firstColor,
|
||||||
|
secondColor,
|
||||||
|
firstThreshold,
|
||||||
|
secondThreshold,
|
||||||
|
side,
|
||||||
|
}: {
|
||||||
|
firstColor?: ColorValue
|
||||||
|
secondColor?: ColorValue
|
||||||
|
firstThreshold?: number
|
||||||
|
secondThreshold?: number
|
||||||
|
side: 'left' | 'right'
|
||||||
|
}): {
|
||||||
|
inputRange: number[]
|
||||||
|
outputRange: ColorValue[]
|
||||||
|
} {
|
||||||
|
if ((secondThreshold && !secondColor) || (!secondThreshold && secondColor)) {
|
||||||
|
throw new Error(
|
||||||
|
'You must provide a second color if you provide a second threshold',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstThreshold) {
|
||||||
|
return {
|
||||||
|
inputRange: [0],
|
||||||
|
outputRange: ['transparent'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = side === 'left' ? -15 : 15
|
||||||
|
|
||||||
|
if (side === 'left') {
|
||||||
|
firstThreshold = -firstThreshold
|
||||||
|
|
||||||
|
if (secondThreshold) {
|
||||||
|
secondThreshold = -secondThreshold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let res
|
||||||
|
if (secondThreshold) {
|
||||||
|
res = {
|
||||||
|
inputRange: [0, firstThreshold + offset, secondThreshold + offset],
|
||||||
|
outputRange: ['transparent', firstColor!, secondColor!],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res = {
|
||||||
|
inputRange: [0, firstThreshold + offset],
|
||||||
|
outputRange: ['transparent', firstColor!],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (side === 'left') {
|
||||||
|
res = {
|
||||||
|
inputRange: res.inputRange.toReversed(),
|
||||||
|
outputRange: res.outputRange.toReversed(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export function SwipeActionItem({children}: {children: React.ReactNode}) {
|
||||||
|
return children
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import {ThreadPost} from '#/state/queries/post-thread'
|
|||||||
import {useComposerControls} from '#/state/shell/composer'
|
import {useComposerControls} from '#/state/shell/composer'
|
||||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||||
import {MAX_POST_LINES} from 'lib/constants'
|
import {MAX_POST_LINES} from 'lib/constants'
|
||||||
|
import {GestureActionView} from 'lib/custom-animations/GestureActionView'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
@@ -28,9 +29,12 @@ import {countLines} from 'lib/strings/helpers'
|
|||||||
import {niceDate} from 'lib/strings/time'
|
import {niceDate} from 'lib/strings/time'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
|
import {usePostLikeMutationQueue} from 'state/queries/post'
|
||||||
import {useSession} from 'state/session'
|
import {useSession} from 'state/session'
|
||||||
import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn'
|
import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Bubble_Stroke2_Corner2_Rounded} from '#/components/icons/Bubble'
|
||||||
|
import {Heart2_Filled_Stroke2_Corner0_Rounded} from '#/components/icons/Heart2'
|
||||||
import {AppModerationCause} from '#/components/Pills'
|
import {AppModerationCause} from '#/components/Pills'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
import {ContentHider} from '../../../components/moderation/ContentHider'
|
import {ContentHider} from '../../../components/moderation/ContentHider'
|
||||||
@@ -181,12 +185,17 @@ let PostThreadItemLoaded = ({
|
|||||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const t = useTheme()
|
||||||
const {_, i18n} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
const {openComposer} = useComposerControls()
|
const {openComposer} = useComposerControls()
|
||||||
const [limitLines, setLimitLines] = React.useState(
|
const [limitLines, setLimitLines] = React.useState(
|
||||||
() => countLines(richText?.text) >= MAX_POST_LINES,
|
() => countLines(richText?.text) >= MAX_POST_LINES,
|
||||||
)
|
)
|
||||||
|
const [queueLike, queueUnlike] = usePostLikeMutationQueue(
|
||||||
|
post,
|
||||||
|
'PostThreadItem',
|
||||||
|
)
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const rootUri = record.reply?.root?.uri || post.uri
|
const rootUri = record.reply?.root?.uri || post.uri
|
||||||
const postHref = React.useMemo(() => {
|
const postHref = React.useMemo(() => {
|
||||||
@@ -465,178 +474,204 @@ let PostThreadItemLoaded = ({
|
|||||||
const isThreadedChildAdjacentBot =
|
const isThreadedChildAdjacentBot =
|
||||||
isThreadedChild && nextPost?.ctx.depth === depth
|
isThreadedChild && nextPost?.ctx.depth === depth
|
||||||
return (
|
return (
|
||||||
<PostOuterWrapper
|
<GestureActionView
|
||||||
post={post}
|
actions={{
|
||||||
depth={depth}
|
leftFirst: {
|
||||||
showParentReplyLine={!!showParentReplyLine}
|
color: '#ec4899',
|
||||||
treeView={treeView}
|
action: () => {
|
||||||
hasPrecedingItem={hasPrecedingItem}
|
if (!post.viewer?.like) {
|
||||||
hideTopBorder={hideTopBorder}>
|
queueLike()
|
||||||
<PostHider
|
} else {
|
||||||
testID={`postThreadItem-by-${post.author.handle}`}
|
queueUnlike()
|
||||||
href={postHref}
|
}
|
||||||
disabled={overrideBlur}
|
},
|
||||||
style={[pal.view]}
|
threshold: 85,
|
||||||
modui={moderation.ui('contentList')}
|
icon: Heart2_Filled_Stroke2_Corner0_Rounded,
|
||||||
iconSize={isThreadedChild ? 26 : 38}
|
},
|
||||||
iconStyles={
|
leftSecond: {
|
||||||
isThreadedChild ? {marginRight: 4} : {marginLeft: 2, marginRight: 2}
|
color: t.palette.primary_500,
|
||||||
}
|
action: () => {
|
||||||
profile={post.author}
|
onPressReply()
|
||||||
interpretFilterAsBlur>
|
},
|
||||||
<View
|
threshold: 170,
|
||||||
style={{
|
icon: Bubble_Stroke2_Corner2_Rounded,
|
||||||
flexDirection: 'row',
|
},
|
||||||
gap: 10,
|
}}>
|
||||||
paddingLeft: 8,
|
<PostOuterWrapper
|
||||||
height: isThreadedChildAdjacentTop ? 8 : 16,
|
post={post}
|
||||||
}}>
|
depth={depth}
|
||||||
<View style={{width: 38}}>
|
showParentReplyLine={!!showParentReplyLine}
|
||||||
{!isThreadedChild && showParentReplyLine && (
|
treeView={treeView}
|
||||||
<View
|
hasPrecedingItem={hasPrecedingItem}
|
||||||
style={[
|
hideTopBorder={hideTopBorder}>
|
||||||
styles.replyLine,
|
<PostHider
|
||||||
{
|
testID={`postThreadItem-by-${post.author.handle}`}
|
||||||
flexGrow: 1,
|
href={postHref}
|
||||||
backgroundColor: pal.colors.replyLine,
|
disabled={overrideBlur}
|
||||||
marginBottom: 4,
|
style={[pal.view]}
|
||||||
},
|
modui={moderation.ui('contentList')}
|
||||||
]}
|
iconSize={isThreadedChild ? 26 : 38}
|
||||||
/>
|
iconStyles={
|
||||||
)}
|
isThreadedChild
|
||||||
</View>
|
? {marginRight: 4}
|
||||||
</View>
|
: {marginLeft: 2, marginRight: 2}
|
||||||
|
}
|
||||||
<View
|
profile={post.author}
|
||||||
style={[
|
interpretFilterAsBlur>
|
||||||
styles.layout,
|
<View
|
||||||
{
|
style={{
|
||||||
paddingBottom:
|
flexDirection: 'row',
|
||||||
showChildReplyLine && !isThreadedChild
|
gap: 10,
|
||||||
? 0
|
paddingLeft: 8,
|
||||||
: isThreadedChildAdjacentBot
|
height: isThreadedChildAdjacentTop ? 8 : 16,
|
||||||
? 4
|
}}>
|
||||||
: 8,
|
<View style={{width: 38}}>
|
||||||
},
|
{!isThreadedChild && showParentReplyLine && (
|
||||||
]}>
|
|
||||||
{/* If we are in threaded mode, the avatar is rendered in PostMeta */}
|
|
||||||
{!isThreadedChild && (
|
|
||||||
<View style={styles.layoutAvi}>
|
|
||||||
<PreviewableUserAvatar
|
|
||||||
size={38}
|
|
||||||
profile={post.author}
|
|
||||||
moderation={moderation.ui('avatar')}
|
|
||||||
type={post.author.associated?.labeler ? 'labeler' : 'user'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{showChildReplyLine && (
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.replyLine,
|
styles.replyLine,
|
||||||
{
|
{
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
backgroundColor: pal.colors.replyLine,
|
backgroundColor: pal.colors.replyLine,
|
||||||
marginTop: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
</View>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
style={
|
style={[
|
||||||
isThreadedChild
|
styles.layout,
|
||||||
? styles.layoutContentThreaded
|
{
|
||||||
: styles.layoutContent
|
paddingBottom:
|
||||||
}>
|
showChildReplyLine && !isThreadedChild
|
||||||
<PostMeta
|
? 0
|
||||||
author={post.author}
|
: isThreadedChildAdjacentBot
|
||||||
moderation={moderation}
|
? 4
|
||||||
authorHasWarning={!!post.author.labels?.length}
|
: 8,
|
||||||
timestamp={post.indexedAt}
|
},
|
||||||
postHref={postHref}
|
]}>
|
||||||
showAvatar={isThreadedChild}
|
{/* If we are in threaded mode, the avatar is rendered in PostMeta */}
|
||||||
avatarModeration={moderation.ui('avatar')}
|
{!isThreadedChild && (
|
||||||
avatarSize={28}
|
<View style={styles.layoutAvi}>
|
||||||
displayNameType="md-bold"
|
<PreviewableUserAvatar
|
||||||
displayNameStyle={isThreadedChild && s.ml2}
|
size={38}
|
||||||
style={
|
profile={post.author}
|
||||||
isThreadedChild && {
|
moderation={moderation.ui('avatar')}
|
||||||
alignItems: 'center',
|
type={post.author.associated?.labeler ? 'labeler' : 'user'}
|
||||||
paddingBottom: isWeb ? 5 : 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<LabelsOnMyPost post={post} />
|
|
||||||
<PostAlerts
|
|
||||||
modui={moderation.ui('contentList')}
|
|
||||||
style={[a.pt_2xs, a.pb_2xs]}
|
|
||||||
additionalCauses={additionalPostAlerts}
|
|
||||||
/>
|
|
||||||
{richText?.text ? (
|
|
||||||
<View style={styles.postTextContainer}>
|
|
||||||
<RichText
|
|
||||||
enableTags
|
|
||||||
value={richText}
|
|
||||||
style={[a.flex_1, a.text_md]}
|
|
||||||
numberOfLines={limitLines ? MAX_POST_LINES : undefined}
|
|
||||||
authorHandle={post.author.handle}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
{limitLines ? (
|
|
||||||
<TextLink
|
|
||||||
text={_(msg`Show More`)}
|
|
||||||
style={pal.link}
|
|
||||||
onPress={onPressShowMore}
|
|
||||||
href="#"
|
|
||||||
/>
|
|
||||||
) : undefined}
|
|
||||||
{post.embed && (
|
|
||||||
<View style={[a.pb_xs]}>
|
|
||||||
<PostEmbeds
|
|
||||||
embed={post.embed}
|
|
||||||
moderation={moderation}
|
|
||||||
viewContext={PostEmbedViewContext.Feed}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{showChildReplyLine && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.replyLine,
|
||||||
|
{
|
||||||
|
flexGrow: 1,
|
||||||
|
backgroundColor: pal.colors.replyLine,
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<PostCtrls
|
|
||||||
post={post}
|
<View
|
||||||
record={record}
|
style={
|
||||||
richText={richText}
|
isThreadedChild
|
||||||
onPressReply={onPressReply}
|
? styles.layoutContentThreaded
|
||||||
logContext="PostThreadItem"
|
: styles.layoutContent
|
||||||
threadgateRecord={threadgateRecord}
|
}>
|
||||||
/>
|
<PostMeta
|
||||||
|
author={post.author}
|
||||||
|
moderation={moderation}
|
||||||
|
authorHasWarning={!!post.author.labels?.length}
|
||||||
|
timestamp={post.indexedAt}
|
||||||
|
postHref={postHref}
|
||||||
|
showAvatar={isThreadedChild}
|
||||||
|
avatarModeration={moderation.ui('avatar')}
|
||||||
|
avatarSize={28}
|
||||||
|
displayNameType="md-bold"
|
||||||
|
displayNameStyle={isThreadedChild && s.ml2}
|
||||||
|
style={
|
||||||
|
isThreadedChild && {
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingBottom: isWeb ? 5 : 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<LabelsOnMyPost post={post} />
|
||||||
|
<PostAlerts
|
||||||
|
modui={moderation.ui('contentList')}
|
||||||
|
style={[a.pt_2xs, a.pb_2xs]}
|
||||||
|
additionalCauses={additionalPostAlerts}
|
||||||
|
/>
|
||||||
|
{richText?.text ? (
|
||||||
|
<View style={styles.postTextContainer}>
|
||||||
|
<RichText
|
||||||
|
enableTags
|
||||||
|
value={richText}
|
||||||
|
style={[a.flex_1, a.text_md]}
|
||||||
|
numberOfLines={limitLines ? MAX_POST_LINES : undefined}
|
||||||
|
authorHandle={post.author.handle}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : undefined}
|
||||||
|
{limitLines ? (
|
||||||
|
<TextLink
|
||||||
|
text={_(msg`Show More`)}
|
||||||
|
style={pal.link}
|
||||||
|
onPress={onPressShowMore}
|
||||||
|
href="#"
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
{post.embed && (
|
||||||
|
<View style={[a.pb_xs]}>
|
||||||
|
<PostEmbeds
|
||||||
|
embed={post.embed}
|
||||||
|
moderation={moderation}
|
||||||
|
viewContext={PostEmbedViewContext.Feed}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<PostCtrls
|
||||||
|
post={post}
|
||||||
|
record={record}
|
||||||
|
richText={richText}
|
||||||
|
onPressReply={onPressReply}
|
||||||
|
logContext="PostThreadItem"
|
||||||
|
threadgateRecord={threadgateRecord}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
{hasMore ? (
|
||||||
{hasMore ? (
|
<Link
|
||||||
<Link
|
style={[
|
||||||
style={[
|
styles.loadMore,
|
||||||
styles.loadMore,
|
{
|
||||||
{
|
paddingLeft: treeView ? 8 : 70,
|
||||||
paddingLeft: treeView ? 8 : 70,
|
paddingTop: 0,
|
||||||
paddingTop: 0,
|
paddingBottom: treeView ? 4 : 12,
|
||||||
paddingBottom: treeView ? 4 : 12,
|
},
|
||||||
},
|
]}
|
||||||
]}
|
href={postHref}
|
||||||
href={postHref}
|
title={itemTitle}
|
||||||
title={itemTitle}
|
noFeedback>
|
||||||
noFeedback>
|
<Text type="sm-medium" style={pal.textLight}>
|
||||||
<Text type="sm-medium" style={pal.textLight}>
|
<Trans>More</Trans>
|
||||||
<Trans>More</Trans>
|
</Text>
|
||||||
</Text>
|
<FontAwesomeIcon
|
||||||
<FontAwesomeIcon
|
icon="angle-right"
|
||||||
icon="angle-right"
|
color={pal.colors.textLight}
|
||||||
color={pal.colors.textLight}
|
size={14}
|
||||||
size={14}
|
/>
|
||||||
/>
|
</Link>
|
||||||
</Link>
|
) : undefined}
|
||||||
) : undefined}
|
</PostHider>
|
||||||
</PostHider>
|
</PostOuterWrapper>
|
||||||
</PostOuterWrapper>
|
</GestureActionView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user