feat: implement pull-up navigation on Android (#4228)
* feat: implement pull-up navigation on Android * refactor: update folder structure * feat: integrate scroll view progress into pull-up navigation * refactor: extract PullUpIndicatorIos component * fix: compatible custom text size * chore: clean code
This commit is contained in:
parent
ad7c5ddb9b
commit
ab7efc34a7
|
|
@ -1,7 +1,8 @@
|
|||
import type { PrimitiveAtom } from "jotai"
|
||||
import type { ReactNode } from "react"
|
||||
import { createContext } from "react"
|
||||
import { createContext, use } from "react"
|
||||
import type { SharedValue } from "react-native-reanimated"
|
||||
import { useDerivedValue } from "react-native-reanimated"
|
||||
|
||||
export interface ScreenItemContextType {
|
||||
screenId: string
|
||||
|
|
@ -20,3 +21,31 @@ export interface ScreenItemContextType {
|
|||
}>
|
||||
}
|
||||
export const ScreenItemContext = createContext<ScreenItemContextType>(null!)
|
||||
|
||||
export const useScrollViewProgress = () => {
|
||||
const { reAnimatedScrollY, scrollViewHeight, scrollViewContentHeight } = use(ScreenItemContext)!
|
||||
|
||||
// Use useDerivedValue to create a reactive SharedValue that updates
|
||||
// whenever any of the input SharedValues change
|
||||
const progress = useDerivedValue(() => {
|
||||
// Calculate how far we've scrolled as a proportion of scrollable content
|
||||
// Scrollable content = total content height - visible height
|
||||
// Progress is clamped between 0 and 1
|
||||
const MAGIC_SHIFT = 95 // Adjust this value based on your layout needs
|
||||
const scrollableHeight = Math.max(
|
||||
0,
|
||||
scrollViewContentHeight.value - scrollViewHeight.value - MAGIC_SHIFT,
|
||||
)
|
||||
|
||||
if (scrollableHeight <= 0) {
|
||||
// If there's no scrollable content, we're at 100% progress
|
||||
return 1
|
||||
}
|
||||
|
||||
// Calculate progress as a value between 0 and 1
|
||||
const progress = Math.min(1, Math.max(0, reAnimatedScrollY.value / scrollableHeight))
|
||||
return progress
|
||||
}, [reAnimatedScrollY, scrollViewHeight, scrollViewContentHeight])
|
||||
|
||||
return progress
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import { cn } from "@follow/utils"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { View } from "react-native"
|
||||
import Animated, { useAnimatedStyle } from "react-native-reanimated"
|
||||
import { useColor } from "react-native-uikit-colors"
|
||||
|
||||
import { Text } from "@/src/components/ui/typography/Text"
|
||||
import { ArrowLeftCuteReIcon } from "@/src/icons/arrow_left_cute_re"
|
||||
|
||||
import type { UsePullUpToNextReturn } from "./types"
|
||||
|
||||
/**
|
||||
* Component that handles pulling up to navigate to the next unread entry for Android
|
||||
*/
|
||||
export const PullUpIndicatorAndroid: UsePullUpToNextReturn["EntryPullUpToNext"] = ({
|
||||
active,
|
||||
hide = false,
|
||||
translateY,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const textColor = useColor("secondaryLabel")
|
||||
const iconColor = useColor("label")
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ translateY: -translateY.value }],
|
||||
// paddingBottom: insets.bottom,
|
||||
opacity: hide ? 0 : 1,
|
||||
}))
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
className="bottom-0 flex w-full flex-row items-center justify-center gap-2 pt-16"
|
||||
style={animatedStyle}
|
||||
>
|
||||
<View
|
||||
className={cn(
|
||||
"flex flex-row items-center gap-2 transition-all duration-200",
|
||||
active ? "opacity-50" : "opacity-80",
|
||||
)}
|
||||
>
|
||||
<View
|
||||
className={cn(
|
||||
"rotate-90 transition-all duration-200",
|
||||
active ? "opacity-0" : "opacity-100",
|
||||
)}
|
||||
>
|
||||
<ArrowLeftCuteReIcon width={16} height={16} color={iconColor} />
|
||||
</View>
|
||||
<Text style={{ color: textColor }}>
|
||||
{active ? t("entry.release_to_next_entry") : t("entry.pull_up_to_next_entry")}
|
||||
</Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { cn } from "@follow/utils"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { View } from "react-native"
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context"
|
||||
import { useColor } from "react-native-uikit-colors"
|
||||
|
||||
import { Text } from "@/src/components/ui/typography/Text"
|
||||
import { ArrowLeftCuteReIcon } from "@/src/icons/arrow_left_cute_re"
|
||||
|
||||
import type { UsePullUpToNextReturn } from "./types"
|
||||
|
||||
/**
|
||||
* Component that handles pulling up to navigate to the next unread entry
|
||||
*/
|
||||
export const PullUpIndicatorIos: UsePullUpToNextReturn["EntryPullUpToNext"] = ({
|
||||
active,
|
||||
hide = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const insets = useSafeAreaInsets()
|
||||
const textColor = useColor("secondaryLabel")
|
||||
const iconColor = useColor("label")
|
||||
|
||||
return (
|
||||
<View
|
||||
className={cn(
|
||||
"absolute bottom-0 flex w-full translate-y-full flex-row items-center justify-center gap-2 pt-4 transition-all duration-200",
|
||||
hide ? "opacity-0" : "opacity-100",
|
||||
)}
|
||||
style={{ paddingBottom: insets.bottom + 20 }}
|
||||
>
|
||||
<View
|
||||
className={cn(
|
||||
"flex flex-row items-center gap-2 transition-all duration-200",
|
||||
active ? "opacity-50" : "opacity-80",
|
||||
)}
|
||||
>
|
||||
<View
|
||||
className={cn(
|
||||
"rotate-90 transition-all duration-200",
|
||||
active ? "opacity-0" : "opacity-100",
|
||||
)}
|
||||
>
|
||||
<ArrowLeftCuteReIcon width={16} height={16} color={iconColor} />
|
||||
</View>
|
||||
<Text style={{ color: textColor }}>
|
||||
{active ? t("entry.release_to_next_entry") : t("entry.pull_up_to_next_entry")}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import type { FC, PropsWithChildren } from "react"
|
||||
import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native"
|
||||
import type { ComposedGesture, GestureType } from "react-native-gesture-handler"
|
||||
import type { SharedValue } from "react-native-reanimated"
|
||||
import type { ReanimatedScrollEvent } from "react-native-reanimated/lib/typescript/hook/commonTypes"
|
||||
|
||||
interface EntryPullUpToNextProps {
|
||||
active: boolean
|
||||
hide?: boolean
|
||||
translateY: SharedValue<number>
|
||||
}
|
||||
|
||||
export interface UsePullUpToNextProps {
|
||||
enabled?: boolean
|
||||
onRefresh?: (() => void) | undefined
|
||||
progressViewOffset?: number
|
||||
}
|
||||
|
||||
interface GestureWrapperProps {
|
||||
enabled?: boolean
|
||||
gesture?: ComposedGesture | GestureType
|
||||
}
|
||||
|
||||
export interface UsePullUpToNextReturn {
|
||||
pullUpViewProps: EntryPullUpToNextProps
|
||||
scrollViewEventHandlers: {
|
||||
onScroll?: (e: ReanimatedScrollEvent) => void
|
||||
onScrollBeginDrag?: (e: NativeSyntheticEvent<NativeScrollEvent>) => void
|
||||
onScrollEndDrag?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
|
||||
}
|
||||
EntryPullUpToNext: FC<EntryPullUpToNextProps>
|
||||
gestureWrapperProps: GestureWrapperProps
|
||||
GestureWrapper: FC<PropsWithChildren<GestureWrapperProps>>
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
import * as Haptics from "expo-haptics"
|
||||
import { useCallback, useRef, useState } from "react"
|
||||
import { View } from "react-native"
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler"
|
||||
import { runOnJS, useSharedValue, withSpring } from "react-native-reanimated"
|
||||
import type { ReanimatedScrollEvent } from "react-native-reanimated/lib/typescript/hook/commonTypes"
|
||||
|
||||
import { useScrollViewProgress } from "@/src/lib/navigation/ScreenItemContext"
|
||||
|
||||
import { PullUpIndicatorAndroid } from "./PullUpIndicatorAndroid"
|
||||
import type { UsePullUpToNextProps, UsePullUpToNextReturn } from "./types"
|
||||
|
||||
const THRESHOLD = 70 // The threshold in pixels to trigger the next entry
|
||||
const FEEDBACK_THRESHOLD = 0.5 // When to give haptic feedback (50% of the way to the threshold)
|
||||
|
||||
const GestureWrapper: UsePullUpToNextReturn["GestureWrapper"] = ({
|
||||
gesture,
|
||||
enabled,
|
||||
children,
|
||||
}) => {
|
||||
if (!enabled || !gesture) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
return (
|
||||
<GestureDetector gesture={gesture}>
|
||||
<View className="flex flex-1">{children}</View>
|
||||
</GestureDetector>
|
||||
)
|
||||
}
|
||||
|
||||
export const usePullUpToNext = ({
|
||||
enabled = true,
|
||||
onRefresh,
|
||||
}: UsePullUpToNextProps): UsePullUpToNextReturn => {
|
||||
const scrollViewProgress = useScrollViewProgress()
|
||||
const isAtEnd = useSharedValue(false)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [dragState, setDragState] = useState(false)
|
||||
const feedbackGiven = useRef(false)
|
||||
const translateY = useSharedValue(0)
|
||||
|
||||
const initialTouchLocation = useSharedValue<{ x: number; y: number } | null>(null)
|
||||
const panGesture = Gesture.Pan()
|
||||
.enabled(enabled)
|
||||
.manualActivation(true)
|
||||
.maxPointers(1)
|
||||
.onBegin((event) => {
|
||||
initialTouchLocation.value = { x: event.x, y: event.y }
|
||||
})
|
||||
.onTouchesMove((evt, state) => {
|
||||
const isShortContent = scrollViewProgress.value === 1
|
||||
// Make sure we only process gestures when at end of content
|
||||
if (!isAtEnd.value && !isShortContent) {
|
||||
state.fail()
|
||||
return
|
||||
}
|
||||
const changedTouch = evt.changedTouches.at(0)
|
||||
if (!initialTouchLocation.value || !changedTouch) {
|
||||
state.fail()
|
||||
return
|
||||
}
|
||||
|
||||
const yDiff = changedTouch.y - initialTouchLocation.value.y
|
||||
const isPullUpPanning = yDiff < -1
|
||||
|
||||
if (isPullUpPanning) {
|
||||
runOnJS(setDragState)(true)
|
||||
state.activate()
|
||||
} else {
|
||||
state.fail()
|
||||
}
|
||||
})
|
||||
.onUpdate((event) => {
|
||||
// Only process upward gestures when at the end of the content
|
||||
if (event.translationY >= 0) {
|
||||
return
|
||||
}
|
||||
// Apply a damping effect to make the pull feel more natural
|
||||
const pullDistance = Math.min(Math.abs(event.translationY) * 0.7, THRESHOLD * 1.5) / 2
|
||||
translateY.value = pullDistance
|
||||
|
||||
// Ratio used to determine when to deactivate the pulling threshold
|
||||
const thresholdRatio = 0.95
|
||||
// Provide haptic feedback when crossing the threshold
|
||||
if (pullDistance > THRESHOLD * FEEDBACK_THRESHOLD && !feedbackGiven.current) {
|
||||
runOnJS(Haptics.impactAsync)(Haptics.ImpactFeedbackStyle.Heavy)
|
||||
feedbackGiven.current = true
|
||||
runOnJS(setRefreshing)(true)
|
||||
} else if (
|
||||
pullDistance < THRESHOLD * FEEDBACK_THRESHOLD * thresholdRatio &&
|
||||
feedbackGiven.current
|
||||
) {
|
||||
feedbackGiven.current = false
|
||||
runOnJS(Haptics.impactAsync)(Haptics.ImpactFeedbackStyle.Soft)
|
||||
runOnJS(setRefreshing)(false)
|
||||
}
|
||||
})
|
||||
.onEnd(() => {
|
||||
feedbackGiven.current = false
|
||||
runOnJS(setDragState)(false)
|
||||
|
||||
if (refreshing) {
|
||||
if (onRefresh) {
|
||||
runOnJS(onRefresh)()
|
||||
}
|
||||
} else {
|
||||
// Not enough pull or not at the end, reset with a nice spring animation
|
||||
translateY.value = withSpring(0, {
|
||||
damping: 16,
|
||||
mass: 1,
|
||||
stiffness: 200,
|
||||
})
|
||||
|
||||
if (refreshing) {
|
||||
runOnJS(setRefreshing)(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Track whether the scroll view is at the end
|
||||
const lastScrollY = useRef(0)
|
||||
const handleScroll = useCallback(
|
||||
(event: ReanimatedScrollEvent) => {
|
||||
const { contentOffset, contentSize, layoutMeasurement } = event
|
||||
if (Math.abs(contentOffset.y - lastScrollY.current) < 5) {
|
||||
return
|
||||
}
|
||||
lastScrollY.current = contentOffset.y
|
||||
// Check if we're near the bottom of the scroll view with a slightly larger buffer
|
||||
const isEnd =
|
||||
contentOffset.y >= contentSize.height - layoutMeasurement.height - 20 &&
|
||||
contentSize.height > layoutMeasurement.height
|
||||
if (isEnd !== isAtEnd.value) {
|
||||
isAtEnd.value = isEnd
|
||||
}
|
||||
},
|
||||
[isAtEnd],
|
||||
)
|
||||
|
||||
if (!enabled) {
|
||||
// Return empty implementation for non-Android platforms
|
||||
return {
|
||||
scrollViewEventHandlers: {},
|
||||
pullUpViewProps: {
|
||||
active: false,
|
||||
hide: true,
|
||||
translateY,
|
||||
} satisfies UsePullUpToNextReturn["pullUpViewProps"],
|
||||
EntryPullUpToNext: () => null,
|
||||
GestureWrapper,
|
||||
gestureWrapperProps: {
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
scrollViewEventHandlers: {
|
||||
onScroll: handleScroll,
|
||||
},
|
||||
pullUpViewProps: {
|
||||
active: refreshing,
|
||||
hide: !dragState,
|
||||
translateY,
|
||||
},
|
||||
|
||||
EntryPullUpToNext: PullUpIndicatorAndroid,
|
||||
GestureWrapper,
|
||||
gestureWrapperProps: {
|
||||
gesture: panGesture,
|
||||
enabled,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +1,24 @@
|
|||
import { cn } from "@follow/utils"
|
||||
import * as Haptics from "expo-haptics"
|
||||
import { useCallback, useRef, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native"
|
||||
import { View } from "react-native"
|
||||
import { useSharedValue } from "react-native-reanimated"
|
||||
import type { ReanimatedScrollEvent } from "react-native-reanimated/lib/typescript/hook/commonTypes"
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context"
|
||||
import { useColor } from "react-native-uikit-colors"
|
||||
|
||||
import { Text } from "@/src/components/ui/typography/Text"
|
||||
import { ArrowLeftCuteReIcon } from "@/src/icons/arrow_left_cute_re"
|
||||
import { PullUpIndicatorIos } from "./PullUpIndicatorIos"
|
||||
import type { UsePullUpToNextProps, UsePullUpToNextReturn } from "./types"
|
||||
|
||||
interface EntryPullUpToNextProps {
|
||||
active: boolean
|
||||
hide?: boolean
|
||||
}
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
const EmptyGestureWrapper: UsePullUpToNextReturn["GestureWrapper"] = ({
|
||||
children,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
}) => children
|
||||
|
||||
/**
|
||||
* Component that handles pulling up to navigate to the next unread entry
|
||||
*/
|
||||
const EntryPullUpToNext = ({ active, hide = false }: EntryPullUpToNextProps) => {
|
||||
const { t } = useTranslation()
|
||||
const insets = useSafeAreaInsets()
|
||||
const textColor = useColor("secondaryLabel")
|
||||
const iconColor = useColor("label")
|
||||
return (
|
||||
<View
|
||||
className={cn(
|
||||
"absolute bottom-0 flex w-full translate-y-full flex-row items-center justify-center gap-2 pt-4 transition-all duration-200",
|
||||
hide ? "opacity-0" : "opacity-100",
|
||||
)}
|
||||
style={{
|
||||
paddingBottom: insets.bottom + 20,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
className={cn(
|
||||
"flex flex-row items-center gap-2 transition-all duration-200",
|
||||
active ? "opacity-50" : "opacity-80",
|
||||
)}
|
||||
>
|
||||
<View
|
||||
className={cn(
|
||||
"rotate-90 transition-all duration-200",
|
||||
active ? "opacity-0" : "opacity-100",
|
||||
)}
|
||||
>
|
||||
<ArrowLeftCuteReIcon width={16} height={16} color={iconColor} />
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
>
|
||||
{active ? t("entry.release_to_next_entry") : t("entry.pull_up_to_next_entry")}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
export const usePullUpToNext = ({
|
||||
enabled = true,
|
||||
onRefresh,
|
||||
progressViewOffset = 70,
|
||||
}: {
|
||||
enabled?: boolean
|
||||
onRefresh?: (() => void) | undefined
|
||||
progressViewOffset?: number
|
||||
} = {}) => {
|
||||
}: UsePullUpToNextProps = {}): UsePullUpToNextReturn => {
|
||||
const dragging = useRef(false)
|
||||
const isOverThreshold = useRef(false)
|
||||
const [dragState, setDragState] = useState(false)
|
||||
|
|
@ -124,14 +75,20 @@ export const usePullUpToNext = ({
|
|||
},
|
||||
[dragging, onRefresh],
|
||||
)
|
||||
const translateY = useSharedValue(0)
|
||||
if (!enabled) {
|
||||
return {
|
||||
scrollViewEventHandlers: {},
|
||||
pullUpViewProps: {
|
||||
active: false,
|
||||
hide: dragState,
|
||||
} satisfies EntryPullUpToNextProps,
|
||||
translateY,
|
||||
} satisfies UsePullUpToNextReturn["pullUpViewProps"],
|
||||
EntryPullUpToNext: () => null,
|
||||
GestureWrapper: EmptyGestureWrapper,
|
||||
gestureWrapperProps: {
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
|
@ -143,7 +100,12 @@ export const usePullUpToNext = ({
|
|||
pullUpViewProps: {
|
||||
active: refreshing,
|
||||
hide: !dragState,
|
||||
} satisfies EntryPullUpToNextProps,
|
||||
EntryPullUpToNext,
|
||||
translateY,
|
||||
} satisfies UsePullUpToNextReturn["pullUpViewProps"],
|
||||
EntryPullUpToNext: PullUpIndicatorIos,
|
||||
GestureWrapper: EmptyGestureWrapper,
|
||||
gestureWrapperProps: {
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ import { checkLanguage } from "@/src/lib/translation"
|
|||
import { EntryContentContext, useEntryContentContext } from "@/src/modules/entry-content/ctx"
|
||||
import { EntryAISummary } from "@/src/modules/entry-content/EntryAISummary"
|
||||
import { EntryNavigationHeader } from "@/src/modules/entry-content/EntryNavigationHeader"
|
||||
import { usePullUpToNext } from "@/src/modules/entry-content/use-pull-up-to-next"
|
||||
import { usePullUpToNext } from "@/src/modules/entry-content/pull-up-navigation/use-pull-up-navigation"
|
||||
|
||||
import { EntrySocialTitle, EntryTitle } from "../../../../modules/entry-content/EntryTitle"
|
||||
|
||||
|
|
@ -62,7 +62,13 @@ export const EntryDetailScreen: NavigationControllerView<{
|
|||
const currentEntryIdx = entryIds.indexOf(entryId)
|
||||
return entryIds[currentEntryIdx + 1]
|
||||
}, [entryId, entryIds])
|
||||
const { EntryPullUpToNext, scrollViewEventHandlers, pullUpViewProps } = usePullUpToNext({
|
||||
const {
|
||||
EntryPullUpToNext,
|
||||
scrollViewEventHandlers,
|
||||
pullUpViewProps,
|
||||
GestureWrapper,
|
||||
gestureWrapperProps,
|
||||
} = usePullUpToNext({
|
||||
enabled: !!nextEntryId,
|
||||
onRefresh: useCallback(() => {
|
||||
if (!nextEntryId) return
|
||||
|
|
@ -85,39 +91,41 @@ export const EntryDetailScreen: NavigationControllerView<{
|
|||
<EntryContentContext value={ctxValue}>
|
||||
<PortalProvider>
|
||||
<BottomTabBarHeightContext value={insets.bottom}>
|
||||
<SafeNavigationScrollView
|
||||
Header={<EntryNavigationHeader entryId={entryId} />}
|
||||
ScrollViewBottom={<EntryPullUpToNext {...pullUpViewProps} />}
|
||||
automaticallyAdjustContentInsets={false}
|
||||
contentContainerClassName="flex min-h-full pb-16"
|
||||
{...scrollViewEventHandlers}
|
||||
>
|
||||
<ItemPressable
|
||||
itemStyle={ItemPressableStyle.UnStyled}
|
||||
onPress={() => entry?.url && openLink(entry.url)}
|
||||
className="rounded-xl py-4"
|
||||
<GestureWrapper {...gestureWrapperProps}>
|
||||
<SafeNavigationScrollView
|
||||
Header={<EntryNavigationHeader entryId={entryId} />}
|
||||
ScrollViewBottom={<EntryPullUpToNext {...pullUpViewProps} />}
|
||||
automaticallyAdjustContentInsets={false}
|
||||
contentContainerClassName="flex min-h-full pb-16"
|
||||
{...scrollViewEventHandlers}
|
||||
>
|
||||
{viewType === FeedViewType.SocialMedia ? (
|
||||
<EntrySocialTitle entryId={entryId} />
|
||||
) : (
|
||||
<>
|
||||
<EntryTitle title={entry?.title || ""} entryId={entryId} />
|
||||
<EntryInfo entryId={entryId} />
|
||||
</>
|
||||
<ItemPressable
|
||||
itemStyle={ItemPressableStyle.UnStyled}
|
||||
onPress={() => entry?.url && openLink(entry.url)}
|
||||
className="rounded-xl py-4"
|
||||
>
|
||||
{viewType === FeedViewType.SocialMedia ? (
|
||||
<EntrySocialTitle entryId={entryId} />
|
||||
) : (
|
||||
<>
|
||||
<EntryTitle title={entry?.title || ""} entryId={entryId} />
|
||||
<EntryInfo entryId={entryId} />
|
||||
</>
|
||||
)}
|
||||
</ItemPressable>
|
||||
<EntryAISummary entryId={entryId} />
|
||||
{entry && (
|
||||
<View className="mt-3">
|
||||
<EntryContentWebViewWithContext entryId={entryId} />
|
||||
</View>
|
||||
)}
|
||||
</ItemPressable>
|
||||
<EntryAISummary entryId={entryId} />
|
||||
{entry && (
|
||||
<View className="mt-3">
|
||||
<EntryContentWebViewWithContext entryId={entryId} />
|
||||
</View>
|
||||
)}
|
||||
{viewType === FeedViewType.SocialMedia && (
|
||||
<View className="mt-2">
|
||||
<EntryInfoSocial entryId={entryId} />
|
||||
</View>
|
||||
)}
|
||||
</SafeNavigationScrollView>
|
||||
{viewType === FeedViewType.SocialMedia && (
|
||||
<View className="mt-2">
|
||||
<EntryInfoSocial entryId={entryId} />
|
||||
</View>
|
||||
)}
|
||||
</SafeNavigationScrollView>
|
||||
</GestureWrapper>
|
||||
</BottomTabBarHeightContext>
|
||||
</PortalProvider>
|
||||
</EntryContentContext>
|
||||
|
|
|
|||
Loading…
Reference in New Issue