refactor(mobile): simplify entry list rendering and context management

- Refactored EntryListContentGrid to use direct entry IDs instead of complex masonry item mapping
- Introduced EntryListContext to provide view type context
- Updated EntryGridItem to handle multiple media types and scrolling
- Simplified ViewPagerList to render a single view instead of multiple pages
- Removed unused pager view logic and simplified entry list rendering

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-02-21 21:48:02 +08:00
parent c4c9ceb385
commit ae89d8d716
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
6 changed files with 164 additions and 156 deletions

View File

@ -56,7 +56,8 @@ class ModalWebViewController: UIViewController {
private func setupWebView() {
view.addSubview(webView)
webView.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.top.equalTo(view.safeAreaLayoutGuide)
make.left.right.bottom.equalToSuperview()
}
}

View File

@ -1,15 +1,14 @@
import { useTypeScriptHappyCallback } from "@follow/hooks"
import type { MasonryFlashListProps } from "@shopify/flash-list"
import type { ElementRef } from "react"
import { forwardRef, useCallback } from "react"
import { forwardRef } from "react"
import { ActivityIndicator, View } from "react-native"
import { useFetchEntriesControls } from "@/src/modules/screen/atoms"
import { useEntryStore } from "@/src/store/entry/store"
import { TimelineSelectorMasonryList } from "../screen/TimelineSelectorList"
import { useOnViewableItemsChanged } from "./hooks"
import type { MasonryItem } from "./templates/EntryGridItem"
// import type { MasonryItem } from "./templates/EntryGridItem"
import { EntryGridItem } from "./templates/EntryGridItem"
export const EntryListContentGrid = forwardRef<
@ -19,60 +18,15 @@ export const EntryListContentGrid = forwardRef<
} & Omit<MasonryFlashListProps<string>, "data" | "renderItem">
>(({ entryIds, ...rest }, ref) => {
const { fetchNextPage, refetch, isRefetching, hasNextPage } = useFetchEntriesControls()
const onViewableItemsChanged = useOnViewableItemsChanged(
(item) => (item.key as any).split("-")[0],
)
const data = useEntryStore(
useCallback(
(state) => {
const data: (MasonryItem & { index: number })[] = []
let index = 0
for (const id of entryIds) {
const entry = state.data[id]
if (!entry) {
continue
}
if (!entry.media) {
continue
}
for (const media of entry.media) {
if (media.type === "photo") {
data.push({
id,
index: index++,
type: "image",
imageUrl: media.url,
blurhash: media.blurhash,
width: media.width,
height: media.height,
})
} else if (media.type === "video") {
data.push({
id,
index: index++,
type: "video",
videoUrl: media.url,
videoPreviewImageUrl: media.preview_image_url,
})
}
}
}
return data
},
[entryIds],
),
)
const onViewableItemsChanged = useOnViewableItemsChanged()
return (
<TimelineSelectorMasonryList
ref={ref}
isRefetching={isRefetching}
data={data}
renderItem={useTypeScriptHappyCallback(({ item }: { item: MasonryItem }) => {
return <EntryGridItem {...item} />
data={entryIds}
renderItem={useTypeScriptHappyCallback(({ item }: { item: string }) => {
return <EntryGridItem id={item} />
}, [])}
keyExtractor={defaultKeyExtractor}
onViewableItemsChanged={onViewableItemsChanged}
@ -94,7 +48,6 @@ export const EntryListContentGrid = forwardRef<
)
})
const defaultKeyExtractor = (item: MasonryItem & { index: number }) => {
const key = `${item.id}-${item.index}`
return key
const defaultKeyExtractor = (item: string) => {
return item
}

View File

@ -0,0 +1,8 @@
import type { FeedViewType } from "@follow/constants"
import { createContext, useContext } from "react"
export const EntryListContextViewContext = createContext<FeedViewType>(null!)
export const useEntryListContextView = () => {
return useContext(EntryListContextViewContext)
}

View File

@ -9,6 +9,7 @@ import { EntryListContentGrid } from "@/src/modules/entry-list/EntryListContentG
import { EntryListContentArticle } from "./EntryListContentArticle"
import { EntryListContentSocial } from "./EntryListContentSocial"
import { EntryListContextViewContext } from "./EntryListContext"
export function EntryListSelector({
entryIds,
@ -47,5 +48,9 @@ export function EntryListSelector({
}
}
return <ContentComponent ref={ref} entryIds={entryIds} />
return (
<EntryListContextViewContext.Provider value={viewId}>
<ContentComponent ref={ref} entryIds={entryIds} />
</EntryListContextViewContext.Provider>
)
}

View File

@ -1,16 +1,10 @@
import { FeedViewType } from "@follow/constants"
import { useIsFocused } from "@react-navigation/native"
import { useEffect, useMemo } from "react"
import { Animated, StyleSheet } from "react-native"
import PagerView from "react-native-pager-view"
import { views } from "@/src/constants/views"
import {
selectTimeline,
useSelectedFeed,
useSelectedView,
useSetDrawerSwipeDisabled,
} from "@/src/modules/screen/atoms"
import { selectTimeline, useSelectedFeed, useSelectedView } from "@/src/modules/screen/atoms"
import {
useEntryIdsByCategory,
useEntryIdsByFeedId,
@ -24,16 +18,6 @@ import { EntryListSelector } from "./EntryListSelector"
import { usePagerView } from "./usePagerView"
export function EntryList() {
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
const isFocused = useIsFocused()
useEffect(() => {
if (isFocused) {
setDrawerSwipeDisabled(false)
} else {
setDrawerSwipeDisabled(true)
}
}, [setDrawerSwipeDisabled, isFocused])
const selectedFeed = useSelectedFeed()
const Content = useMemo(() => {

View File

@ -1,6 +1,7 @@
import { FeedViewType } from "@follow/constants"
import { uniqBy } from "es-toolkit/compat"
import { LinearGradient } from "expo-linear-gradient"
import { useEffect, useMemo } from "react"
import { useEffect, useMemo, useState } from "react"
import { ScrollView, Text, View } from "react-native"
import Animated, { useSharedValue, withTiming } from "react-native-reanimated"
import { useSafeAreaInsets } from "react-native-safe-area-context"
@ -15,99 +16,155 @@ import { FeedIcon } from "@/src/components/ui/icon/feed-icon"
import { ImageContextMenu } from "@/src/components/ui/image/ImageContextMenu"
import { PreviewImage } from "@/src/components/ui/image/PreviewImage"
import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
import type { MediaModel } from "@/src/database/schemas/types"
import { openLink } from "@/src/lib/native"
import { useEntry } from "@/src/store/entry/hooks"
import { useFeed } from "@/src/store/feed/hooks"
import { useSelectedView } from "../../screen/atoms"
import { useEntryListContextView } from "../EntryListContext"
export type MasonryItem = {
id: string
} & (
| {
type: "image"
imageUrl: string
blurhash?: string
height?: number
width?: number
}
| {
type: "video"
videoUrl: string
videoPreviewImageUrl?: string
}
)
export function EntryGridItem(props: MasonryItem) {
const { type, id } = props
const view = useSelectedView()
export function EntryGridItem({ id }: { id: string }) {
const item = useEntry(id)
const view = useEntryListContextView()
const pictureViewFilterNoImage = useUISettingKey("pictureViewFilterNoImage")
if (!item || !item.media) {
return null
}
const Content = useMemo(() => {
switch (type) {
case "image": {
const { imageUrl, blurhash, height, width } = props
const aspectRatio = height && width ? width / height : 16 / 9
const hasMedia = item.media.length > 0
return imageUrl ? (
<ImageContextMenu imageUrl={imageUrl}>
<PreviewImage
onPreview={() => {
if (item) {
setWebViewEntry(item)
}
}}
imageUrl={imageUrl}
blurhash={blurhash}
aspectRatio={aspectRatio}
Accessory={EntryGridItemAccessory}
AccessoryProps={{
id,
}}
/>
</ImageContextMenu>
) : (
<View className="aspect-video w-full items-center justify-center">
<Text className="text-label text-center">No media available</Text>
</View>
)
}
case "video": {
const { videoPreviewImageUrl } = props
if (pictureViewFilterNoImage && !hasMedia && view === FeedViewType.Pictures) {
return null
}
if (!hasMedia) {
return (
<View className="aspect-video w-full items-center justify-center">
<Text className="text-label text-center">No media available</Text>
</View>
)
}
return (
<>
{videoPreviewImageUrl ? (
<ImageContextMenu imageUrl={videoPreviewImageUrl}>
<PreviewImage imageUrl={videoPreviewImageUrl} aspectRatio={16 / 9} />
</ImageContextMenu>
) : (
<View className="aspect-video w-full items-center justify-center">
<Text className="text-label text-center">No media available</Text>
const WrapperComponent = view === FeedViewType.Videos ? ItemPressable : View
return (
<WrapperComponent
className="m-1 overflow-hidden rounded-md"
onPress={() => {
if (!item.url) {
return
}
if (view === FeedViewType.Videos) {
openLink(item.url)
}
}}
>
<MediaItems
media={item.media}
title={item.title || ""}
view={view!}
entryId={id}
onPreview={() => {
if (item) {
setWebViewEntry(item)
}
}}
/>
</WrapperComponent>
)
}
const MediaItems = ({
media,
view,
entryId,
onPreview,
title,
}: {
media: MediaModel[]
view: FeedViewType
entryId: string
onPreview: () => void
title: string
}) => {
const firstMedia = media[0]
const [containerWidth, setContainerWidth] = useState(0)
const uniqMedia = useMemo(() => {
return uniqBy(media, "url")
}, [media])
if (!firstMedia) {
return null
}
const { height } = firstMedia
const { width } = firstMedia
const aspectRatio = width && height ? width / height : 1
if (view === FeedViewType.Videos) {
const mediaUrl = firstMedia.preview_image_url || firstMedia.url
const aspectRatio = 16 / 9
return (
<View>
<View className="flex-1" style={{ aspectRatio }}>
{mediaUrl && (
<PreviewImage imageUrl={mediaUrl} aspectRatio={aspectRatio} onPreview={onPreview} />
)}
</View>
<Text className="text-label p-2 font-medium" numberOfLines={2}>
{title}
</Text>
</View>
)
}
return (
<View
onLayout={({ nativeEvent }) => {
setContainerWidth(nativeEvent.layout.width)
}}
>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
pagingEnabled
className="flex-1"
contentContainerClassName="flex-row"
style={{ aspectRatio }}
>
{uniqMedia.map((m, index) => {
if (m.type === "photo") {
return (
<View key={index} className="relative" style={{ width: containerWidth }}>
<ImageContextMenu imageUrl={m.url}>
<PreviewImage
onPreview={onPreview}
imageUrl={m.url}
aspectRatio={m.width && m.height ? m.width / m.height : 1}
Accessory={EntryGridItemAccessory}
AccessoryProps={{
id: entryId,
}}
/>
</ImageContextMenu>
</View>
)}
<Text className="text-label p-2 font-medium" numberOfLines={2}>
{item?.title}
</Text>
</>
)
}
}
}, [type, JSON.stringify(props), item?.title])
if (!item) {
return null
}
)
}
if (
pictureViewFilterNoImage &&
type === "image" &&
!props.imageUrl &&
view === FeedViewType.Pictures
) {
return null
}
return <ItemPressable className="m-1 overflow-hidden rounded-md">{Content}</ItemPressable>
return (
<PreviewImage
key={index}
onPreview={() => {
// open player
}}
imageUrl={m.url}
aspectRatio={m.width && m.height ? m.width / m.height : 1}
/>
)
})}
</ScrollView>
</View>
)
}
const EntryGridItemAccessory = ({ id }: { id: string }) => {