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:
parent
c4c9ceb385
commit
ae89d8d716
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue