diff --git a/apps/mobile/src/components/ui/carousel/MediaCarousel.tsx b/apps/mobile/src/components/ui/carousel/MediaCarousel.tsx index 95e52edef..fc4a19bf6 100644 --- a/apps/mobile/src/components/ui/carousel/MediaCarousel.tsx +++ b/apps/mobile/src/components/ui/carousel/MediaCarousel.tsx @@ -30,18 +30,21 @@ export const MediaCarousel = ({ noPreview?: boolean } & Pick) => { const [containerWidth, setContainerWidth] = useState(0) + const containerHeight = Math.floor(containerWidth / aspectRatio) const hasMany = media.length > 1 // const activeIndex = useSharedValue(0) const [activeIndex, setActiveIndex] = useState(0) + const Wrapper = noPreview ? View : ImageContextMenu + return ( { setContainerWidth(e.nativeEvent.layout.width) }} > - + { setActiveIndex(Math.round(e.nativeEvent.contentOffset.x / containerWidth)) @@ -52,26 +55,27 @@ export const MediaCarousel = ({ showsHorizontalScrollIndicator={false} pagingEnabled className="flex-1" - contentContainerClassName="flex-row overflow-hidden rounded-md" - style={{ aspectRatio }} + // We need to fixed the height of the container to prevent the carousel from resizing + // See https://github.com/Shopify/flash-list/issues/797 + style={{ height: containerHeight }} > {media.map((m, index) => { if (m.type === "photo") { return ( - + - + ) } @@ -84,7 +88,7 @@ export const MediaCarousel = ({ // open player }} imageUrl={m.url} - aspectRatio={m.width && m.height ? m.width / m.height : 1} + aspectRatio={aspectRatio} /> ) })} diff --git a/apps/mobile/src/modules/entry-content/EntryGridFooter.tsx b/apps/mobile/src/modules/entry-content/EntryGridFooter.tsx index eecf58c9b..245aa0bc9 100644 --- a/apps/mobile/src/modules/entry-content/EntryGridFooter.tsx +++ b/apps/mobile/src/modules/entry-content/EntryGridFooter.tsx @@ -54,7 +54,11 @@ export const EntryGridFooter = ({ {entry.description && ( {entry.description} diff --git a/apps/mobile/src/modules/entry-list/EntryListContentGrid.tsx b/apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx similarity index 90% rename from apps/mobile/src/modules/entry-list/EntryListContentGrid.tsx rename to apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx index b4d48c40e..5b3817f51 100644 --- a/apps/mobile/src/modules/entry-list/EntryListContentGrid.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx @@ -9,9 +9,9 @@ import { useFetchEntriesControls } from "@/src/modules/screen/atoms" import { TimelineSelectorMasonryList } from "../screen/TimelineSelectorList" import { useOnViewableItemsChanged } from "./hooks" // import type { MasonryItem } from "./templates/EntryGridItem" -import { EntryGridItem } from "./templates/EntryGridItem" +import { EntryPictureItem } from "./templates/EntryPictureItem" -export const EntryListContentGrid = forwardRef< +export const EntryListContentPicture = forwardRef< ElementRef, { entryIds: string[] @@ -28,7 +28,7 @@ export const EntryListContentGrid = forwardRef< isRefetching={isRefetching} data={entryIds} renderItem={useTypeScriptHappyCallback(({ item }: { item: string }) => { - return + return }, [])} keyExtractor={defaultKeyExtractor} onViewableItemsChanged={onViewableItemsChanged} diff --git a/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx b/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx new file mode 100644 index 000000000..9d86668b1 --- /dev/null +++ b/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx @@ -0,0 +1,81 @@ +import { useTypeScriptHappyCallback } from "@follow/hooks" +import type { MasonryFlashListProps } from "@shopify/flash-list" +import type { ElementRef } from "react" +import { forwardRef, useMemo } from "react" +import { View } from "react-native" + +import { useFetchEntriesControls } from "@/src/modules/screen/atoms" + +import { TimelineSelectorMasonryList } from "../screen/TimelineSelectorList" +import { useOnViewableItemsChanged } from "./hooks" +import { EntryVideoItem } from "./templates/EntryVideoItem" + +export const EntryListContentVideo = forwardRef< + ElementRef, + { + entryIds: string[] + } & Omit, "data" | "renderItem"> +>(({ entryIds, ...rest }, ref) => { + const { fetchNextPage, refetch, isRefetching, isFetching } = useFetchEntriesControls() + const { onViewableItemsChanged, onScroll } = useOnViewableItemsChanged({ + disabled: isRefetching, + }) + + const ListFooterComponent = useMemo( + () => + isFetching ? ( + + + + + ) : null, + [isFetching], + ) + + return ( + { + return + }, [])} + keyExtractor={defaultKeyExtractor} + onViewableItemsChanged={onViewableItemsChanged} + onScroll={onScroll} + onEndReached={fetchNextPage} + numColumns={2} + estimatedItemSize={100} + ListFooterComponent={ListFooterComponent} + {...rest} + onRefresh={refetch} + /> + ) +}) + +const defaultKeyExtractor = (item: string) => { + return item +} + +export function EntryItemSkeleton() { + return ( + + {/* Video thumbnail */} + + + {/* Description and footer */} + + {/* Description */} + + + + {/* Footer with feed icon and metadata */} + + + + + + + + ) +} diff --git a/apps/mobile/src/modules/entry-list/EntryListSelector.tsx b/apps/mobile/src/modules/entry-list/EntryListSelector.tsx index 300d364e1..4465cbb0e 100644 --- a/apps/mobile/src/modules/entry-list/EntryListSelector.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListSelector.tsx @@ -5,10 +5,11 @@ import { useContext, useEffect, useRef } from "react" import type { ScrollView } from "react-native" import { SetAttachNavigationScrollViewContext } from "@/src/components/layouts/tabbar/contexts/AttachNavigationScrollViewContext" -import { EntryListContentGrid } from "@/src/modules/entry-list/EntryListContentGrid" +import { EntryListContentPicture } from "@/src/modules/entry-list/EntryListContentPicture" import { EntryListContentArticle } from "./EntryListContentArticle" import { EntryListContentSocial } from "./EntryListContentSocial" +import { EntryListContentVideo } from "./EntryListContentVideo" import { EntryListContextViewContext } from "./EntryListContext" export function EntryListSelector({ @@ -30,16 +31,19 @@ export function EntryListSelector({ } }, [setAttachNavigationScrollViewRef, ref, active]) - let ContentComponent: typeof EntryListContentSocial | typeof EntryListContentGrid = + let ContentComponent: typeof EntryListContentSocial | typeof EntryListContentPicture = EntryListContentArticle switch (viewId) { case FeedViewType.SocialMedia: { ContentComponent = EntryListContentSocial break } - case FeedViewType.Pictures: + case FeedViewType.Pictures: { + ContentComponent = EntryListContentPicture + break + } case FeedViewType.Videos: { - ContentComponent = EntryListContentGrid + ContentComponent = EntryListContentVideo break } case FeedViewType.Articles: { diff --git a/apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntryPictureItem.tsx similarity index 73% rename from apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx rename to apps/mobile/src/modules/entry-list/templates/EntryPictureItem.tsx index 32d482670..df71468b5 100644 --- a/apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx +++ b/apps/mobile/src/modules/entry-list/templates/EntryPictureItem.tsx @@ -1,5 +1,3 @@ -import { FeedViewType } from "@follow/constants" -import { transformVideoUrl } from "@follow/utils" import { uniqBy } from "es-toolkit/compat" import { LinearGradient } from "expo-linear-gradient" import { useEffect, useMemo } from "react" @@ -11,19 +9,13 @@ import { EntryContentWebView } from "@/src/components/native/webview/EntryConten import { MediaCarousel } from "@/src/components/ui/carousel/MediaCarousel" import { RelativeDateTime } from "@/src/components/ui/datetime/RelativeDateTime" import { FeedIcon } from "@/src/components/ui/icon/feed-icon" -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 { unreadSyncService } from "@/src/store/unread/store" -import { VideoContextMenu } from "../../context-menu/video" -import { useEntryListContextView } from "../EntryListContext" - -export function EntryGridItem({ id }: { id: string }) { +export function EntryPictureItem({ id }: { id: string }) { const item = useEntry(id) - const view = useEntryListContextView() if (!item || !item.media) { return null @@ -39,32 +31,14 @@ export function EntryGridItem({ id }: { id: string }) { ) } - if (view === FeedViewType.Pictures) { - return ( - - unreadSyncService.markEntryAsRead(id)} - /> - - ) - } - return ( - - { - unreadSyncService.markEntryAsRead(id) - if (!item.url) return - const formattedUrl = transformVideoUrl({ url: item.url }) || item.url - openLink(formattedUrl) - }} - > - - - + + unreadSyncService.markEntryAsRead(id)} + /> + ) } diff --git a/apps/mobile/src/modules/entry-list/templates/EntryVideoItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntryVideoItem.tsx new file mode 100644 index 000000000..9def0f513 --- /dev/null +++ b/apps/mobile/src/modules/entry-list/templates/EntryVideoItem.tsx @@ -0,0 +1,41 @@ +import { transformVideoUrl } from "@follow/utils" + +import { MediaCarousel } from "@/src/components/ui/carousel/MediaCarousel" +import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable" +import { openLink } from "@/src/lib/native" +import { useEntry } from "@/src/store/entry/hooks" +import { unreadSyncService } from "@/src/store/unread/store" + +import { VideoContextMenu } from "../../context-menu/video" + +export function EntryVideoItem({ id }: { id: string }) { + const item = useEntry(id) + + if (!item || !item.media) { + return null + } + + const firstMedia = item.media[0]! + + return ( + + { + unreadSyncService.markEntryAsRead(id) + if (!item.url) return + const formattedUrl = transformVideoUrl({ url: item.url }) || item.url + openLink(formattedUrl) + }} + > + + + + ) +}