Fix layout shift when liking a post on Android, make countwheel generic
This commit is contained in:
@@ -110,6 +110,7 @@ const Context = createContext<AnalyticsBaseContextType>({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
Context.displayName = 'AnalyticsContext'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures that deviceId is set and migrated from legacy storage. Handled on
|
* Ensures that deviceId is set and migrated from legacy storage. Handled on
|
||||||
|
|||||||
@@ -130,8 +130,11 @@ export function PostControlButtonText({style, ...props}: TextProps) {
|
|||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
color,
|
color,
|
||||||
|
a.user_select_none,
|
||||||
big ? a.text_md : a.text_sm,
|
big ? a.text_md : a.text_sm,
|
||||||
active && a.font_semi_bold,
|
active && a.font_semi_bold,
|
||||||
|
// prevent layout shift on android
|
||||||
|
{includeFontPadding: false, textAlignVertical: 'center'},
|
||||||
style,
|
style,
|
||||||
]}
|
]}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
ProgressGuideAction,
|
ProgressGuideAction,
|
||||||
useProgressGuideControls,
|
useProgressGuideControls,
|
||||||
} from '#/state/shell/progress-guide'
|
} from '#/state/shell/progress-guide'
|
||||||
import {atoms as a, useBreakpoints} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Reply as Bubble} from '#/components/icons/Reply'
|
import {Reply as Bubble} from '#/components/icons/Reply'
|
||||||
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
||||||
import * as Skele from '#/components/Skeleton'
|
import * as Skele from '#/components/Skeleton'
|
||||||
@@ -74,6 +74,7 @@ let PostControls = ({
|
|||||||
forceGoogleTranslate?: boolean
|
forceGoogleTranslate?: boolean
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const {openComposer} = useOpenComposer()
|
const {openComposer} = useOpenComposer()
|
||||||
const {feedDescriptor} = useFeedFeedbackContext()
|
const {feedDescriptor} = useFeedFeedbackContext()
|
||||||
@@ -270,6 +271,8 @@ let PostControls = ({
|
|||||||
<PostControlButton
|
<PostControlButton
|
||||||
testID="likeBtn"
|
testID="likeBtn"
|
||||||
big={big}
|
big={big}
|
||||||
|
active={Boolean(post.viewer?.like)}
|
||||||
|
activeColor={t.palette.pink}
|
||||||
onPress={() => requireAuth(() => onPressToggleLike())}
|
onPress={() => requireAuth(() => onPressToggleLike())}
|
||||||
label={
|
label={
|
||||||
post.viewer?.like
|
post.viewer?.like
|
||||||
@@ -296,10 +299,14 @@ let PostControls = ({
|
|||||||
hasBeenToggled={hasLikeIconBeenToggled}
|
hasBeenToggled={hasLikeIconBeenToggled}
|
||||||
/>
|
/>
|
||||||
<CountWheel
|
<CountWheel
|
||||||
likeCount={post.likeCount ?? 0}
|
count={post.likeCount ?? 0}
|
||||||
big={big}
|
isToggled={Boolean(post.viewer?.like)}
|
||||||
isLiked={Boolean(post.viewer?.like)}
|
|
||||||
hasBeenToggled={hasLikeIconBeenToggled}
|
hasBeenToggled={hasLikeIconBeenToggled}
|
||||||
|
renderCount={({count}) => (
|
||||||
|
<PostControlButtonText>
|
||||||
|
{formatPostStatCount(count)}
|
||||||
|
</PostControlButtonText>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</PostControlButton>
|
</PostControlButton>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ import Animated, {
|
|||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
|
|
||||||
import {decideShouldRoll} from '#/lib/custom-animations/util'
|
import {decideShouldRoll} from '#/lib/custom-animations/util'
|
||||||
import {s} from '#/lib/styles'
|
import {atoms as a} from '#/alf'
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
|
||||||
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
|
||||||
|
|
||||||
const animationConfig = {
|
const animationConfig = {
|
||||||
duration: 400,
|
duration: 400,
|
||||||
@@ -87,89 +84,66 @@ function ExitingDown() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CountWheel({
|
export function CountWheel({
|
||||||
likeCount,
|
count,
|
||||||
big,
|
isToggled,
|
||||||
isLiked,
|
|
||||||
hasBeenToggled,
|
hasBeenToggled,
|
||||||
|
renderCount,
|
||||||
}: {
|
}: {
|
||||||
likeCount: number
|
count: number
|
||||||
big?: boolean
|
isToggled: boolean
|
||||||
isLiked: boolean
|
|
||||||
hasBeenToggled: boolean
|
hasBeenToggled: boolean
|
||||||
|
renderCount: (props: {count: number}) => React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
|
||||||
const shouldAnimate = !useReducedMotion() && hasBeenToggled
|
const shouldAnimate = !useReducedMotion() && hasBeenToggled
|
||||||
const shouldRoll = decideShouldRoll(isLiked, likeCount)
|
const shouldRoll = decideShouldRoll(isToggled, count)
|
||||||
|
|
||||||
// Incrementing the key will cause the `Animated.View` to re-render, with the newly selected entering/exiting
|
// Incrementing the key will cause the `Animated.View` to re-render, with the newly selected entering/exiting
|
||||||
// animation
|
// animation
|
||||||
// The initial entering/exiting animations will get skipped, since these will happen on screen mounts and would
|
// The initial entering/exiting animations will get skipped, since these will happen on screen mounts and would
|
||||||
// be unnecessary
|
// be unnecessary
|
||||||
const [key, setKey] = useState(0)
|
const [key, setKey] = useState(0)
|
||||||
const [prevCount, setPrevCount] = useState(likeCount)
|
const [prevCount, setPrevCount] = useState(count)
|
||||||
const prevIsLiked = useRef(isLiked)
|
const prevIsToggled = useRef(isToggled)
|
||||||
const formatPostStatCount = useFormatPostStatCount()
|
|
||||||
const formattedCount = formatPostStatCount(likeCount)
|
|
||||||
const formattedPrevCount = formatPostStatCount(prevCount)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLiked === prevIsLiked.current) {
|
if (isToggled === prevIsToggled.current) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPrevCount = isLiked ? likeCount - 1 : likeCount + 1
|
const newPrevCount = isToggled ? count - 1 : count + 1
|
||||||
setKey(prev => prev + 1)
|
setKey(prev => prev + 1)
|
||||||
setPrevCount(newPrevCount)
|
setPrevCount(newPrevCount)
|
||||||
prevIsLiked.current = isLiked
|
prevIsToggled.current = isToggled
|
||||||
}, [isLiked, likeCount])
|
}, [isToggled, count])
|
||||||
|
|
||||||
const enteringAnimation =
|
const enteringAnimation =
|
||||||
shouldAnimate && shouldRoll
|
shouldAnimate && shouldRoll
|
||||||
? isLiked
|
? isToggled
|
||||||
? EnteringUp
|
? EnteringUp
|
||||||
: EnteringDown
|
: EnteringDown
|
||||||
: undefined
|
: undefined
|
||||||
const exitingAnimation =
|
const exitingAnimation =
|
||||||
shouldAnimate && shouldRoll
|
shouldAnimate && shouldRoll
|
||||||
? isLiked
|
? isToggled
|
||||||
? ExitingUp
|
? ExitingUp
|
||||||
: ExitingDown
|
: ExitingDown
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutAnimationConfig skipEntering skipExiting>
|
<LayoutAnimationConfig skipEntering skipExiting>
|
||||||
{likeCount > 0 ? (
|
{count > 0 ? (
|
||||||
<View style={[a.justify_center]}>
|
<View style={[a.justify_center]}>
|
||||||
<Animated.View entering={enteringAnimation} key={key}>
|
<Animated.View entering={enteringAnimation} key={key}>
|
||||||
<Text
|
{renderCount({count})}
|
||||||
testID="likeCount"
|
|
||||||
style={[
|
|
||||||
big ? a.text_md : a.text_sm,
|
|
||||||
a.user_select_none,
|
|
||||||
isLiked
|
|
||||||
? [a.font_semi_bold, s.likeColor]
|
|
||||||
: {color: t.palette.contrast_500},
|
|
||||||
]}>
|
|
||||||
{formattedCount}
|
|
||||||
</Text>
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
{shouldAnimate && (likeCount > 1 || !isLiked) ? (
|
{shouldAnimate && (count > 1 || !isToggled) ? (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={exitingAnimation}
|
entering={exitingAnimation}
|
||||||
// Add 2 to the key so there are never duplicates
|
// Add 2 to the key so there are never duplicates
|
||||||
key={key + 2}
|
key={key + 2}
|
||||||
style={[a.absolute, {width: 50, opacity: 0}]}
|
style={[a.absolute, {width: 50, opacity: 0}]}
|
||||||
aria-disabled={true}>
|
aria-disabled={true}>
|
||||||
<Text
|
{renderCount({count: prevCount})}
|
||||||
style={[
|
|
||||||
big ? a.text_md : a.text_sm,
|
|
||||||
a.user_select_none,
|
|
||||||
isLiked
|
|
||||||
? [a.font_semi_bold, s.likeColor]
|
|
||||||
: {color: t.palette.contrast_500},
|
|
||||||
]}>
|
|
||||||
{formattedPrevCount}
|
|
||||||
</Text>
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ import {View} from 'react-native'
|
|||||||
import {useReducedMotion} from 'react-native-reanimated'
|
import {useReducedMotion} from 'react-native-reanimated'
|
||||||
|
|
||||||
import {decideShouldRoll} from '#/lib/custom-animations/util'
|
import {decideShouldRoll} from '#/lib/custom-animations/util'
|
||||||
import {s} from '#/lib/styles'
|
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
|
||||||
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
|
||||||
|
|
||||||
const animationConfig = {
|
const animationConfig = {
|
||||||
duration: 400,
|
duration: 400,
|
||||||
@@ -35,50 +31,46 @@ const exitingDownKeyframe = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export function CountWheel({
|
export function CountWheel({
|
||||||
likeCount,
|
count,
|
||||||
big,
|
isToggled,
|
||||||
isLiked,
|
|
||||||
hasBeenToggled,
|
hasBeenToggled,
|
||||||
|
renderCount,
|
||||||
}: {
|
}: {
|
||||||
likeCount: number
|
count: number
|
||||||
big?: boolean
|
isToggled: boolean
|
||||||
isLiked: boolean
|
|
||||||
hasBeenToggled: boolean
|
hasBeenToggled: boolean
|
||||||
|
renderCount: (props: {count: number}) => React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
|
||||||
const shouldAnimate = !useReducedMotion() && hasBeenToggled
|
const shouldAnimate = !useReducedMotion() && hasBeenToggled
|
||||||
const shouldRoll = decideShouldRoll(isLiked, likeCount)
|
const shouldRoll = decideShouldRoll(isToggled, count)
|
||||||
|
|
||||||
const countView = useRef<HTMLDivElement>(null)
|
const countView = useRef<HTMLDivElement>(null)
|
||||||
const prevCountView = useRef<HTMLDivElement>(null)
|
const prevCountView = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const [prevCount, setPrevCount] = useState(likeCount)
|
const [prevCount, setPrevCount] = useState(count)
|
||||||
const prevIsLiked = useRef(isLiked)
|
const prevIsToggled = useRef(isToggled)
|
||||||
const formatPostStatCount = useFormatPostStatCount()
|
|
||||||
const formattedCount = formatPostStatCount(likeCount)
|
|
||||||
const formattedPrevCount = formatPostStatCount(prevCount)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLiked === prevIsLiked.current) {
|
if (isToggled === prevIsToggled.current) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPrevCount = isLiked ? likeCount - 1 : likeCount + 1
|
const newPrevCount = isToggled ? count - 1 : count + 1
|
||||||
if (shouldAnimate && shouldRoll) {
|
if (shouldAnimate && shouldRoll) {
|
||||||
countView.current?.animate?.(
|
countView.current?.animate?.(
|
||||||
isLiked ? enteringUpKeyframe : enteringDownKeyframe,
|
isToggled ? enteringUpKeyframe : enteringDownKeyframe,
|
||||||
animationConfig,
|
animationConfig,
|
||||||
)
|
)
|
||||||
prevCountView.current?.animate?.(
|
prevCountView.current?.animate?.(
|
||||||
isLiked ? exitingUpKeyframe : exitingDownKeyframe,
|
isToggled ? exitingUpKeyframe : exitingDownKeyframe,
|
||||||
animationConfig,
|
animationConfig,
|
||||||
)
|
)
|
||||||
setPrevCount(newPrevCount)
|
setPrevCount(newPrevCount)
|
||||||
}
|
}
|
||||||
prevIsLiked.current = isLiked
|
prevIsToggled.current = isToggled
|
||||||
}, [isLiked, likeCount, shouldAnimate, shouldRoll])
|
}, [isToggled, count, shouldAnimate, shouldRoll])
|
||||||
|
|
||||||
if (likeCount < 1) {
|
if (count < 1) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,34 +79,15 @@ export function CountWheel({
|
|||||||
<View
|
<View
|
||||||
// @ts-expect-error is div
|
// @ts-expect-error is div
|
||||||
ref={countView}>
|
ref={countView}>
|
||||||
<Text
|
{renderCount({count})}
|
||||||
testID="likeCount"
|
|
||||||
style={[
|
|
||||||
big ? a.text_md : a.text_sm,
|
|
||||||
a.user_select_none,
|
|
||||||
isLiked
|
|
||||||
? [a.font_semi_bold, s.likeColor]
|
|
||||||
: {color: t.palette.contrast_500},
|
|
||||||
]}>
|
|
||||||
{formattedCount}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
{shouldAnimate && (likeCount > 1 || !isLiked) ? (
|
{shouldAnimate && (count > 1 || !isToggled) ? (
|
||||||
<View
|
<View
|
||||||
style={{position: 'absolute', opacity: 0}}
|
style={{position: 'absolute', opacity: 0}}
|
||||||
aria-disabled={true}
|
aria-disabled={true}
|
||||||
// @ts-expect-error is div
|
// @ts-expect-error is div
|
||||||
ref={prevCountView}>
|
ref={prevCountView}>
|
||||||
<Text
|
{renderCount({count: prevCount})}
|
||||||
style={[
|
|
||||||
big ? a.text_md : a.text_sm,
|
|
||||||
a.user_select_none,
|
|
||||||
isLiked
|
|
||||||
? [a.font_semi_bold, s.likeColor]
|
|
||||||
: {color: t.palette.contrast_500},
|
|
||||||
]}>
|
|
||||||
{formattedPrevCount}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
Reference in New Issue
Block a user