diff --git a/apps/mobile/native/ios/SharedWebView/ModalWebViewController.swift b/apps/mobile/native/ios/SharedWebView/ModalWebViewController.swift index a26e125b9..5d290ff82 100644 --- a/apps/mobile/native/ios/SharedWebView/ModalWebViewController.swift +++ b/apps/mobile/native/ios/SharedWebView/ModalWebViewController.swift @@ -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() } } diff --git a/apps/mobile/src/modules/entry-list/EntryListContentGrid.tsx b/apps/mobile/src/modules/entry-list/EntryListContentGrid.tsx index 5c61cea93..07a638985 100644 --- a/apps/mobile/src/modules/entry-list/EntryListContentGrid.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListContentGrid.tsx @@ -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, "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 ( { - return + data={entryIds} + renderItem={useTypeScriptHappyCallback(({ item }: { item: string }) => { + return }, [])} 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 } diff --git a/apps/mobile/src/modules/entry-list/EntryListContext.tsx b/apps/mobile/src/modules/entry-list/EntryListContext.tsx new file mode 100644 index 000000000..10ecdca76 --- /dev/null +++ b/apps/mobile/src/modules/entry-list/EntryListContext.tsx @@ -0,0 +1,8 @@ +import type { FeedViewType } from "@follow/constants" +import { createContext, useContext } from "react" + +export const EntryListContextViewContext = createContext(null!) + +export const useEntryListContextView = () => { + return useContext(EntryListContextViewContext) +} diff --git a/apps/mobile/src/modules/entry-list/EntryListSelector.tsx b/apps/mobile/src/modules/entry-list/EntryListSelector.tsx index 20718c0da..7d6dc1060 100644 --- a/apps/mobile/src/modules/entry-list/EntryListSelector.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListSelector.tsx @@ -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 + return ( + + + + ) } diff --git a/apps/mobile/src/modules/entry-list/index.tsx b/apps/mobile/src/modules/entry-list/index.tsx index cb5823dda..b61ba3b63 100644 --- a/apps/mobile/src/modules/entry-list/index.tsx +++ b/apps/mobile/src/modules/entry-list/index.tsx @@ -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(() => { diff --git a/apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx index 567b68e2a..1c6666562 100644 --- a/apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx +++ b/apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx @@ -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 ? ( - - { - if (item) { - setWebViewEntry(item) - } - }} - imageUrl={imageUrl} - blurhash={blurhash} - aspectRatio={aspectRatio} - Accessory={EntryGridItemAccessory} - AccessoryProps={{ - id, - }} - /> - - ) : ( - - No media available - - ) - } - case "video": { - const { videoPreviewImageUrl } = props + if (pictureViewFilterNoImage && !hasMedia && view === FeedViewType.Pictures) { + return null + } + if (!hasMedia) { + return ( + + No media available + + ) + } - return ( - <> - {videoPreviewImageUrl ? ( - - - - ) : ( - - No media available + const WrapperComponent = view === FeedViewType.Videos ? ItemPressable : View + + return ( + { + if (!item.url) { + return + } + if (view === FeedViewType.Videos) { + openLink(item.url) + } + }} + > + { + if (item) { + setWebViewEntry(item) + } + }} + /> + + ) +} + +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 ( + + + {mediaUrl && ( + + )} + + + {title} + + + ) + } + + return ( + { + setContainerWidth(nativeEvent.layout.width) + }} + > + + {uniqMedia.map((m, index) => { + if (m.type === "photo") { + return ( + + + + - )} - - {item?.title} - - - ) - } - } - }, [type, JSON.stringify(props), item?.title]) - if (!item) { - return null - } + ) + } - if ( - pictureViewFilterNoImage && - type === "image" && - !props.imageUrl && - view === FeedViewType.Pictures - ) { - return null - } - - return {Content} + return ( + { + // open player + }} + imageUrl={m.url} + aspectRatio={m.width && m.height ? m.width / m.height : 1} + /> + ) + })} + + + ) } const EntryGridItemAccessory = ({ id }: { id: string }) => {