diff --git a/__tests__/hooks/useCommunityCurationsDrops.test.ts b/__tests__/hooks/useCommunityCurationsDrops.test.ts index 81e653d57d..291014dd02 100644 --- a/__tests__/hooks/useCommunityCurationsDrops.test.ts +++ b/__tests__/hooks/useCommunityCurationsDrops.test.ts @@ -42,24 +42,12 @@ const getDefaultQueryResult = ( isLoading: false, }); -const buildDrop = ({ - id, - mimeType, -}: { - readonly id: string; - readonly mimeType?: string | undefined; -}): ApiDrop => +const buildDrop = ({ id }: { readonly id: string }): ApiDrop => ({ id, metadata: [], nft_links: [], - parts: mimeType - ? [ - { - media: [{ mime_type: mimeType }], - }, - ] - : [], + parts: [], }) as unknown as ApiDrop; describe("useCommunityCurationsDrops", () => { @@ -105,23 +93,20 @@ describe("useCommunityCurationsDrops", () => { ).toBeUndefined(); }); - it("dedupes loaded drops and keeps existing media filtering", () => { - const imageDrop = buildDrop({ id: "image-drop", mimeType: "image/png" }); - const videoDrop = buildDrop({ id: "video-drop", mimeType: "video/mp4" }); - const duplicateVideoDrop = buildDrop({ - id: "video-drop", - mimeType: "video/mp4", - }); + it("dedupes loaded drops", () => { + const firstDrop = buildDrop({ id: "first-drop" }); + const secondDrop = buildDrop({ id: "second-drop" }); + const duplicateSecondDrop = buildDrop({ id: "second-drop" }); mockUseInfiniteQuery.mockReturnValue( getDefaultQueryResult([ { - data: [imageDrop, videoDrop], + data: [firstDrop, secondDrop], page: 1, next: true, }, { - data: [duplicateVideoDrop], + data: [duplicateSecondDrop], page: 2, next: false, }, @@ -129,13 +114,16 @@ describe("useCommunityCurationsDrops", () => { ); const { result } = renderHook(() => - useCommunityCurationsDrops({ limit: 12, mediaFilter: "video" }) + useCommunityCurationsDrops({ limit: 12 }) ); expect(result.current.allDrops.map((drop) => drop.id)).toEqual([ - "image-drop", - "video-drop", + "first-drop", + "second-drop", + ]); + expect(result.current.drops.map((drop) => drop.id)).toEqual([ + "first-drop", + "second-drop", ]); - expect(result.current.drops.map((drop) => drop.id)).toEqual(["video-drop"]); }); }); diff --git a/components/community-curations/CommunityCurations.tsx b/components/community-curations/CommunityCurations.tsx index acf08cda06..e4de0cb2da 100644 --- a/components/community-curations/CommunityCurations.tsx +++ b/components/community-curations/CommunityCurations.tsx @@ -3,21 +3,9 @@ import { COMMUNITY_CURATIONS_LIMIT } from "@/components/community-curations/communityCurations.constants"; import CommunityCurationsMasonry from "@/components/community-curations/CommunityCurationsMasonry"; import { useLayout } from "@/components/brain/my-stream/layout/LayoutContext"; -import type { CommonSelectItem } from "@/components/utils/select/CommonSelect"; -import CommonTabs from "@/components/utils/select/tabs/CommonTabs"; -import { - useCommunityCurationsDrops, - type CommunityCurationsMediaFilter, -} from "@/hooks/useCommunityCurationsDrops"; +import { useCommunityCurationsDrops } from "@/hooks/useCommunityCurationsDrops"; import { useCallback, useState } from "react"; -const MEDIA_FILTER_OPTIONS: CommonSelectItem[] = - [ - { key: "all", label: "All", value: "all" }, - { key: "image", label: "Images", value: "image" }, - { key: "video", label: "Video", value: "video" }, - ]; - const COMMUNITY_CURATIONS_SKELETON_CARDS = [ { id: "compact", mediaHeight: 210, lines: 2 }, { id: "tall", mediaHeight: 320, lines: 4 }, @@ -111,8 +99,6 @@ export default function CommunityCurations() { const [scrollContainer, setScrollContainer] = useState( null ); - const [mediaFilter, setMediaFilter] = - useState("all"); const { allDrops, drops, @@ -122,7 +108,6 @@ export default function CommunityCurations() { isFetchingNextPage, isLoading, } = useCommunityCurationsDrops({ - mediaFilter, limit: COMMUNITY_CURATIONS_LIMIT, }); @@ -132,14 +117,6 @@ export default function CommunityCurations() { !isInitialLoading && !isError && drops.length === 0 && !hasMorePages; const shouldShowMasonry = !isInitialLoading && !isError && (drops.length > 0 || hasMorePages); - const emptyStateTitle = - mediaFilter === "all" - ? "No curated drops yet" - : `No ${mediaFilter} drops found`; - const emptyStateDescription = - mediaFilter === "all" - ? "Community-curated drops will appear here when visible curations have activity." - : "Try All to see every community-curated drop."; const handleFetchNextPage = useCallback(async () => { await fetchNextPage(); }, [fetchNextPage]); @@ -151,7 +128,7 @@ export default function CommunityCurations() { style={waveViewStyle} >
-
+

Community Curations @@ -160,19 +137,6 @@ export default function CommunityCurations() { Community-curated drops from across 6529 Waves.

- -
-
- -
-
@@ -187,8 +151,8 @@ export default function CommunityCurations() { {shouldShowEmptyState && ( )} diff --git a/components/community-curations/CommunityCurationsMasonry.tsx b/components/community-curations/CommunityCurationsMasonry.tsx index da8810bbff..e936821bb7 100644 --- a/components/community-curations/CommunityCurationsMasonry.tsx +++ b/components/community-curations/CommunityCurationsMasonry.tsx @@ -6,6 +6,7 @@ import CircleLoader, { import { TweetPreviewModeProvider } from "@/components/tweets/TweetPreviewModeContext"; import Drop, { DropLocation } from "@/components/waves/drops/Drop"; import type { ExtendedDrop } from "@/helpers/waves/drop.helpers"; +import { useNavigateToDropWave } from "@/hooks/useNavigateToDropWave"; import { useIntersectionObserver } from "@/hooks/scroll/useIntersectionObserver"; import { type RenderComponentProps, @@ -239,6 +240,8 @@ function CommunityCurationsInfiniteScrollTrigger({ function CommunityCurationsMasonryItem({ data: drop, }: RenderComponentProps) { + const navigateToDropWave = useNavigateToDropWave(); + return (
diff --git a/components/community-curations/communityCurations.helpers.ts b/components/community-curations/communityCurations.helpers.ts deleted file mode 100644 index 4a6ba6f0a1..0000000000 --- a/components/community-curations/communityCurations.helpers.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { ApiDrop } from "@/generated/models/ApiDrop"; -import { ApiNftLinkMediaPreviewStatusEnum } from "@/generated/models/ApiNftLinkMediaPreview"; -import { getDropPreviewImageUrl } from "@/helpers/waves/drop.helpers"; - -export type CommunityCurationsMediaType = "image" | "video" | "audio" | "other"; - -const toNonEmptyString = (value: string | null | undefined): string | null => { - const trimmed = value?.trim(); - if (!trimmed) { - return null; - } - - return trimmed; -}; - -const getMediaTypeFromMimeType = ( - mimeType: string | null | undefined -): CommunityCurationsMediaType | null => { - const normalized = mimeType?.toLowerCase(); - if (!normalized) { - return null; - } - - if (normalized.startsWith("image/")) { - return "image"; - } - - if (normalized.startsWith("video/")) { - return "video"; - } - - if (normalized.startsWith("audio/")) { - return "audio"; - } - - return null; -}; - -const getMediaTypeFromUrl = ( - url: string | null | undefined -): CommunityCurationsMediaType | null => { - const normalized = url?.split(/[?#]/)[0]?.toLowerCase(); - if (!normalized) { - return null; - } - - if (/\.(avif|gif|jpe?g|png|webp|svg)$/.test(normalized)) { - return "image"; - } - - if (/\.(m3u8|mov|mp4|webm)$/.test(normalized)) { - return "video"; - } - - if (/\.(aac|flac|m4a|mp3|ogg|wav)$/.test(normalized)) { - return "audio"; - } - - return null; -}; - -const getNftLinkMediaType = ( - drop: ApiDrop -): CommunityCurationsMediaType | null => { - for (const nftLink of drop.nft_links ?? []) { - const preview = nftLink.data?.media_preview; - const previewUrl = - preview?.status === ApiNftLinkMediaPreviewStatusEnum.Ready - ? (toNonEmptyString(preview.card_url) ?? - toNonEmptyString(preview.small_url) ?? - toNonEmptyString(preview.thumb_url)) - : null; - const previewMediaType = - getMediaTypeFromMimeType(preview?.mime_type) ?? - getMediaTypeFromUrl(previewUrl); - if (previewMediaType) { - return previewMediaType; - } - - const mediaUri = toNonEmptyString(nftLink.data?.media_uri); - const mediaUriType = getMediaTypeFromUrl(mediaUri); - if (mediaUriType) { - return mediaUriType; - } - } - - return null; -}; - -export const getCommunityCurationsMediaType = ( - drop: ApiDrop -): CommunityCurationsMediaType => { - if (getDropPreviewImageUrl(drop.metadata)) { - return "image"; - } - - const media = drop.parts.flatMap((part) => part.media).at(0); - if (!media) { - return getNftLinkMediaType(drop) ?? "other"; - } - - return getMediaTypeFromMimeType(media.mime_type) ?? "other"; -}; diff --git a/components/user/waves/UserPageProfileWaveMasonry.tsx b/components/user/waves/UserPageProfileWaveMasonry.tsx index 4c8b244069..aaf7ff1ec7 100644 --- a/components/user/waves/UserPageProfileWaveMasonry.tsx +++ b/components/user/waves/UserPageProfileWaveMasonry.tsx @@ -7,7 +7,6 @@ import CircleLoader, { import { Spinner } from "@/components/dotLoader/DotLoader"; import { TweetPreviewModeProvider } from "@/components/tweets/TweetPreviewModeContext"; import CommonIntersectionElement from "@/components/utils/CommonIntersectionElement"; -import type { ApiDrop } from "@/generated/models/ApiDrop"; import { ApiDropType } from "@/generated/models/ApiDropType"; import Drop, { DropLocation } from "@/components/waves/drops/Drop"; import DropMinimalIdentityRow from "@/components/waves/drops/DropMinimalIdentityRow"; @@ -20,6 +19,7 @@ import { areSameProfileIdentity } from "@/helpers/ProfileHelpers"; import type { ExtendedDrop } from "@/helpers/waves/drop.helpers"; import { useCurationManagementPermission } from "@/hooks/useCurationManagementPermission"; import { useDropCurationMembershipMutation } from "@/hooks/drops/useDropCurationMembershipMutation"; +import { useNavigateToDropWave } from "@/hooks/useNavigateToDropWave"; import useDeviceInfo from "@/hooks/useDeviceInfo"; import useIsTouchDevice from "@/hooks/useIsTouchDevice"; import { XMarkIcon } from "@heroicons/react/24/outline"; @@ -221,6 +221,7 @@ function UserPageProfileWaveMasonryCard({ const [activePartIndex, setActivePartIndex] = useState(0); const replyTo = drop.reply_to; const activePart = drop.parts[activePartIndex] ?? drop.parts[0]; + const navigateToDropWave = useNavigateToDropWave(); const layout = getProfileMasonryCardLayout({ activePart, drop, @@ -237,10 +238,10 @@ function UserPageProfileWaveMasonryCard({ drop={drop} activePartIndex={activePartIndex} setActivePartIndex={setActivePartIndex} - onQuoteClick={(_quotedDrop: ApiDrop) => {}} + onQuoteClick={navigateToDropWave} onLongPress={() => {}} setLongPressTriggered={(_triggered: boolean) => {}} - onDropContentClick={undefined} + onDropContentClick={navigateToDropWave} mediaImageScale={ImageScale.AUTOx1080} fullWidthMedia={true} /> @@ -262,7 +263,8 @@ function UserPageProfileWaveMasonryCard({ dropViewDropId={null} onReply={() => {}} onReplyClick={() => {}} - onQuoteClick={() => {}} + onQuoteClick={navigateToDropWave} + onDropContentClick={navigateToDropWave} identityMode={layout.identityMode} showInteractions={false} /> diff --git a/hooks/useCommunityCurationsDrops.ts b/hooks/useCommunityCurationsDrops.ts index 769836e36b..6f671f4fb3 100644 --- a/hooks/useCommunityCurationsDrops.ts +++ b/hooks/useCommunityCurationsDrops.ts @@ -1,23 +1,14 @@ "use client"; -import { - getCommunityCurationsMediaType, - type CommunityCurationsMediaType, -} from "@/components/community-curations/communityCurations.helpers"; import type { ApiDrop } from "@/generated/models/ApiDrop"; import { DropSize, type ExtendedDrop } from "@/helpers/waves/drop.helpers"; import { commonApiFetch } from "@/services/api/common-api"; import { useInfiniteQuery } from "@tanstack/react-query"; import { useMemo } from "react"; -export type CommunityCurationsMediaFilter = - | "all" - | Exclude; - const COMMUNITY_CURATIONS_DROPS_QUERY_KEY = "COMMUNITY_CURATIONS_DROPS"; interface UseCommunityCurationsDropsProps { - readonly mediaFilter?: CommunityCurationsMediaFilter | undefined; readonly limit: number; readonly enabled?: boolean | undefined; } @@ -56,12 +47,6 @@ const getUniqueDrops = ( return drops; }; -const matchesMediaFilter = ( - drop: ExtendedDrop, - mediaFilter: CommunityCurationsMediaFilter -): boolean => - mediaFilter === "all" || getCommunityCurationsMediaType(drop) === mediaFilter; - const fetchCommunityCurationsDrops = ({ limit, page, @@ -79,7 +64,6 @@ const fetchCommunityCurationsDrops = ({ }; export function useCommunityCurationsDrops({ - mediaFilter = "all", limit, enabled = true, }: UseCommunityCurationsDropsProps) { @@ -102,14 +86,9 @@ export function useCommunityCurationsDrops({ [query.data?.pages] ); - const drops = useMemo( - () => allDrops.filter((drop) => matchesMediaFilter(drop, mediaFilter)), - [allDrops, mediaFilter] - ); - return { ...query, allDrops, - drops, + drops: allDrops, }; } diff --git a/hooks/useNavigateToDropWave.ts b/hooks/useNavigateToDropWave.ts new file mode 100644 index 0000000000..7bae32af44 --- /dev/null +++ b/hooks/useNavigateToDropWave.ts @@ -0,0 +1,49 @@ +"use client"; + +import { getWaveRoute } from "@/helpers/navigation.helpers"; +import { useRouter } from "next/navigation"; +import { useCallback } from "react"; +import useDeviceInfo from "./useDeviceInfo"; + +type DropWaveNavigationTarget = { + readonly serial_no?: number | string | null; + readonly wave?: { + readonly id?: string | null; + readonly chat?: { + readonly scope?: { + readonly group?: { + readonly is_direct_message?: boolean | null; + } | null; + } | null; + } | null; + } | null; +}; + +export function useNavigateToDropWave(): ( + drop: DropWaveNavigationTarget +) => void { + const router = useRouter(); + const { isApp } = useDeviceInfo(); + + return useCallback( + (drop: DropWaveNavigationTarget) => { + const wave = drop.wave; + const waveId = wave?.id?.trim(); + const serialNo = `${drop.serial_no ?? ""}`.trim(); + + if (!wave || !waveId || !serialNo) { + return; + } + + const href = getWaveRoute({ + waveId, + serialNo, + isDirectMessage: wave.chat?.scope?.group?.is_direct_message === true, + isApp, + }); + + router.push(href); + }, + [isApp, router] + ); +}