-
-
-
+
+
+
+
+
-
+
Be the First to Make a Vote
-
+
Vote on this drop to see voter rankings appear here.
diff --git a/components/waves/drop/useSingleWaveDropData.ts b/components/waves/drop/useSingleWaveDropData.ts
index 0eb7381abf..b9536d2649 100644
--- a/components/waves/drop/useSingleWaveDropData.ts
+++ b/components/waves/drop/useSingleWaveDropData.ts
@@ -3,14 +3,29 @@
import { useCallback, useMemo } from "react";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import { DropSize } from "@/helpers/waves/drop.helpers";
-import { useDrop } from "@/hooks/useDrop";
+import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
import { useWaveData } from "@/hooks/useWaveData";
+import { useQuery } from "@tanstack/react-query";
+import type { ApiDrop } from "@/generated/models/ApiDrop";
+import { DROP_DETAIL_STALE_TIME_MS } from "@/services/api/drop-api";
+import { fetchDropV2ById } from "@/services/api/wave-drops-v2-api";
export const useSingleWaveDropData = (
initialDrop: ExtendedDrop,
onClose: () => void
) => {
- const { drop } = useDrop({ dropId: initialDrop.id });
+ const { data: drop } = useQuery
({
+ queryKey: [
+ QueryKey.DROP,
+ {
+ drop_id: initialDrop.id,
+ view: "single-wave-drop",
+ },
+ ],
+ queryFn: ({ signal }) =>
+ fetchDropV2ById(initialDrop.id, signal, { includeTopRaters: false }),
+ staleTime: DROP_DETAIL_STALE_TIME_MS,
+ });
const onWaveNotFound = useCallback(() => {
onClose();
diff --git a/components/waves/drops/CurationDropFooter.tsx b/components/waves/drops/CurationDropFooter.tsx
index 5c7bb01a44..d1369d2d65 100644
--- a/components/waves/drops/CurationDropFooter.tsx
+++ b/components/waves/drops/CurationDropFooter.tsx
@@ -5,6 +5,7 @@ import clsx from "clsx";
import WaveDropActionsAddReaction from "./WaveDropActionsAddReaction";
import WaveDropActionsCopyLink from "./WaveDropActionsCopyLink";
import WaveDropReactions from "./WaveDropReactions";
+import { getReactionCount } from "./reaction-utils";
interface CurationDropFooterProps {
readonly drop: ExtendedDrop;
@@ -16,7 +17,7 @@ export default function CurationDropFooter({
className,
}: CurationDropFooterProps) {
const hasVisibleReactions = drop.reactions.some(
- (reaction) => reaction.profiles.length > 0
+ (reaction) => getReactionCount(reaction) > 0
);
return (
diff --git a/components/waves/drops/DropAuthorBadges.tsx b/components/waves/drops/DropAuthorBadges.tsx
index ccab7ede99..077f5084cc 100644
--- a/components/waves/drops/DropAuthorBadges.tsx
+++ b/components/waves/drops/DropAuthorBadges.tsx
@@ -39,10 +39,19 @@ interface DropAuthorBadgesProfile {
readonly artist_of_prevote_cards?: readonly number[] | null;
readonly profile_wave_id?: string | null;
readonly is_wave_creator?: boolean | null;
+ readonly badges?: {
+ readonly artist_of_main_stage_submissions?: number | null;
+ readonly artist_of_memes?: number | null;
+ readonly profile_wave_id?: string | null;
+ } | null;
readonly classification: ApiProfileClassification;
readonly sub_classification: string | null;
}
+type ApiProfileMinWithAuthorBadges = ApiProfileMin & {
+ readonly badges?: DropAuthorBadgesProfile["badges"];
+};
+
interface DropAuthorBadgesProps {
readonly profile: DropAuthorBadgesProfile;
readonly tooltipIdPrefix?: string | undefined;
@@ -61,10 +70,16 @@ interface DropAuthorBadgesProps {
const DEFAULT_CONTAINER_CLASS = "tw-inline-flex tw-items-center tw-gap-x-1.5";
-const toApiProfileMin = (profile: DropAuthorBadgesProfile): ApiProfileMin => {
+const getProfileWaveId = (profile: DropAuthorBadgesProfile): string | null =>
+ profile.profile_wave_id ?? profile.badges?.profile_wave_id ?? null;
+
+const toApiProfileMin = (
+ profile: DropAuthorBadgesProfile
+): ApiProfileMinWithAuthorBadges => {
const primaryAddress =
profile.primary_address ?? profile.primary_wallet ?? "";
const fallbackId = primaryAddress;
+ const profileWaveId = getProfileWaveId(profile);
return {
id: profile.id ?? fallbackId,
@@ -97,10 +112,11 @@ const toApiProfileMin = (profile: DropAuthorBadgesProfile): ApiProfileMin => {
profile.artist_of_prevote_cards !== undefined
? [...profile.artist_of_prevote_cards]
: [],
- profile_wave_id: profile.profile_wave_id ?? null,
- is_wave_creator: profile.is_wave_creator === true,
+ profile_wave_id: profileWaveId,
+ is_wave_creator: profile.is_wave_creator === true || profileWaveId !== null,
classification: profile.classification,
sub_classification: profile.sub_classification,
+ badges: profile.badges,
};
};
@@ -115,7 +131,8 @@ export const DropAuthorBadges: React.FC = ({
const submissionCount = getSubmissionCount(profile);
const trophyCount = getTrophyArtworkCount(profile);
const hasActivityBadge = submissionCount > 0 || trophyCount > 0;
- const isWaveCreator = profile.is_wave_creator === true;
+ const isWaveCreator =
+ profile.is_wave_creator === true || getProfileWaveId(profile) !== null;
const modalUser = React.useMemo(() => toApiProfileMin(profile), [profile]);
diff --git a/components/waves/drops/WaveDropActionsMarkUnread.tsx b/components/waves/drops/WaveDropActionsMarkUnread.tsx
index 0f4780d5e7..f6c154a7c5 100644
--- a/components/waves/drops/WaveDropActionsMarkUnread.tsx
+++ b/components/waves/drops/WaveDropActionsMarkUnread.tsx
@@ -60,6 +60,11 @@ export default function WaveDropActionsMarkUnread({
queryClient.invalidateQueries({
queryKey: [QueryKey.WAVES_OVERVIEW],
});
+ queryClient
+ .invalidateQueries({
+ queryKey: [QueryKey.WAVES_V2],
+ })
+ .catch(() => undefined);
queryClient.invalidateQueries({
queryKey: [QueryKey.WAVE, { wave_id: drop.wave.id }],
diff --git a/components/waves/drops/WaveDropQuoteWithSerialNo.tsx b/components/waves/drops/WaveDropQuoteWithSerialNo.tsx
index 925d5e6ad7..1812a09de6 100644
--- a/components/waves/drops/WaveDropQuoteWithSerialNo.tsx
+++ b/components/waves/drops/WaveDropQuoteWithSerialNo.tsx
@@ -3,12 +3,12 @@
import React, { useEffect, useState } from "react";
import WaveDropQuote from "./WaveDropQuote";
-import { commonApiFetch } from "@/services/api/common-api";
import { useQuery } from "@tanstack/react-query";
import { WaveDropsSearchStrategy } from "@/contexts/wave/hooks/types";
import type { ApiWaveDropsFeed } from "@/generated/models/ApiWaveDropsFeed";
import type { ApiDrop } from "@/generated/models/ApiDrop";
import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
+import { fetchWaveDropsFeedV2 } from "@/services/api/wave-drops-v2-api";
interface WaveDropQuoteWithSerialNoProps {
readonly serialNo: number;
readonly waveId: string;
@@ -43,20 +43,13 @@ const WaveDropQuoteWithSerialNo: React.FC = ({
strategy: WaveDropsSearchStrategy.Both,
},
],
- queryFn: async () => {
- const params: Record = {
- limit: "1",
- serial_no_limit: `${serialNo}`,
- search_strategy: WaveDropsSearchStrategy.Both,
- };
-
- const results = await commonApiFetch({
- endpoint: `waves/${waveId}/drops`,
- params,
- });
-
- return results;
- },
+ queryFn: async () =>
+ fetchWaveDropsFeedV2({
+ waveId,
+ limit: 1,
+ serialNoLimit: serialNo,
+ searchStrategy: WaveDropsSearchStrategy.Both,
+ }),
});
const [drop, setDrop] = useState(null);
useEffect(() => {
diff --git a/components/waves/drops/WaveDropReactions.tsx b/components/waves/drops/WaveDropReactions.tsx
index aeadf28588..fa0c42689b 100644
--- a/components/waves/drops/WaveDropReactions.tsx
+++ b/components/waves/drops/WaveDropReactions.tsx
@@ -28,8 +28,9 @@ import React, {
import { Tooltip } from "react-tooltip";
import {
cloneReactionEntries,
- findReactionIndex,
+ applyProfileReactionToEntries,
getReactionErrorMessage,
+ getReactionCount,
removeUserFromReactions,
toProfileMin,
} from "./reaction-utils";
@@ -44,18 +45,196 @@ import {
} from "@/utils/monitoring/dropReactionMonitoring";
import styles from "./WaveDropReactions.module.scss";
import WaveDropReactionsDetailDialog from "./WaveDropReactionsDetailDialog";
+import { fetchDropReactionDetailsV2 } from "@/services/api/wave-drops-v2-api";
interface WaveDropReactionsProps {
readonly drop: ApiDrop;
}
+interface DetailedReactionsState {
+ readonly dropId: string;
+ readonly reactions: ApiDropReaction[];
+}
+
+const getReactionClassNames = ({
+ animate,
+ canReact,
+ selected,
+}: {
+ readonly animate: boolean;
+ readonly canReact: boolean;
+ readonly selected: boolean;
+}) => {
+ let hoverStyle = "";
+ if (canReact) {
+ hoverStyle = selected
+ ? "hover:tw-border-primary-500 hover:tw-bg-primary-500/10"
+ : "hover:tw-border-iron-500 hover:tw-bg-iron-900/40";
+ }
+
+ let animationStyle = "";
+ if (animate) {
+ animationStyle = selected
+ ? styles["reactionSlideUp"]!
+ : styles["reactionSlideDown"]!;
+ }
+
+ return {
+ borderStyle: selected ? "tw-border-primary-500" : "tw-border-iron-700",
+ bgStyle: selected ? "tw-bg-primary-500/10" : "tw-bg-iron-900/40",
+ hoverStyle,
+ animationStyle,
+ };
+};
+
+function ReactionTooltipContent({
+ displayProfiles,
+ isDetailsLoading,
+ moreCount,
+ onMoreClick,
+ total,
+}: {
+ readonly displayProfiles: ApiDropReaction["profiles"];
+ readonly isDetailsLoading: boolean;
+ readonly moreCount: number;
+ readonly onMoreClick: (e: React.MouseEvent) => void;
+ readonly total: number;
+}) {
+ if (isDetailsLoading && displayProfiles.length === 0) {
+ return Loading reactions...;
+ }
+
+ if (displayProfiles.length === 0) {
+ return (
+
+ {formatLargeNumber(total)} {total === 1 ? "reaction" : "reactions"}
+
+ );
+ }
+
+ return (
+
+ by{" "}
+ {displayProfiles.map((profile, index) => {
+ const displayName = profile.handle ?? profile.id;
+ const isLast = index === displayProfiles.length - 1;
+
+ return (
+
+ {profile.handle ? (
+ e.stopPropagation()}
+ >
+ {displayName}
+
+ ) : (
+ {displayName}
+ )}
+ {isLast ? null : ", "}
+
+ );
+ })}
+ {moreCount > 0 && (
+ <>
+ {" "}
+
+ >
+ )}
+
+ );
+}
+
const WaveDropReactions: React.FC = ({ drop }) => {
const [dialogReaction, setDialogReaction] = useState(null);
+ const [detailedReactionsState, setDetailedReactionsState] =
+ useState(null);
+ const [detailsLoadingDropId, setDetailsLoadingDropId] = useState<
+ string | null
+ >(null);
+ const detailsRequestRef = useRef<{
+ readonly dropId: string;
+ readonly promise: Promise;
+ } | null>(null);
const isTouchDevice = useIsTouchDevice();
+ const detailedReactions =
+ detailedReactionsState?.dropId === drop.id
+ ? detailedReactionsState.reactions
+ : null;
+ const detailsLoading = detailsLoadingDropId === drop.id;
+
+ const reactionsWithDetails = useMemo(() => {
+ if (!detailedReactions) {
+ return drop.reactions;
+ }
- const handleOpenDialog = useCallback((reactionKey: string) => {
- setDialogReaction(reactionKey);
- }, []);
+ const detailsByReaction = new Map(
+ detailedReactions.map((reaction) => [reaction.reaction, reaction])
+ );
+
+ return drop.reactions.map((reaction) => {
+ const detailedReaction = detailsByReaction.get(reaction.reaction);
+ if (!detailedReaction) {
+ return reaction;
+ }
+ const profilesById = new Map(
+ detailedReaction.profiles.map((profile) => [profile.id, profile])
+ );
+ for (const profile of reaction.profiles) {
+ profilesById.set(profile.id, profile);
+ }
+
+ return {
+ ...reaction,
+ profiles: [...profilesById.values()],
+ count: getReactionCount(reaction),
+ };
+ });
+ }, [detailedReactions, drop.reactions]);
+
+ const loadReactionDetails = useCallback(() => {
+ if (detailedReactions) {
+ return null;
+ }
+
+ if (detailsRequestRef.current?.dropId === drop.id) {
+ return detailsRequestRef.current.promise;
+ }
+
+ const requestDropId = drop.id;
+ const request = (async () => {
+ setDetailsLoadingDropId(requestDropId);
+ try {
+ const reactions = await fetchDropReactionDetailsV2(requestDropId);
+ setDetailedReactionsState({ dropId: requestDropId, reactions });
+ } catch {
+ setDetailedReactionsState({ dropId: requestDropId, reactions: [] });
+ } finally {
+ setDetailsLoadingDropId((current) =>
+ current === requestDropId ? null : current
+ );
+ detailsRequestRef.current = null;
+ }
+ })();
+
+ detailsRequestRef.current = { dropId: requestDropId, promise: request };
+ return request;
+ }, [detailedReactions, drop.id]);
+
+ const handleOpenDialog = useCallback(
+ (reactionKey: string) => {
+ setDialogReaction(reactionKey);
+ loadReactionDetails()?.catch(() => undefined);
+ },
+ [loadReactionDetails]
+ );
const handleCloseDialog = useCallback(() => {
setDialogReaction(null);
@@ -63,20 +242,23 @@ const WaveDropReactions: React.FC = ({ drop }) => {
return (
<>
- {drop.reactions.map((reaction) => (
+ {reactionsWithDetails.map((reaction) => (
))}
>
);
@@ -86,11 +268,15 @@ function WaveDropReaction({
drop,
reaction,
onOpenDetailDialog,
+ onLoadDetails,
+ isDetailsLoading,
isTouchDevice,
}: {
readonly drop: ApiDrop;
readonly reaction: ApiDropReaction;
readonly onOpenDetailDialog: (reactionKey: string) => void;
+ readonly onLoadDetails: () => Promise | null;
+ readonly isDetailsLoading: boolean;
readonly isTouchDevice: boolean;
}) {
const { setToast, connectedProfile } = useAuth();
@@ -134,7 +320,7 @@ function WaveDropReaction({
[touchHandlers]
);
- const [total, setTotal] = useState(reaction.profiles.length);
+ const [total, setTotal] = useState(getReactionCount(reaction));
const [selected, setSelected] = useState(
reaction.reaction === drop.context_profile_context?.reaction
);
@@ -163,18 +349,20 @@ function WaveDropReaction({
}, [drop.context_profile_context?.reaction, reaction.reaction]);
useEffect(() => {
+ const nextTotal = getReactionCount(reaction);
if (reaction.profiles === prevProfilesRef.current) {
- return;
+ const timeoutId = setTimeout(() => {
+ setTotal((current) => (current === nextTotal ? current : nextTotal));
+ }, 0);
+ return () => clearTimeout(timeoutId);
}
prevProfilesRef.current = reaction.profiles;
- const nextTotal = reaction.profiles.length;
-
const timeoutId = setTimeout(() => {
setTotal((current) => (current === nextTotal ? current : nextTotal));
}, 0);
return () => clearTimeout(timeoutId);
- }, [reaction.profiles]);
+ }, [reaction]);
// Trigger animation when total changes
useEffect(() => {
@@ -275,32 +463,15 @@ function WaveDropReaction({
const reactions = cloneReactionEntries(draft.reactions);
const userId = connectedProfile?.id ?? null;
- const reactionsWithoutUser = removeUserFromReactions(
- reactions,
- userId
- );
-
- if (willSelect && userProfileMin) {
- const existingIndex = findReactionIndex(
- reactionsWithoutUser,
- reaction.reaction
- );
-
- if (existingIndex >= 0) {
- const target = reactionsWithoutUser[existingIndex]!;
- reactionsWithoutUser[existingIndex] = {
- ...target,
- profiles: [...target.profiles, userProfileMin],
- };
- } else {
- reactionsWithoutUser.push({
- reaction: reaction.reaction,
- profiles: [userProfileMin],
- });
- }
- }
-
- draft.reactions = reactionsWithoutUser;
+ draft.reactions = userProfileMin
+ ? applyProfileReactionToEntries({
+ entries: reactions,
+ nextReaction: willSelect ? reaction.reaction : null,
+ previousReaction:
+ drop.context_profile_context?.reaction ?? null,
+ profileMin: userProfileMin,
+ })
+ : removeUserFromReactions(reactions, userId);
const existingContext: ApiDropContextProfileContext =
draft.context_profile_context ??
drop.context_profile_context ?? {
@@ -423,6 +594,12 @@ function WaveDropReaction({
return { displayProfiles, moreCount };
}, [reaction.profiles, total]);
+ const handlePointerEnter = useCallback(() => {
+ if (!isTouchDevice) {
+ onLoadDetails()?.catch(() => undefined);
+ }
+ }, [isTouchDevice, onLoadDetails]);
+
const handleMoreClick = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
@@ -431,23 +608,17 @@ function WaveDropReaction({
[onOpenDetailDialog, reaction.reaction]
);
- // styles
- const borderStyle = selected ? "tw-border-primary-500" : "tw-border-iron-700";
- const bgStyle = selected ? "tw-bg-primary-500/10" : "tw-bg-iron-900/40";
- let hoverStyle = "";
- if (canReact) {
- hoverStyle = selected
- ? "hover:tw-border-primary-500 hover:tw-bg-primary-500/10"
- : "hover:tw-border-iron-500 hover:tw-bg-iron-900/40";
- }
- let animationStyle = "";
- if (animate) {
- if (selected) {
- animationStyle = styles["reactionSlideUp"]!;
- } else {
- animationStyle = styles["reactionSlideDown"]!;
- }
- }
+ const { animationStyle, bgStyle, borderStyle, hoverStyle } =
+ getReactionClassNames({ animate, canReact, selected });
+ const tooltipContent = (
+
+ );
if (!emojiNode || total === 0) return null;
return (
@@ -457,6 +628,8 @@ function WaveDropReaction({
onClick={handleClick}
disabled={!canReact}
aria-disabled={!canReact}
+ onMouseEnter={handlePointerEnter}
+ onFocus={handlePointerEnter}
{...(!isTouchDevice && { "data-tooltip-id": tooltipId })}
data-text-selection-exclude="true"
className={clsx(
@@ -493,45 +666,7 @@ function WaveDropReaction({
>
{emojiNodeTooltip}
-
- by{" "}
- {tooltipProfiles.displayProfiles.map((profile, index) => {
- const displayName = profile.handle ?? profile.id;
- const isLast =
- index === tooltipProfiles.displayProfiles.length - 1;
- const showComma = !isLast;
-
- return (
-
- {profile.handle ? (
- e.stopPropagation()}
- >
- {displayName}
-
- ) : (
- {displayName}
- )}
- {showComma && ", "}
-
- );
- })}
- {tooltipProfiles.moreCount > 0 && (
- <>
- {" "}
-
- >
- )}
-
+ {tooltipContent}
)}
diff --git a/components/waves/drops/WaveDropReactionsDetailDialog.tsx b/components/waves/drops/WaveDropReactionsDetailDialog.tsx
index 99986bfa63..163ad7f5cd 100644
--- a/components/waves/drops/WaveDropReactionsDetailDialog.tsx
+++ b/components/waves/drops/WaveDropReactionsDetailDialog.tsx
@@ -8,12 +8,14 @@ import clsx from "clsx";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
+import { getReactionCount } from "./reaction-utils";
interface WaveDropReactionsDetailDialogProps {
readonly isOpen: boolean;
readonly onClose: () => void;
readonly reactions: ApiDropReaction[];
readonly initialReaction?: string | undefined;
+ readonly isLoading?: boolean | undefined;
}
export default function WaveDropReactionsDetailDialog({
@@ -21,6 +23,7 @@ export default function WaveDropReactionsDetailDialog({
onClose,
reactions,
initialReaction,
+ isLoading = false,
}: WaveDropReactionsDetailDialogProps) {
const [selectedReaction, setSelectedReaction] = useState(
initialReaction ?? reactions[0]?.reaction ?? ""
@@ -45,7 +48,10 @@ export default function WaveDropReactionsDetailDialog({
selectedReaction={selectedReaction}
onSelectReaction={setSelectedReaction}
/>
-
+
);
@@ -135,7 +141,7 @@ function ReactionButton({
{emojiNode}
);
@@ -143,9 +149,19 @@ function ReactionButton({
function ProfilesList({
profiles,
+ isLoading,
}: {
readonly profiles: ApiDropReaction["profiles"];
+ readonly isLoading: boolean;
}) {
+ if (isLoading && profiles.length === 0) {
+ return (
+
diff --git a/components/waves/drops/WaveDropReply.tsx b/components/waves/drops/WaveDropReply.tsx
index 546449a975..b7556559de 100644
--- a/components/waves/drops/WaveDropReply.tsx
+++ b/components/waves/drops/WaveDropReply.tsx
@@ -23,16 +23,12 @@ interface WaveDropReplyProps {
*/
export default function WaveDropReply({
dropId,
- dropPartId,
+ dropPartId: _dropPartId,
maybeDrop,
onReplyClick,
}: WaveDropReplyProps) {
const fixedReplyHeightClasses = "tw-h-[24px] tw-min-h-[24px] tw-max-h-[24px]";
- const { drop, content, isLoading } = useDropContent(
- dropId,
- dropPartId,
- maybeDrop
- );
+ const { drop, content, isLoading } = useDropContent(dropId, 1, maybeDrop);
const replyPreviewContent = useMemo(() => {
if (content.apiMedia.length > 0 || content.segments.length !== 1) {
return content;
@@ -88,7 +84,11 @@ export default function WaveDropReply({
onReplyClick(drop.serial_no)}
+ onClick={() => {
+ if (drop.serial_no > 0) {
+ onReplyClick(drop.serial_no);
+ }
+ }}
className="tw-min-w-0 tw-flex-1 tw-overflow-hidden"
textClassName="tw-min-w-0 tw-overflow-hidden"
linkify={false}
diff --git a/components/waves/drops/reaction-utils.ts b/components/waves/drops/reaction-utils.ts
index 954ef7df29..7d29c5c9a9 100644
--- a/components/waves/drops/reaction-utils.ts
+++ b/components/waves/drops/reaction-utils.ts
@@ -9,6 +9,28 @@ type ReactionEntry = {
[key: string]: unknown;
};
+export const getReactionCount = (
+ reaction: Pick & { readonly count?: unknown }
+): number => {
+ if (
+ typeof reaction.count === "number" &&
+ Number.isFinite(reaction.count) &&
+ reaction.count >= 0
+ ) {
+ return reaction.count;
+ }
+
+ return reaction.profiles.length;
+};
+
+const withReactionCount = (
+ entry: ReactionEntry,
+ count: number
+): ReactionEntry => ({
+ ...entry,
+ count: Math.max(0, count),
+});
+
export const cloneReactionEntries = (
reactions: readonly ApiDropReaction[] | null | undefined
): ReactionEntry[] => {
@@ -64,7 +86,7 @@ export const removeUserFromReactions = (
return sanitizedEntries;
};
-export const findReactionIndex = (
+const findReactionIndex = (
entries: ReactionEntry[],
reactionCode: string
): number => {
@@ -77,6 +99,80 @@ export const findReactionIndex = (
return -1;
};
+export const applyProfileReactionToEntries = ({
+ entries,
+ nextReaction,
+ previousReaction,
+ profileMin,
+}: {
+ readonly entries: ReactionEntry[];
+ readonly nextReaction: string | null;
+ readonly previousReaction: string | null;
+ readonly profileMin: ApiProfileMin;
+}): ReactionEntry[] => {
+ const normalizedPreviousReaction =
+ previousReaction === nextReaction ? null : previousReaction;
+ const userId = profileMin.id;
+ const nextEntries: ReactionEntry[] = [];
+
+ for (const entry of entries) {
+ const filteredProfiles = duplicateProfilesWithoutUser(
+ entry.profiles,
+ userId
+ );
+ const shouldDecrement =
+ normalizedPreviousReaction !== null &&
+ entry.reaction === normalizedPreviousReaction;
+ const nextCount = getReactionCount(entry) - (shouldDecrement ? 1 : 0);
+
+ if (nextCount > 0) {
+ nextEntries.push(
+ withReactionCount(
+ {
+ ...entry,
+ profiles: filteredProfiles,
+ },
+ nextCount
+ )
+ );
+ }
+ }
+
+ if (nextReaction === null) {
+ return nextEntries;
+ }
+
+ const existingIndex = findReactionIndex(nextEntries, nextReaction);
+ if (existingIndex >= 0) {
+ const target = nextEntries[existingIndex]!;
+ const hasProfile = target.profiles.some(
+ (profile) => profile.id === profileMin.id
+ );
+ nextEntries[existingIndex] = withReactionCount(
+ {
+ ...target,
+ profiles: hasProfile
+ ? target.profiles
+ : [...target.profiles, profileMin],
+ },
+ getReactionCount(target) + 1
+ );
+ return nextEntries;
+ }
+
+ nextEntries.push(
+ withReactionCount(
+ {
+ reaction: nextReaction,
+ profiles: [profileMin],
+ },
+ 1
+ )
+ );
+
+ return nextEntries;
+};
+
export const toProfileMin = (
profile: ApiIdentity | null
): ApiProfileMin | null => {
diff --git a/components/waves/drops/useDropContent.ts b/components/waves/drops/useDropContent.ts
index 1c372e9abb..f2c61d0dad 100644
--- a/components/waves/drops/useDropContent.ts
+++ b/components/waves/drops/useDropContent.ts
@@ -1,7 +1,7 @@
"use client";
import { useMemo } from "react";
-import { keepPreviousData, useQuery } from "@tanstack/react-query";
+import { useQuery } from "@tanstack/react-query";
import type { ApiDrop } from "@/generated/models/ApiDrop";
import { sanitizeErrorForUser } from "@/utils/error-sanitizer";
import type { ProcessedContent } from "./media-utils";
@@ -27,6 +27,11 @@ export const useDropContent = (
dropPartId: number,
maybeDrop: ApiDrop | null
): UseDropContentResult => {
+ const previewPart = maybeDrop?.parts.find((p) => p.part_id === dropPartId);
+ const shouldFetchDrop =
+ !maybeDrop ||
+ (!previewPart?.content?.trim() && (previewPart?.media.length ?? 0) === 0);
+
// Fetch drop data
const {
data: drop,
@@ -35,9 +40,9 @@ export const useDropContent = (
} = useQuery({
queryKey: getDropQueryKey(dropId),
queryFn: () => fetchDropByIdBatched(dropId),
- placeholderData: keepPreviousData,
- initialData: maybeDrop ?? undefined,
- enabled: !maybeDrop,
+ placeholderData: (previousDrop) => previousDrop ?? maybeDrop ?? undefined,
+ initialData: shouldFetchDrop ? undefined : maybeDrop,
+ enabled: shouldFetchDrop,
staleTime: DROP_DETAIL_STALE_TIME_MS,
});
@@ -76,7 +81,7 @@ export const useDropContent = (
return {
drop: drop ?? null,
content,
- isLoading: isFetching && !maybeDrop,
+ isLoading: isFetching && !drop,
error,
};
};
diff --git a/components/waves/drops/wave-drops-all/index.tsx b/components/waves/drops/wave-drops-all/index.tsx
index 25eb95750a..b5e4dae738 100644
--- a/components/waves/drops/wave-drops-all/index.tsx
+++ b/components/waves/drops/wave-drops-all/index.tsx
@@ -9,10 +9,14 @@ import {
} from "@/contexts/wave/UnreadDividerContext";
import { useWaveChatScrollOptional } from "@/contexts/wave/WaveChatScrollContext";
import type { ApiDrop } from "@/generated/models/ApiDrop";
+import type { ApiWave } from "@/generated/models/ApiWave";
import { getWaveRoute } from "@/helpers/navigation.helpers";
import type { Drop, ExtendedDrop } from "@/helpers/waves/drop.helpers";
import { DropSize } from "@/helpers/waves/drop.helpers";
-import { isWaveDirectMessage } from "@/helpers/waves/wave.helpers";
+import {
+ isWaveDirectMessage,
+ toApiWaveMin,
+} from "@/helpers/waves/wave.helpers";
import useDeviceInfo from "@/hooks/useDeviceInfo";
import { useScrollBehavior } from "@/hooks/useScrollBehavior";
import { useVirtualizedWaveDrops } from "@/hooks/useVirtualizedWaveDrops";
@@ -35,6 +39,7 @@ const EMPTY_DROPS: Drop[] = [];
interface WaveDropsAllProps {
readonly waveId: string;
+ readonly wave?: ApiWave | undefined;
readonly dropId: string | null;
readonly onReply: ({
drop,
@@ -57,6 +62,7 @@ interface WaveDropsAllProps {
const WaveDropsAllInner: React.FC = ({
waveId,
+ wave,
dropId,
onReply,
activeDrop,
@@ -77,7 +83,7 @@ const WaveDropsAllInner: React.FC = ({
const containerRef = useRef(null);
const { waveMessages, fetchNextPage, waitAndRevealDrop } =
- useVirtualizedWaveDrops(waveId, dropId);
+ useVirtualizedWaveDrops(waveId, dropId, wave);
const { setUnreadDividerSerialNo } = useUnreadDivider();
@@ -87,7 +93,7 @@ const WaveDropsAllInner: React.FC = ({
isMuted
);
- const { data: boostedDrops } = useWaveBoostedDrops({ waveId });
+ const { data: boostedDrops } = useWaveBoostedDrops({ waveId, wave });
const scrollBehavior = useScrollBehavior();
const {
@@ -138,6 +144,23 @@ const WaveDropsAllInner: React.FC = ({
shouldPinToBottom,
});
+ const renderedWaveMessagesWithFullWave = useMemo(() => {
+ if (!renderedWaveMessages || !wave) {
+ return renderedWaveMessages;
+ }
+
+ const waveMin = toApiWaveMin(wave);
+ return {
+ ...renderedWaveMessages,
+ drops: renderedWaveMessages.drops.map(
+ (drop: Drop): Drop =>
+ drop.type === DropSize.FULL && drop.wave.id === wave.id
+ ? { ...drop, wave: waveMin }
+ : drop
+ ),
+ };
+ }, [renderedWaveMessages, wave]);
+
const {
serialTarget,
queueSerialTarget,
@@ -265,7 +288,7 @@ const WaveDropsAllInner: React.FC = ({
>
=
const WaveDropsAll: React.FC = ({
waveId,
+ wave,
dropId,
onReply,
activeDrop,
@@ -327,6 +351,7 @@ const WaveDropsAll: React.FC = ({
undefined);
onSuccess?.();
} catch (error) {
const defaultMessage = isMuted
@@ -57,7 +62,7 @@ export default function WaveMute({
e.stopPropagation();
handleToggleMute();
}}
- className="tw-flex tw-items-center tw-gap-2 tw-bg-transparent tw-w-full tw-border-none tw-px-3 tw-py-1 tw-text-sm tw-leading-6 tw-text-iron-300 hover:tw-bg-iron-800 tw-text-left tw-transition tw-duration-300 tw-ease-out"
+ className="tw-flex tw-w-full tw-items-center tw-gap-2 tw-border-none tw-bg-transparent tw-px-3 tw-py-1 tw-text-left tw-text-sm tw-leading-6 tw-text-iron-300 tw-transition tw-duration-300 tw-ease-out hover:tw-bg-iron-800"
role="menuitem"
tabIndex={-1}
>
diff --git a/components/waves/leaderboard/content/WaveLeaderboardDropContent.tsx b/components/waves/leaderboard/content/WaveLeaderboardDropContent.tsx
index cd77e6ebd6..a9219b3754 100644
--- a/components/waves/leaderboard/content/WaveLeaderboardDropContent.tsx
+++ b/components/waves/leaderboard/content/WaveLeaderboardDropContent.tsx
@@ -3,17 +3,10 @@
import React, { useState } from "react";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import WaveDropContent from "@/components/waves/drops/WaveDropContent";
-import WaveDropMetadata from "@/components/waves/drops/WaveDropMetadata";
import { useRouter } from "next/navigation";
import WaveDropReactions from "@/components/waves/drops/WaveDropReactions";
import type { DropContentPresentation } from "@/components/waves/drops/dropContentPresentation";
-import {
- getDropIdentityProfile,
- getDropVisibleMetadata,
-} from "@/components/waves/drops/identityDisplay.helpers";
-import { areSameProfileIdentity } from "@/helpers/ProfileHelpers";
import { getWaveRoute } from "@/helpers/navigation.helpers";
-import { WaveLeaderboardIdentity } from "../identity/WaveLeaderboardIdentity";
interface WaveLeaderboardDropContentProps {
readonly drop: ExtendedDrop;
@@ -32,20 +25,6 @@ export const WaveLeaderboardDropContent: React.FC<
}) => {
const router = useRouter();
const [activePartIndex, setActivePartIndex] = useState(0);
- const visibleMetadata = getDropVisibleMetadata({
- wave: drop.wave,
- metadata: drop.metadata,
- });
- const identityProfile = getDropIdentityProfile({
- wave: drop.wave,
- metadata: drop.metadata,
- });
- const isSelfNominee = identityProfile
- ? areSameProfileIdentity({
- left: drop.author,
- right: identityProfile,
- })
- : false;
const onDropContentClick = (clickedDrop: ExtendedDrop) => {
const href = getWaveRoute({
@@ -71,16 +50,6 @@ export const WaveLeaderboardDropContent: React.FC<
mediaContainerHeightClassName={mediaContainerHeightClassName}
contentPresentation={contentPresentation}
/>
-
- {!!visibleMetadata.length && (
-
- )}
diff --git a/components/waves/leaderboard/drops/DefaultWaveLeaderboardDrop.tsx b/components/waves/leaderboard/drops/DefaultWaveLeaderboardDrop.tsx
index 7969f71339..450b1a7979 100644
--- a/components/waves/leaderboard/drops/DefaultWaveLeaderboardDrop.tsx
+++ b/components/waves/leaderboard/drops/DefaultWaveLeaderboardDrop.tsx
@@ -20,7 +20,6 @@ import { startDropOpen } from "@/utils/monitoring/dropOpenTiming";
import React from "react";
import { createPortal } from "react-dom";
import { WaveLeaderboardDropContent } from "../content/WaveLeaderboardDropContent";
-import { WaveLeaderboardDropFooter } from "./footer/WaveLeaderboardDropFooter";
import { WaveLeaderboardDropAuthorAvatar } from "./header/WaveLeaderboardDropAuthor";
import { WaveLeaderboardDropHeader } from "./header/WaveLeaderboardDropHeader";
import { WaveLeaderboardDropRaters } from "./header/WaveleaderboardDropRaters";
@@ -172,7 +171,6 @@ export const DefaultWaveLeaderboardDrop: React.FC<
winningThreshold={winningThreshold}
isVotingClosed={isVotingClosed}
/>
-
= ({ drop }) => {
- return ;
-};
diff --git a/components/waves/leaderboard/drops/header/WaveleaderboardDropRaters.tsx b/components/waves/leaderboard/drops/header/WaveleaderboardDropRaters.tsx
index 039c8927fc..c81adaec11 100644
--- a/components/waves/leaderboard/drops/header/WaveleaderboardDropRaters.tsx
+++ b/components/waves/leaderboard/drops/header/WaveleaderboardDropRaters.tsx
@@ -1,12 +1,7 @@
import React from "react";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import { formatNumberWithCommas } from "@/helpers/Helpers";
-import { Tooltip } from "react-tooltip";
-import Link from "next/link";
-import Image from "next/image";
-import { getScaledImageUri, ImageScale } from "@/helpers/image.helpers";
import DropVoteProgressing from "@/components/drops/view/utils/DropVoteProgressing";
-import { TOOLTIP_STYLES } from "@/helpers/tooltip.helpers";
import {
WAVE_VOTING_LABELS,
WAVE_VOTE_STATS_LABELS,
@@ -105,52 +100,6 @@ export const WaveLeaderboardDropRaters: React.FC<
-
- {drop.top_raters.map((voter, index) => {
- const voterLabel =
- voter.profile.handle ?? voter.profile.primary_address;
- const tooltipId = `voter-${drop.id}-${voter.profile.id}`;
-
- return (
-
-
-
- {voter.profile.pfp ? (
-
- ) : (
-
- )}
-
-
-
- {voterLabel} • {formatNumberWithCommas(voter.rating)}{" "}
- {votingLabel}
-
-
- );
- })}
-
{formatNumberWithCommas(drop.raters_count)} {votersCountLabel}
diff --git a/components/waves/leaderboard/sidebar/WaveLeaderboardRightSidebarBoostedDrops.tsx b/components/waves/leaderboard/sidebar/WaveLeaderboardRightSidebarBoostedDrops.tsx
index 4517498ae8..972fb83ac2 100644
--- a/components/waves/leaderboard/sidebar/WaveLeaderboardRightSidebarBoostedDrops.tsx
+++ b/components/waves/leaderboard/sidebar/WaveLeaderboardRightSidebarBoostedDrops.tsx
@@ -22,6 +22,7 @@ export const WaveLeaderboardRightSidebarBoostedDrops =
const { data: boostedDrops, isLoading } = useWaveBoostedDrops({
waveId: wave.id,
+ wave,
limit: MAX_BOOSTED_DROPS,
timeWindow,
});
diff --git a/components/waves/quorum/QuorumParticipationDropLinkPreview.tsx b/components/waves/quorum/QuorumParticipationDropLinkPreview.tsx
index c0b6d26c09..78318e1890 100644
--- a/components/waves/quorum/QuorumParticipationDropLinkPreview.tsx
+++ b/components/waves/quorum/QuorumParticipationDropLinkPreview.tsx
@@ -8,10 +8,8 @@ import LinkHandlerFrame from "@/components/waves/LinkHandlerFrame";
import { DropLocation } from "@/components/waves/drops/drop.types";
import type { DropInteractionParams } from "@/components/waves/drops/drop.types";
import WaveDropQuote from "@/components/waves/drops/WaveDropQuote";
-import { WaveDropsSearchStrategy } from "@/contexts/wave/hooks/types";
import type { ApiDrop } from "@/generated/models/ApiDrop";
import { ApiDropType } from "@/generated/models/ApiDropType";
-import type { ApiWaveDropsFeed } from "@/generated/models/ApiWaveDropsFeed";
import {
convertApiDropToExtendedDrop,
type ExtendedDrop,
@@ -21,7 +19,7 @@ import {
fetchDropByIdBatched,
getDropQueryKey,
} from "@/services/api/drop-api";
-import { commonApiFetch } from "@/services/api/common-api";
+import { fetchQuorumParticipationDropPreviewBySerialNoV2 } from "@/services/api/quorum-participation-drop-preview-v2-api";
import QuorumParticipationDrop from "./QuorumParticipationDrop";
interface QuorumParticipationDropLinkPreviewProps {
@@ -52,32 +50,6 @@ const toSerialNumber = (
return Number.isFinite(parsed) ? parsed : null;
};
-const fetchDropBySerialNo = async ({
- waveId,
- serialNo,
-}: {
- readonly waveId: string;
- readonly serialNo: number;
-}): Promise
=> {
- const results = await commonApiFetch({
- endpoint: `waves/${waveId}/drops`,
- params: {
- limit: "1",
- serial_no_limit: `${serialNo}`,
- search_strategy: WaveDropsSearchStrategy.Both,
- },
- });
-
- const drop = results.drops.find(
- (candidate) => candidate.serial_no === serialNo
- );
- if (!drop) {
- return null;
- }
-
- return { ...drop, wave: results.wave } as ApiDrop;
-};
-
export default function QuorumParticipationDropLinkPreview({
href,
waveId,
@@ -112,7 +84,7 @@ export default function QuorumParticipationDropLinkPreview({
}
if (parsedSerialNo !== null) {
- return await fetchDropBySerialNo({
+ return await fetchQuorumParticipationDropPreviewBySerialNoV2({
waveId,
serialNo: parsedSerialNo,
});
diff --git a/components/waves/specs/wave-notification-settings/useWaveMuteSettings.ts b/components/waves/specs/wave-notification-settings/useWaveMuteSettings.ts
index 5c9c01adea..cafde342c2 100644
--- a/components/waves/specs/wave-notification-settings/useWaveMuteSettings.ts
+++ b/components/waves/specs/wave-notification-settings/useWaveMuteSettings.ts
@@ -27,6 +27,9 @@ export function useWaveMuteSettings(wave: ApiWave) {
queryClient.invalidateQueries({
queryKey: [QueryKey.WAVES_OVERVIEW],
}),
+ queryClient.invalidateQueries({
+ queryKey: [QueryKey.WAVES_V2],
+ }),
]);
} catch (error) {
const defaultMessage = isMuted
diff --git a/components/waves/winners/WaveWinners.tsx b/components/waves/winners/WaveWinners.tsx
index c647e198d5..32b5da0ee5 100644
--- a/components/waves/winners/WaveWinners.tsx
+++ b/components/waves/winners/WaveWinners.tsx
@@ -43,6 +43,7 @@ export const WaveWinners: React.FC = ({
hasNextPage,
} = useWaveDecisions({
waveId: wave.id,
+ wave,
enabled: true, // Always enabled now that we use it for both types
loadAllPages: isApproveWave,
pageSize: isApproveWave
diff --git a/components/waves/winners/WaveWinnersSmall.tsx b/components/waves/winners/WaveWinnersSmall.tsx
index f29a6fd2d2..b1f75aadb9 100644
--- a/components/waves/winners/WaveWinnersSmall.tsx
+++ b/components/waves/winners/WaveWinnersSmall.tsx
@@ -55,6 +55,7 @@ export const WaveWinnersSmall = memo(
hasNextPage,
} = useWaveDecisions({
waveId: wave.id,
+ wave,
enabled: true, // Always enabled now that we use it for both types
loadAllPages: isApproveWave,
pageSize: isApproveWave
diff --git a/components/waves/winners/drops/MemesWaveWinnerDrop.tsx b/components/waves/winners/drops/MemesWaveWinnerDrop.tsx
index e1c31922d6..9186af0513 100644
--- a/components/waves/winners/drops/MemesWaveWinnerDrop.tsx
+++ b/components/waves/winners/drops/MemesWaveWinnerDrop.tsx
@@ -51,6 +51,22 @@ const isClickFromCardDom = (
return event.currentTarget.contains(event.target as Node);
};
+const getNonEmptyText = (
+ value: string | null | undefined
+): string | undefined => {
+ const trimmed = value?.trim();
+ return trimmed && trimmed.length > 0 ? trimmed : undefined;
+};
+
+const getMetadataValue = (
+ winner: ApiWaveDecisionWinner,
+ dataKey: string
+): string | undefined =>
+ getNonEmptyText(
+ winner.drop.metadata.find((metadata) => metadata.data_key === dataKey)
+ ?.data_value
+ );
+
export const MemesWaveWinnersDrop: React.FC = ({
winner,
wave,
@@ -108,11 +124,13 @@ export const MemesWaveWinnersDrop: React.FC = ({
}, [handleMobileMenuOpenChange]);
const title =
- winner.drop.metadata.find((m) => m.data_key === "title")?.data_value ??
+ getNonEmptyText(winner.drop.title) ??
+ getMetadataValue(winner, "title") ??
"Artwork Title";
const description =
- winner.drop.metadata.find((m) => m.data_key === "description")
- ?.data_value ?? "This is an artwork submission for The Memes collection.";
+ getNonEmptyText(winner.drop.parts.at(0)?.content) ??
+ getMetadataValue(winner, "description") ??
+ "This is an artwork submission for The Memes collection.";
const artworkMedia = winner.drop.parts.at(0)?.media.at(0);
@@ -266,47 +284,49 @@ export const MemesWaveWinnersDrop: React.FC = ({
-
- {topVoters.map((voter) => (
-
- e.stopPropagation()}
- scroll={false}
- className="tw-transition-transform desktop-hover:hover:tw-translate-y-[-2px]"
- data-tooltip-id={`voter-${voter.profile.handle ?? voter.profile.primary_address}-${voter.rating}`}
- >
- {voter.profile.pfp ? (
-
- ) : (
-
- )}
-
-
- {voter.profile.handle} -{" "}
- {formatNumberWithCommas(voter.rating)}
-
-
- ))}
-
+ {topVoters.length > 0 && (
+
+ {topVoters.map((voter) => (
+
+ e.stopPropagation()}
+ scroll={false}
+ className="tw-transition-transform desktop-hover:hover:tw-translate-y-[-2px]"
+ data-tooltip-id={`voter-${voter.profile.handle ?? voter.profile.primary_address}-${voter.rating}`}
+ >
+ {voter.profile.pfp ? (
+
+ ) : (
+
+ )}
+
+
+ {voter.profile.handle} -{" "}
+ {formatNumberWithCommas(voter.rating)}
+
+
+ ))}
+
+ )}
{formatNumberWithCommas(ratersCount)}{" "}
diff --git a/components/waves/winners/drops/header/WaveWinnersDropHeaderVoters.tsx b/components/waves/winners/drops/header/WaveWinnersDropHeaderVoters.tsx
index 912f773b27..72478cd0a2 100644
--- a/components/waves/winners/drops/header/WaveWinnersDropHeaderVoters.tsx
+++ b/components/waves/winners/drops/header/WaveWinnersDropHeaderVoters.tsx
@@ -17,20 +17,23 @@ export default function WaveWinnersDropHeaderVoters({
const hasUserVoted = userVote !== 0;
const isNegativeVote = userVote < 0;
const userVoteClass = isNegativeVote ? "tw-text-rose-400" : "tw-text-iron-50";
+ const hasTopRaters = winner.drop.top_raters.length > 0;
return (
-
- {winner.drop.top_raters.map((voter, index) => (
-
- ))}
-
+ {hasTopRaters && (
+
+ {winner.drop.top_raters.map((voter, index) => (
+
+ ))}
+
+ )}
{formatNumberWithCommas(winner.drop.raters_count)}{" "}
diff --git a/contexts/wave/hooks/useEnhancedWavesListCore.ts b/contexts/wave/hooks/useEnhancedWavesListCore.ts
index 0328b608f1..4f9f0b7c28 100644
--- a/contexts/wave/hooks/useEnhancedWavesListCore.ts
+++ b/contexts/wave/hooks/useEnhancedWavesListCore.ts
@@ -1,7 +1,7 @@
"use client";
-import type { ApiWave } from "@/generated/models/ApiWave";
import type { ApiWaveType } from "@/generated/models/ApiWaveType";
+import type { SidebarWave, SidebarWaveContributor } from "@/types/waves.types";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { MinimalWaveNewDropsCount } from "./useNewDropCounter";
import useNewDropCounter, { getNewestTimestamp } from "./useNewDropCounter";
@@ -14,7 +14,7 @@ export interface MinimalWave {
type: ApiWaveType;
newDropsCount: MinimalWaveNewDropsCount;
picture: string | null;
- contributors: { pfp: string; identity: string }[];
+ contributors: readonly SidebarWaveContributor[];
isPinned: boolean;
isMuted: boolean;
unreadDropsCount: number;
@@ -22,13 +22,10 @@ export interface MinimalWave {
firstUnreadDropSerialNo: number | null;
}
-// Wave type that includes the computed isPinned field from useWavesList
-interface EnhancedApiWave extends ApiWave {
- isPinned?: boolean;
-}
+type EnhancedSidebarWave = SidebarWave & { readonly isPinned?: boolean };
interface WavesDataSource {
- waves: EnhancedApiWave[];
+ waves: EnhancedSidebarWave[];
isFetching: boolean;
isFetchingNextPage: boolean;
hasNextPage: boolean;
@@ -117,20 +114,20 @@ function useEnhancedWavesListCore(
}, [activeWaveId, resetWaveUnreadCount]);
const mapWave = useCallback(
- (wave: EnhancedApiWave): MinimalWave => {
+ (wave: EnhancedSidebarWave): MinimalWave => {
const wsData = newDropsCounts[wave.id];
const hasNewWsDrops = (wsData?.count ?? 0) > 0;
const newDrops = {
count: wsData?.count ?? 0,
latestDropTimestamp: getNewestTimestamp(
wsData?.latestDropTimestamp,
- wave.metrics.latest_drop_timestamp ?? null
+ wave.latestDropTimestamp ?? null
),
firstUnreadSerialNo: wsData?.firstUnreadSerialNo ?? null,
};
const isCleared = clearedUnreadWaveIds.has(wave.id) && !hasNewWsDrops;
const forcedCount = forcedUnreadCounts[wave.id];
- const apiFirstUnread = wave.metrics.first_unread_drop_serial_no ?? null;
+ const apiFirstUnread = wave.firstUnreadDropSerialNo ?? null;
const wsFirstUnread = wsData?.firstUnreadSerialNo ?? null;
const wasCleared = clearedUnreadWaveIds.has(wave.id);
let firstUnreadDropSerialNo: number | null = null;
@@ -152,28 +149,24 @@ function useEnhancedWavesListCore(
} else if (wasCleared && hasNewWsDrops) {
unreadDropsCount = wsData?.count ?? 0;
} else if (hasNewWsDrops) {
- unreadDropsCount =
- wave.metrics.your_unread_drops_count + (wsData?.count ?? 0);
+ unreadDropsCount = wave.unreadDropsCount + (wsData?.count ?? 0);
} else {
- unreadDropsCount = wave.metrics.your_unread_drops_count;
+ unreadDropsCount = wave.unreadDropsCount;
}
return {
id: wave.id,
name: wave.name,
- type: wave.wave.type,
+ type: wave.type,
picture: wave.picture,
- contributors: wave.contributors_overview.map((c) => ({
- pfp: c.contributor_pfp,
- identity: c.contributor_identity,
- })),
+ contributors: wave.contributors,
newDropsCount: newDrops,
isPinned: options.supportsPinning
? (wave.isPinned ?? wave.pinned ?? false)
: false,
- isMuted: wave.metrics.muted,
+ isMuted: wave.muted,
unreadDropsCount,
- latestReadTimestamp: wave.metrics.your_latest_read_timestamp,
+ latestReadTimestamp: wave.latestReadTimestamp,
firstUnreadDropSerialNo,
};
},
diff --git a/contexts/wave/hooks/useNewDropCounter.ts b/contexts/wave/hooks/useNewDropCounter.ts
index f7a98a8466..bf88f24c05 100644
--- a/contexts/wave/hooks/useNewDropCounter.ts
+++ b/contexts/wave/hooks/useNewDropCounter.ts
@@ -1,11 +1,11 @@
"use client";
-import { AuthContext } from "@/components/auth/Auth";
-import type { ApiWave } from "@/generated/models/ApiWave";
+import { useAuth } from "@/components/auth/Auth";
import type { WsDropUpdateMessage } from "@/helpers/Types";
import { WsMessageType } from "@/helpers/Types";
import { useWebSocketMessage } from "@/services/websocket/useWebSocketMessage";
-import { useCallback, useContext, useEffect, useRef, useState } from "react";
+import type { SidebarWave } from "@/types/waves.types";
+import { useCallback, useEffect, useRef, useState } from "react";
/**
* Interface for tracking new drops count for a wave
@@ -53,11 +53,11 @@ export function getNewestTimestamp(
*/
function useNewDropCounter(
activeWaveId: string | null,
- waves: ApiWave[],
+ waves: SidebarWave[],
refetchWaves: () => void,
options: UseNewDropCounterOptions = {}
) {
- const { connectedProfile } = useContext(AuthContext);
+ const { connectedProfile } = useAuth();
const {
otherListWaveIds = DEFAULT_OTHER_LIST_WAVE_IDS,
unknownWaveRefetchCooldownMs = DEFAULT_UNKNOWN_WAVE_REFETCH_COOLDOWN_MS,
@@ -78,8 +78,8 @@ function useNewDropCounter(
count: 0,
latestDropTimestamp: getNewestTimestamp(
prev[waveId]?.latestDropTimestamp,
- waves.find((wave) => wave.id === waveId)?.metrics
- .latest_drop_timestamp ?? null
+ waves.find((wave) => wave.id === waveId)?.latestDropTimestamp ??
+ null
),
firstUnreadSerialNo: null,
},
@@ -96,7 +96,7 @@ function useNewDropCounter(
count: 0,
latestDropTimestamp: getNewestTimestamp(
prev[wave.id]?.latestDropTimestamp,
- wave.metrics.latest_drop_timestamp ?? null
+ wave.latestDropTimestamp ?? null
),
firstUnreadSerialNo: null,
};
@@ -158,7 +158,7 @@ function useNewDropCounter(
return;
}
- if (wave.metrics.muted) return;
+ if (wave.muted) return;
if (
connectedProfile?.handle?.toLowerCase() ===
diff --git a/contexts/wave/utils/wave-messages-utils.ts b/contexts/wave/utils/wave-messages-utils.ts
index bb5df1bba6..91980d748d 100644
--- a/contexts/wave/utils/wave-messages-utils.ts
+++ b/contexts/wave/utils/wave-messages-utils.ts
@@ -1,14 +1,11 @@
import { WAVE_DROPS_PARAMS } from "@/components/react-query-wrapper/utils/query-utils";
import type { ApiDrop } from "@/generated/models/ApiDrop";
import type { ApiDropId } from "@/generated/models/ApiDropId";
-import type { ApiWaveDropsFeed } from "@/generated/models/ApiWaveDropsFeed";
import { ApiDropSearchStrategy } from "@/generated/models/ApiDropSearchStrategy";
import type { Drop } from "@/helpers/waves/drop.helpers";
import { DropSize, getStableDropKey } from "@/helpers/waves/drop.helpers";
-import {
- commonApiFetch,
- commonApiFetchWithRetry,
-} from "@/services/api/common-api";
+import { commonApiFetchWithRetry } from "@/services/api/common-api";
+import { fetchWaveDropsFeedV2 } from "@/services/api/wave-drops-v2-api";
import type { WaveMessagesUpdate } from "../hooks/types";
/**
@@ -25,17 +22,13 @@ export async function fetchWaveMessages(
signal?: AbortSignal,
updateEligibility?: (waveId: string, eligibility: any) => void
): Promise {
- const params: Record = {
- limit: WAVE_DROPS_PARAMS.limit.toString(),
- };
- if (serialNo) {
- params["serial_no_less_than"] = `${serialNo}`;
- }
-
try {
- const data = await commonApiFetch({
- endpoint: `waves/${waveId}/drops`,
- params,
+ const data = await fetchWaveDropsFeedV2({
+ waveId,
+ limit: WAVE_DROPS_PARAMS.limit,
+ serialNoLimit: serialNo,
+ searchStrategy:
+ serialNo === null ? undefined : ApiDropSearchStrategy.Older,
signal,
});
@@ -52,10 +45,7 @@ export async function fetchWaveMessages(
});
}
- return data.drops.map((drop) => ({
- ...drop,
- wave: data.wave,
- }));
+ return data.drops as ApiDrop[];
} catch (error) {
// Check if this is an abort error
if (error instanceof DOMException && error.name === "AbortError") {
@@ -75,30 +65,17 @@ export async function fetchAroundSerialNoWaveMessages(
serialNo: number,
signal?: AbortSignal
): Promise {
- const params: Record = {
- limit: WAVE_DROPS_PARAMS.limit.toString(),
- };
-
- params["search_strategy"] = ApiDropSearchStrategy.Both;
- params["serial_no_limit"] = `${serialNo}`;
-
try {
- const data = await commonApiFetchWithRetry({
- endpoint: `waves/${waveId}/drops`,
- params,
+ const data = await fetchWaveDropsFeedV2({
+ waveId,
+ limit: WAVE_DROPS_PARAMS.limit,
+ serialNoLimit: serialNo,
+ searchStrategy: ApiDropSearchStrategy.Both,
signal,
- retryOptions: {
- maxRetries: 2,
- initialDelayMs: 300,
- backoffFactor: 1.5,
- jitter: 0.1,
- },
+ withRetry: true,
});
- return data.drops.map((drop) => ({
- ...drop,
- wave: data.wave,
- }));
+ return data.drops as ApiDrop[];
} catch (error) {
// Check if this is an abort error
if (error instanceof DOMException && error.name === "AbortError") {
@@ -333,26 +310,15 @@ export async function fetchNewestWaveMessages(
signal?: AbortSignal,
updateEligibility?: (waveId: string, eligibility: any) => void
): Promise<{ drops: ApiDrop[] | null; highestSerialNo: number | null }> {
- const params: Record = {
- limit: limit.toString(),
- };
- if (sinceSerialNo !== null) {
- // Assuming API uses these parameters for fetching newer messages
- params["serial_no_limit"] = `${sinceSerialNo}`;
- params["search_strategy"] = ApiDropSearchStrategy.Newer;
- }
-
try {
- const data = await commonApiFetchWithRetry({
- endpoint: `waves/${waveId}/drops`,
- params,
+ const data = await fetchWaveDropsFeedV2({
+ waveId,
+ limit,
+ serialNoLimit: sinceSerialNo,
+ searchStrategy:
+ sinceSerialNo === null ? undefined : ApiDropSearchStrategy.Newer,
signal,
- retryOptions: {
- maxRetries: 2,
- initialDelayMs: 300,
- backoffFactor: 1.5,
- jitter: 0.1,
- },
+ withRetry: true,
});
// Update centralized eligibility if callback provided
@@ -368,10 +334,7 @@ export async function fetchNewestWaveMessages(
});
}
- const fetchedDrops = data.drops.map((drop) => ({
- ...drop,
- wave: data.wave,
- }));
+ const fetchedDrops = data.drops as ApiDrop[];
const highestSerialNo = getHighestSerialNo(fetchedDrops);
diff --git a/generated/models/ApiNotificationV2.ts b/generated/models/ApiNotificationV2.ts
index e94d6f50f0..41ee0c6633 100644
--- a/generated/models/ApiNotificationV2.ts
+++ b/generated/models/ApiNotificationV2.ts
@@ -15,6 +15,7 @@ import { ApiDropV2 } from '../models/ApiDropV2';
import { ApiIdentityOverview } from '../models/ApiIdentityOverview';
import { ApiNotificationAdditionalContextV2 } from '../models/ApiNotificationAdditionalContextV2';
import { ApiNotificationCause } from '../models/ApiNotificationCause';
+import { ApiWaveOverview } from '../models/ApiWaveOverview';
import { HttpFile } from '../http/http';
export class ApiNotificationV2 {
@@ -24,6 +25,7 @@ export class ApiNotificationV2 {
'read_at': number | null;
'related_identity': ApiIdentityOverview;
'related_drops': Array;
+ 'related_wave'?: ApiWaveOverview;
'additional_context': ApiNotificationAdditionalContextV2;
static readonly discriminator: string | undefined = undefined;
@@ -67,6 +69,12 @@ export class ApiNotificationV2 {
"type": "Array",
"format": ""
},
+ {
+ "name": "related_wave",
+ "baseName": "related_wave",
+ "type": "ApiWaveOverview",
+ "format": ""
+ },
{
"name": "additional_context",
"baseName": "additional_context",
diff --git a/generated/models/ApiWaveOverview.ts b/generated/models/ApiWaveOverview.ts
index 1f3d1b92dc..33853e80da 100644
--- a/generated/models/ApiWaveOverview.ts
+++ b/generated/models/ApiWaveOverview.ts
@@ -12,6 +12,8 @@
*/
import { ApiWaveOverviewContextProfileContext } from '../models/ApiWaveOverviewContextProfileContext';
+import { ApiWaveOverviewContributor } from '../models/ApiWaveOverviewContributor';
+import { ApiWaveOverviewDescriptionDrop } from '../models/ApiWaveOverviewDescriptionDrop';
import { HttpFile } from '../http/http';
export class ApiWaveOverview {
@@ -23,6 +25,10 @@ export class ApiWaveOverview {
'subscribers_count': number;
'has_competition': boolean;
'is_dm_wave': boolean;
+ 'description_drop': ApiWaveOverviewDescriptionDrop;
+ 'total_drops_count': number;
+ 'is_private': boolean;
+ 'contributors'?: Array;
'context_profile_context'?: ApiWaveOverviewContextProfileContext;
static readonly discriminator: string | undefined = undefined;
@@ -78,6 +84,30 @@ export class ApiWaveOverview {
"type": "boolean",
"format": ""
},
+ {
+ "name": "description_drop",
+ "baseName": "description_drop",
+ "type": "ApiWaveOverviewDescriptionDrop",
+ "format": ""
+ },
+ {
+ "name": "total_drops_count",
+ "baseName": "total_drops_count",
+ "type": "number",
+ "format": "int64"
+ },
+ {
+ "name": "is_private",
+ "baseName": "is_private",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "contributors",
+ "baseName": "contributors",
+ "type": "Array",
+ "format": ""
+ },
{
"name": "context_profile_context",
"baseName": "context_profile_context",
diff --git a/generated/models/ApiWaveOverviewContextProfileContext.ts b/generated/models/ApiWaveOverviewContextProfileContext.ts
index 1a64cfd08a..5ee989f832 100644
--- a/generated/models/ApiWaveOverviewContextProfileContext.ts
+++ b/generated/models/ApiWaveOverviewContextProfileContext.ts
@@ -18,6 +18,7 @@ export class ApiWaveOverviewContextProfileContext {
'pinned': boolean;
'can_chat': boolean;
'unread_drops': number;
+ 'first_unread_drop_serial_no'?: number;
'muted': boolean;
static readonly discriminator: string | undefined = undefined;
@@ -49,6 +50,12 @@ export class ApiWaveOverviewContextProfileContext {
"type": "number",
"format": "int64"
},
+ {
+ "name": "first_unread_drop_serial_no",
+ "baseName": "first_unread_drop_serial_no",
+ "type": "number",
+ "format": "int64"
+ },
{
"name": "muted",
"baseName": "muted",
diff --git a/generated/models/ApiWaveOverviewContributor.ts b/generated/models/ApiWaveOverviewContributor.ts
new file mode 100644
index 0000000000..783493e3b8
--- /dev/null
+++ b/generated/models/ApiWaveOverviewContributor.ts
@@ -0,0 +1,44 @@
+// @ts-nocheck
+/**
+ * 6529.io API
+ * This is the API interface description. Brief terminology overview and an authentication example can be found at https://6529.io/about/api.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { HttpFile } from '../http/http';
+
+export class ApiWaveOverviewContributor {
+ 'handle': string | null;
+ 'pfp': string | null;
+
+ static readonly discriminator: string | undefined = undefined;
+
+ static readonly mapping: {[index: string]: string} | undefined = undefined;
+
+ static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [
+ {
+ "name": "handle",
+ "baseName": "handle",
+ "type": "string",
+ "format": ""
+ },
+ {
+ "name": "pfp",
+ "baseName": "pfp",
+ "type": "string",
+ "format": ""
+ } ];
+
+ static getAttributeTypeMap() {
+ return ApiWaveOverviewContributor.attributeTypeMap;
+ }
+
+ public constructor() {
+ }
+}
diff --git a/generated/models/ApiWaveOverviewDescriptionDrop.ts b/generated/models/ApiWaveOverviewDescriptionDrop.ts
new file mode 100644
index 0000000000..a2e6ced597
--- /dev/null
+++ b/generated/models/ApiWaveOverviewDescriptionDrop.ts
@@ -0,0 +1,45 @@
+// @ts-nocheck
+/**
+ * 6529.io API
+ * This is the API interface description. Brief terminology overview and an authentication example can be found at https://6529.io/about/api.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { ApiDropMedia } from '../models/ApiDropMedia';
+import { HttpFile } from '../http/http';
+
+export class ApiWaveOverviewDescriptionDrop {
+ 'contents'?: string;
+ 'media'?: Array;
+
+ static readonly discriminator: string | undefined = undefined;
+
+ static readonly mapping: {[index: string]: string} | undefined = undefined;
+
+ static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [
+ {
+ "name": "contents",
+ "baseName": "contents",
+ "type": "string",
+ "format": ""
+ },
+ {
+ "name": "media",
+ "baseName": "media",
+ "type": "Array",
+ "format": ""
+ } ];
+
+ static getAttributeTypeMap() {
+ return ApiWaveOverviewDescriptionDrop.attributeTypeMap;
+ }
+
+ public constructor() {
+ }
+}
diff --git a/generated/models/ObjectSerializer.ts b/generated/models/ObjectSerializer.ts
index b41fefb7a2..d8bbf54075 100644
--- a/generated/models/ObjectSerializer.ts
+++ b/generated/models/ObjectSerializer.ts
@@ -284,6 +284,8 @@ export * from '../models/ApiWaveOutcomeType';
export * from '../models/ApiWaveOutcomesPage';
export * from '../models/ApiWaveOverview';
export * from '../models/ApiWaveOverviewContextProfileContext';
+export * from '../models/ApiWaveOverviewContributor';
+export * from '../models/ApiWaveOverviewDescriptionDrop';
export * from '../models/ApiWaveOverviewPage';
export * from '../models/ApiWaveParticipationConfig';
export * from '../models/ApiWaveParticipationIdentitySubmissionAllowDuplicates';
@@ -551,7 +553,7 @@ import { ApiNotification } from '../models/ApiNotification';
import { ApiNotificationAdditionalContextV2 } from '../models/ApiNotificationAdditionalContextV2';
import { ApiNotificationCause } from '../models/ApiNotificationCause';
import { ApiNotificationDropReactedReactor } from '../models/ApiNotificationDropReactedReactor';
-import { ApiNotificationV2 } from '../models/ApiNotificationV2';
+import { ApiNotificationV2 } from '../models/ApiNotificationV2';
import { ApiNotificationsResponse } from '../models/ApiNotificationsResponse';
import { ApiNotificationsResponseV2 } from '../models/ApiNotificationsResponseV2';
import { ApiOutgoingIdentitySubscriptionsPage } from '../models/ApiOutgoingIdentitySubscriptionsPage';
@@ -655,6 +657,8 @@ import { ApiWaveOutcomeType } from '../models/ApiWaveOutcomeType';
import { ApiWaveOutcomesPage } from '../models/ApiWaveOutcomesPage';
import { ApiWaveOverview } from '../models/ApiWaveOverview';
import { ApiWaveOverviewContextProfileContext } from '../models/ApiWaveOverviewContextProfileContext';
+import { ApiWaveOverviewContributor } from '../models/ApiWaveOverviewContributor';
+import { ApiWaveOverviewDescriptionDrop } from '../models/ApiWaveOverviewDescriptionDrop';
import { ApiWaveOverviewPage } from '../models/ApiWaveOverviewPage';
import { ApiWaveParticipationConfig } from '../models/ApiWaveParticipationConfig';
import { ApiWaveParticipationIdentitySubmissionAllowDuplicates } from '../models/ApiWaveParticipationIdentitySubmissionAllowDuplicates';
@@ -1061,6 +1065,8 @@ let typeMap: {[index: string]: any} = {
"ApiWaveOutcomesPage": ApiWaveOutcomesPage,
"ApiWaveOverview": ApiWaveOverview,
"ApiWaveOverviewContextProfileContext": ApiWaveOverviewContextProfileContext,
+ "ApiWaveOverviewContributor": ApiWaveOverviewContributor,
+ "ApiWaveOverviewDescriptionDrop": ApiWaveOverviewDescriptionDrop,
"ApiWaveOverviewPage": ApiWaveOverviewPage,
"ApiWaveParticipationConfig": ApiWaveParticipationConfig,
"ApiWaveParticipationSubmissionStrategy": ApiWaveParticipationSubmissionStrategy,
diff --git a/helpers/artist-activity.helpers.ts b/helpers/artist-activity.helpers.ts
index ad9d336e86..8b05cd4d4a 100644
--- a/helpers/artist-activity.helpers.ts
+++ b/helpers/artist-activity.helpers.ts
@@ -2,18 +2,41 @@ interface ArtistActivityProfileLike {
readonly active_main_stage_submission_ids?: readonly string[] | null;
readonly winner_main_stage_drop_ids?: readonly string[] | null;
readonly artist_of_prevote_cards?: readonly number[] | null;
+ readonly badges?: {
+ readonly artist_of_main_stage_submissions?: number | null;
+ readonly artist_of_memes?: number | null;
+ } | null;
}
+const getNonNegativeCount = (count: number | null | undefined): number => {
+ if (typeof count !== "number" || !Number.isFinite(count)) {
+ return 0;
+ }
+ return Math.max(0, count);
+};
+
+const getArrayCount = (
+ items: readonly string[] | readonly number[] | null | undefined
+): number => items?.length ?? 0;
+
export const getSubmissionCount = (
profile: ArtistActivityProfileLike
-): number => profile.active_main_stage_submission_ids?.length ?? 0;
+): number =>
+ Math.max(
+ getArrayCount(profile.active_main_stage_submission_ids),
+ getNonNegativeCount(profile.badges?.artist_of_main_stage_submissions)
+ );
const getWinnerCount = (profile: ArtistActivityProfileLike): number =>
- profile.winner_main_stage_drop_ids?.length ?? 0;
+ getArrayCount(profile.winner_main_stage_drop_ids);
const getPrevoteArtistCount = (profile: ArtistActivityProfileLike): number =>
- profile.artist_of_prevote_cards?.length ?? 0;
+ getArrayCount(profile.artist_of_prevote_cards);
export const getTrophyArtworkCount = (
profile: ArtistActivityProfileLike
-): number => getWinnerCount(profile) + getPrevoteArtistCount(profile);
+): number =>
+ Math.max(
+ getWinnerCount(profile) + getPrevoteArtistCount(profile),
+ getNonNegativeCount(profile.badges?.artist_of_memes)
+ );
diff --git a/helpers/stream.helpers.ts b/helpers/stream.helpers.ts
index f254dd3ff1..29262a2610 100644
--- a/helpers/stream.helpers.ts
+++ b/helpers/stream.helpers.ts
@@ -8,9 +8,14 @@ import {
} from "@/components/react-query-wrapper/utils/query-utils";
import { jwtDecode } from "jwt-decode";
import { getUserProfile } from "./server.helpers";
-import type { TypedFeedItem, TypedNotificationsResponse } from "@/types/feed.types";
-import type { ApiWaveDropsFeed } from "@/generated/models/ApiWaveDropsFeed";
+import type { TypedFeedItem } from "@/types/feed.types";
import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
+import {
+ fetchWavesV2Page,
+ getWavesV2OverviewQueryKeyParams,
+} from "@/services/api/waves-v2-api";
+import { fetchWaveDropsFeedV2 } from "@/services/api/wave-drops-v2-api";
+import { fetchNotificationsV2 } from "@/services/api/notifications-v2-api";
const getWalletFromJwt = (headers: Record): string | null => {
const jwt = headers["Authorization"]?.split(" ")[1] ?? null;
@@ -43,36 +48,29 @@ const prefetchAuthenticatedWavesOverview = async ({
queryClient: QueryClient;
headers: Record;
}) => {
+ const queryKeyParams = getWavesV2OverviewQueryKeyParams({
+ pageSize: WAVE_FOLLOWING_WAVES_PARAMS.limit,
+ overviewType: WAVE_FOLLOWING_WAVES_PARAMS.initialWavesOverviewType,
+ following:
+ WAVE_FOLLOWING_WAVES_PARAMS.only_waves_followed_by_authenticated_user,
+ directMessage: false,
+ });
+
await queryClient.prefetchInfiniteQuery({
- queryKey: [
- QueryKey.WAVES_OVERVIEW,
- {
- limit: WAVE_FOLLOWING_WAVES_PARAMS.limit,
- type: WAVE_FOLLOWING_WAVES_PARAMS.initialWavesOverviewType,
- only_waves_followed_by_authenticated_user:
+ queryKey: [QueryKey.WAVES_V2, queryKeyParams],
+ queryFn: async ({ pageParam }: { pageParam: number }) => {
+ return await fetchWavesV2Page({
+ page: pageParam,
+ pageSize: WAVE_FOLLOWING_WAVES_PARAMS.limit,
+ overviewType: WAVE_FOLLOWING_WAVES_PARAMS.initialWavesOverviewType,
+ following:
WAVE_FOLLOWING_WAVES_PARAMS.only_waves_followed_by_authenticated_user,
- },
- ],
- queryFn: async ({ pageParam }: { pageParam: number | null }) => {
- const queryParams: Record = {
- limit: `${WAVE_FOLLOWING_WAVES_PARAMS.limit}`,
- offset: `${pageParam}`,
- type: WAVE_FOLLOWING_WAVES_PARAMS.initialWavesOverviewType,
- only_waves_followed_by_authenticated_user: `${WAVE_FOLLOWING_WAVES_PARAMS.only_waves_followed_by_authenticated_user}`,
- };
-
-
- return await commonApiFetch({
- endpoint: `waves-overview`,
- params: queryParams,
+ directMessage: false,
headers,
});
},
- initialPageParam: 0,
- getNextPageParam: (_, allPages) =>
- allPages.at(-1)?.length === WAVE_FOLLOWING_WAVES_PARAMS.limit
- ? allPages.flat().length
- : null,
+ initialPageParam: 1,
+ getNextPageParam: (lastPage) => (lastPage.next ? lastPage.page + 1 : null),
pages: 1,
staleTime: 60000,
});
@@ -136,16 +134,10 @@ const prefetchAuthenticatedWaveFeedItems = async ({
},
],
queryFn: async ({ pageParam }: { pageParam: number | null }) => {
- const params: Record = {
- limit: WAVE_DROPS_PARAMS.limit.toString(),
- };
-
- if (pageParam) {
- params["serial_no_less_than"] = `${pageParam}`;
- }
- return await commonApiFetch({
- endpoint: `waves/${waveId}/drops`,
- params,
+ return await fetchWaveDropsFeedV2({
+ waveId: waveId!,
+ limit: WAVE_DROPS_PARAMS.limit,
+ serialNoLimit: pageParam,
headers,
});
},
@@ -296,18 +288,12 @@ const prefetchAuthenticatedNotificationsItems = async ({
await queryClient.prefetchInfiniteQuery({
queryKey: [
QueryKey.IDENTITY_NOTIFICATIONS,
- { identity: handle, limit: "10" },
+ { identity: handle, limit: "10", cause: null, version: "v2" },
],
queryFn: async ({ pageParam }: { pageParam: number | null }) => {
- const params: Record = {
+ return await fetchNotificationsV2({
limit: "10",
- };
- if (pageParam) {
- params["id_less_than"] = `${pageParam}`;
- }
- return await commonApiFetch({
- endpoint: `notifications`,
- params,
+ pageParam,
headers,
});
},
diff --git a/hooks/drops/useDropInteractionRules.ts b/hooks/drops/useDropInteractionRules.ts
index 92dcfc6f47..2a673672f1 100644
--- a/hooks/drops/useDropInteractionRules.ts
+++ b/hooks/drops/useDropInteractionRules.ts
@@ -1,7 +1,6 @@
"use client";
-import { useContext } from "react";
-import { AuthContext } from "@/components/auth/Auth";
+import { useAuth } from "@/components/auth/Auth";
import type { ApiDrop } from "@/generated/models/ApiDrop";
import { ApiDropType } from "@/generated/models/ApiDropType";
import { DropVoteState } from "./types";
@@ -25,7 +24,7 @@ interface DropInteractionRules {
* @returns Object containing boolean flags for different interaction possibilities
*/
export function useDropInteractionRules(drop: ApiDrop): DropInteractionRules {
- const { connectedProfile, activeProfileProxy } = useContext(AuthContext);
+ const { connectedProfile, activeProfileProxy } = useAuth();
// Check if this is a winner drop
const isWinner = drop.drop_type === ApiDropType.Winner;
diff --git a/hooks/drops/useDropReaction.ts b/hooks/drops/useDropReaction.ts
index e8629205ed..d4ce5ed30e 100644
--- a/hooks/drops/useDropReaction.ts
+++ b/hooks/drops/useDropReaction.ts
@@ -17,10 +17,9 @@ import type { InfiniteData } from "@tanstack/react-query";
import { useQueryClient } from "@tanstack/react-query";
import { useCallback, useRef } from "react";
import {
+ applyProfileReactionToEntries,
cloneReactionEntries,
- findReactionIndex,
getReactionErrorMessage,
- removeUserFromReactions,
toProfileMin,
} from "@/components/waves/drops/reaction-utils";
import {
@@ -153,31 +152,18 @@ const applyReactionToCacheDrop = ({
return drop;
}
- const reactionsWithoutUser = removeUserFromReactions(
- cloneReactionEntries(drop.reactions),
- profileMin.id
- );
-
- if (reactionCode !== null) {
- const targetIndex = findReactionIndex(reactionsWithoutUser, reactionCode);
-
- if (targetIndex >= 0) {
- const target = reactionsWithoutUser[targetIndex]!;
- reactionsWithoutUser[targetIndex] = {
- ...target,
- profiles: [...target.profiles, profileMin],
- };
- } else {
- reactionsWithoutUser.push({
- reaction: reactionCode,
- profiles: [profileMin],
- });
- }
- }
+ const previousReaction =
+ drop.context_profile_context?.reaction ?? baseContext?.reaction ?? null;
+ const reactions = applyProfileReactionToEntries({
+ entries: cloneReactionEntries(drop.reactions),
+ nextReaction: reactionCode,
+ previousReaction,
+ profileMin,
+ });
return {
...drop,
- reactions: reactionsWithoutUser,
+ reactions,
context_profile_context: {
...(drop.context_profile_context ??
baseContext ??
diff --git a/hooks/useCommunityCurationsDrops.ts b/hooks/useCommunityCurationsDrops.ts
index 9a818cac00..1385cf66aa 100644
--- a/hooks/useCommunityCurationsDrops.ts
+++ b/hooks/useCommunityCurationsDrops.ts
@@ -1,8 +1,12 @@
"use client";
import type { ApiDrop } from "@/generated/models/ApiDrop";
+import type { ApiDropV2 } from "@/generated/models/ApiDropV2";
+import type { ApiDropV2PageWithoutCount } from "@/generated/models/ApiDropV2PageWithoutCount";
import { DropSize, type ExtendedDrop } from "@/helpers/waves/drop.helpers";
import { commonApiFetch } from "@/services/api/common-api";
+import { createFallbackWaveMin } from "@/services/api/drop-v2-mappers";
+import { mapLeaderboardDropV2 } from "@/services/api/wave-drops-v2-api";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useMemo } from "react";
@@ -47,21 +51,34 @@ const getUniqueDrops = (
return drops;
};
+const mapCommunityCurationDropV2 = (drop: ApiDropV2): ApiDrop => {
+ const wave = createFallbackWaveMin(drop.author.badges.profile_wave_id ?? "");
+ const mappedDrop = mapLeaderboardDropV2({ drop, wave });
+
+ return {
+ ...mappedDrop,
+ wave,
+ };
+};
+
const fetchCommunityCurationsDrops = ({
limit,
page,
}: {
readonly limit: number;
readonly page: number;
-}): Promise => {
- return commonApiFetch({
- endpoint: "curated-profile-wave-drops",
+}): Promise =>
+ commonApiFetch({
+ endpoint: "v2/curated-profile-wave-drops",
params: {
page: `${page}`,
page_size: `${limit}`,
},
- });
-};
+ }).then((response) => ({
+ data: response.data.map(mapCommunityCurationDropV2),
+ page: response.page,
+ next: response.next,
+ }));
export function useCommunityCurationsDrops({
limit,
diff --git a/hooks/useConnectedAccountsUnreadNotifications.ts b/hooks/useConnectedAccountsUnreadNotifications.ts
index f1d22ca2bc..6d24e55461 100644
--- a/hooks/useConnectedAccountsUnreadNotifications.ts
+++ b/hooks/useConnectedAccountsUnreadNotifications.ts
@@ -3,7 +3,7 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
import { getDefaultQueryRetry } from "@/components/react-query-wrapper/utils/query-utils";
-import type { ApiNotificationsResponse } from "@/generated/models/ApiNotificationsResponse";
+import type { ApiNotificationsResponseV2 } from "@/generated/models/ApiNotificationsResponseV2";
import type { ConnectedWalletAccount } from "@/services/auth/auth.utils";
import { commonApiFetch } from "@/services/api/common-api";
import useCapacitor from "./useCapacitor";
@@ -28,8 +28,8 @@ const fetchUnreadCountForAccount = async (
return 0;
}
- const notifications = await commonApiFetch({
- endpoint: "notifications",
+ const notifications = await commonApiFetch({
+ endpoint: "v2/notifications",
params: { limit: "1" },
headers: {
Authorization: `Bearer ${account.jwt}`,
@@ -46,6 +46,7 @@ export function useConnectedAccountsUnreadNotifications(
const queryKey = [
QueryKey.CONNECTED_ACCOUNT_UNREAD_NOTIFICATIONS,
"connected-account-unread-counts",
+ "v2",
accounts.map((account) => toAddressKey(account.address)),
] as const;
diff --git a/hooks/useDmWavesList.ts b/hooks/useDmWavesList.ts
index 6d377c1d2f..06b65ec771 100644
--- a/hooks/useDmWavesList.ts
+++ b/hooks/useDmWavesList.ts
@@ -3,7 +3,7 @@
import { useCallback, useMemo } from "react";
import { useAuth } from "@/components/auth/Auth";
import { useSeizeConnectContext } from "@/components/auth/SeizeConnectContext";
-import { useWavesOverview } from "./useWavesOverview";
+import { useWavesV2 } from "./useWavesV2";
import {
SIDEBAR_WAVES_OVERVIEW_REFETCH_INTERVAL_MS,
WAVE_FOLLOWING_WAVES_PARAMS,
@@ -33,9 +33,9 @@ const useDmWavesList = () => {
fetchNextPage,
status,
refetch,
- } = useWavesOverview({
- type: WAVE_FOLLOWING_WAVES_PARAMS.initialWavesOverviewType,
- limit: WAVE_FOLLOWING_WAVES_PARAMS.limit,
+ } = useWavesV2({
+ overviewType: WAVE_FOLLOWING_WAVES_PARAMS.initialWavesOverviewType,
+ pageSize: WAVE_FOLLOWING_WAVES_PARAMS.limit,
directMessage: true,
viewerIdentityKey,
refetchInterval: SIDEBAR_WAVES_OVERVIEW_REFETCH_INTERVAL_MS,
@@ -45,8 +45,7 @@ const useDmWavesList = () => {
// sort by latest drop
const sorted = useMemo(() => {
return [...mainWaves].sort(
- (a, b) =>
- b.metrics.latest_drop_timestamp - a.metrics.latest_drop_timestamp
+ (a, b) => (b.latestDropTimestamp ?? 0) - (a.latestDropTimestamp ?? 0)
);
}, [mainWaves]);
diff --git a/hooks/useDropMessages.ts b/hooks/useDropMessages.ts
index c849b4d257..cac78821b0 100644
--- a/hooks/useDropMessages.ts
+++ b/hooks/useDropMessages.ts
@@ -17,20 +17,27 @@ import {
} from "@/components/react-query-wrapper/utils/updateAttachmentInCachedDrops";
import type { ApiAttachment } from "@/generated/models/ApiAttachment";
import type { ApiWaveDropsFeed } from "@/generated/models/ApiWaveDropsFeed";
+import type { ApiWave } from "@/generated/models/ApiWave";
import {
generateUniqueKeys,
mapToExtendedDrops,
} from "@/helpers/waves/wave-drops.helpers";
-import { commonApiFetch } from "@/services/api/common-api";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import type { WsDropUpdateMessage } from "@/helpers/Types";
import { WsMessageType } from "@/helpers/Types";
import { useWebSocketMessage } from "@/services/websocket/useWebSocketMessage";
-import { WaveDropsSearchStrategy } from "@/contexts/wave/hooks/types";
-
-export function useDropMessages(waveId: string, dropId: string | null) {
+import {
+ fetchDropRepliesV2,
+ type ApiWaveDropsV2PageFeed,
+} from "@/services/api/wave-drops-v2-api";
+
+export function useDropMessages(
+ waveId: string,
+ dropId: string | null,
+ wave?: ApiWave
+) {
const { isCapacitor } = useCapacitor();
const queryClient = useQueryClient();
const [init, setInit] = useState(false);
@@ -57,36 +64,19 @@ export function useDropMessages(waveId: string, dropId: string | null) {
pageParam,
}: {
pageParam: {
- serialNo: number | null;
- strategy: WaveDropsSearchStrategy;
+ page: number;
} | null;
- }) => {
- const params: Record = {
- limit: WAVE_DROPS_PARAMS.limit.toString(),
- drop_id: dropId ?? "",
- };
-
- if (pageParam?.serialNo) {
- params["serial_no_limit"] = `${pageParam.serialNo}`;
- params["search_strategy"] = `${pageParam.strategy}`;
- }
-
- const results = await commonApiFetch({
- endpoint: `waves/${waveId}/drops`,
- params,
- });
-
- return results;
- },
+ }) =>
+ fetchDropRepliesV2({
+ parentDropId: dropId ?? "",
+ page: pageParam?.page ?? 1,
+ pageSize: WAVE_DROPS_PARAMS.limit,
+ wave,
+ }),
enabled: !!dropId,
initialPageParam: null,
getNextPageParam: (lastPage) =>
- lastPage.drops.at(-1)?.serial_no
- ? {
- serialNo: lastPage.drops.at(-1)?.serial_no ?? null,
- strategy: WaveDropsSearchStrategy.Older,
- }
- : null,
+ lastPage.next ? { page: lastPage.page + 1 } : null,
placeholderData: keepPreviousData,
staleTime: 60000,
refetchOnWindowFocus: true,
@@ -102,7 +92,7 @@ export function useDropMessages(waveId: string, dropId: string | null) {
}, [hasNextPage, isFetchingNextPage, onFetchNextPage]);
const processDrops = (
- pages: ApiWaveDropsFeed[] | undefined,
+ pages: (ApiWaveDropsFeed | ApiWaveDropsV2PageFeed)[] | undefined,
previousDrops: ExtendedDrop[],
isReverse: boolean
) => {
diff --git a/hooks/useFavouriteWavesOfIdentity.ts b/hooks/useFavouriteWavesOfIdentity.ts
index 05f54736f0..a9ab8ea12a 100644
--- a/hooks/useFavouriteWavesOfIdentity.ts
+++ b/hooks/useFavouriteWavesOfIdentity.ts
@@ -3,8 +3,9 @@
import { useQuery } from "@tanstack/react-query";
import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
import { getDefaultQueryRetry } from "@/components/react-query-wrapper/utils/query-utils";
-import type { ApiWave } from "@/generated/models/ApiWave";
-import { commonApiFetch } from "@/services/api/common-api";
+import { ApiWavesV2ListType } from "@/generated/models/ApiWavesV2ListType";
+import { fetchWavesV2Page } from "@/services/api/waves-v2-api";
+import type { SidebarWave } from "@/types/waves.types";
interface UseFavouriteWavesOfIdentityProps {
readonly identityKey: string | null;
@@ -20,21 +21,21 @@ export function useFavouriteWavesOfIdentity({
const normalizedIdentityKey = identityKey?.trim() ?? null;
const activeIdentityKey = normalizedIdentityKey ?? "";
- const query = useQuery({
+ const query = useQuery({
queryKey: [
QueryKey.IDENTITY_FAVOURITE_WAVES,
{ identity_key: normalizedIdentityKey, limit },
],
- queryFn: async () =>
- await commonApiFetch({
- endpoint: `waves-overview/favourites-of-identity/${encodeURIComponent(
- activeIdentityKey
- )}`,
- params: {
- limit: `${limit}`,
- offset: "0",
- },
- }),
+ queryFn: async () => {
+ const page = await fetchWavesV2Page({
+ view: ApiWavesV2ListType.Favourites,
+ page: 1,
+ pageSize: limit,
+ identity: activeIdentityKey,
+ });
+
+ return page.waves;
+ },
enabled: enabled && activeIdentityKey.length > 0,
...getDefaultQueryRetry(),
});
diff --git a/hooks/useNotificationsQuery.tsx b/hooks/useNotificationsQuery.tsx
index 511288232e..2d5fbada8f 100644
--- a/hooks/useNotificationsQuery.tsx
+++ b/hooks/useNotificationsQuery.tsx
@@ -3,7 +3,7 @@
import { groupReactionNotifications } from "@/components/brain/notifications/utils/groupReactionNotifications";
import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
import type { ApiNotificationCause } from "@/generated/models/ApiNotificationCause";
-import { commonApiFetch } from "@/services/api/common-api";
+import { fetchNotificationsV2 } from "@/services/api/notifications-v2-api";
import type {
NotificationDisplayItem,
TypedNotificationsResponse,
@@ -60,6 +60,7 @@ const getIdentityNotificationsQueryKey = (
cause: cause?.length
? [...cause].sort((a, b) => a.localeCompare(b)).join(",")
: null,
+ version: "v2",
},
] as const;
@@ -69,19 +70,10 @@ const fetchNotifications = async ({
pageParam,
signal,
}: NotificationsQueryParams) => {
- const params: Record = { limit };
-
- if (pageParam != null) {
- params["id_less_than"] = String(pageParam);
- }
-
- if (cause?.length) {
- params["cause"] = cause.join(",");
- }
-
- return await commonApiFetch({
- endpoint: "notifications",
- params,
+ return await fetchNotificationsV2({
+ limit,
+ cause,
+ pageParam,
signal,
});
};
diff --git a/hooks/usePinnedWavesServer.ts b/hooks/usePinnedWavesServer.ts
index c00d709a3e..fe9423f8d4 100644
--- a/hooks/usePinnedWavesServer.ts
+++ b/hooks/usePinnedWavesServer.ts
@@ -1,27 +1,27 @@
"use client";
+import { useCallback, useEffect, useMemo, useRef, type RefObject } from "react";
import {
- useCallback,
- useContext,
- useEffect,
- useMemo,
- useRef,
- type RefObject,
-} from "react";
-import {
+ type InfiniteData,
useMutation,
useQuery,
useQueryClient,
type QueryClient,
type QueryObserverResult,
} from "@tanstack/react-query";
-import { AuthContext } from "@/components/auth/Auth";
+import { useAuth } from "@/components/auth/Auth";
import { useSeizeConnectContext } from "@/components/auth/SeizeConnectContext";
import { useSeizeSettingsOptional } from "@/contexts/SeizeSettingsContext";
import { pinnedWavesApi } from "@/services/api/pinned-waves-api";
-import type { ApiWave } from "@/generated/models/ApiWave";
import { ApiWavesPinFilter } from "@/generated/models/ApiWavesPinFilter";
+import { ApiWavesOverviewType } from "@/generated/models/ApiWavesOverviewType";
import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
+import {
+ fetchWavesV2Page,
+ getWavesV2OverviewQueryKeyParams,
+ type WavesV2OverviewQueryKeyParams,
+} from "@/services/api/waves-v2-api";
+import type { SidebarWave, SidebarWavesPage } from "@/types/waves.types";
export const MAX_PINNED_WAVES = 20;
@@ -29,34 +29,26 @@ export const MAX_PINNED_WAVES = 20;
const PINNED_WAVES_STALE_TIME = 5 * 60 * 1000; // 5 minutes
const PINNED_WAVES_GC_TIME = 10 * 60 * 1000; // 10 minutes
const PINNED_WAVES_REFETCH_INTERVAL = 2 * 60 * 1000; // 2 minutes
-
-// Type definitions for React Query data structures
-interface InfiniteQueryData {
- pages: T[][];
- pageParams: unknown[];
-}
+const PINNED_WAVES_PAGE_SIZE = MAX_PINNED_WAVES;
type PinnedWavesQueryKey = readonly [
- QueryKey.WAVES_OVERVIEW,
- {
- readonly pinned: ApiWavesPinFilter.Pinned;
- readonly viewer_identity?: string;
- },
+ QueryKey.WAVES_V2,
+ WavesV2OverviewQueryKeyParams,
];
interface MutationContext {
- previousPinnedWaves: ApiWave[] | undefined;
+ previousPinnedWaves: SidebarWave[] | undefined;
}
interface UsePinnedWavesServerReturn {
- pinnedWaves: ApiWave[];
+ pinnedWaves: SidebarWave[];
pinnedIds: string[];
isLoading: boolean;
isError: boolean;
error: Error | null;
pinWave: (waveId: string) => Promise;
unpinWave: (waveId: string) => Promise;
- refetch: () => Promise>;
+ refetch: () => Promise>;
isOperationInProgress: (waveId: string) => boolean;
canPinWave: (waveId: string) => boolean;
}
@@ -65,11 +57,13 @@ function createPinnedWavesQueryKey(
viewerIdentityKey: string | null
): PinnedWavesQueryKey {
return [
- QueryKey.WAVES_OVERVIEW,
- {
+ QueryKey.WAVES_V2,
+ getWavesV2OverviewQueryKeyParams({
+ overviewType: ApiWavesOverviewType.MostSubscribed,
+ pageSize: PINNED_WAVES_PAGE_SIZE,
pinned: ApiWavesPinFilter.Pinned,
- ...(viewerIdentityKey ? { viewer_identity: viewerIdentityKey } : {}),
- },
+ viewerIdentityKey,
+ }),
] as const;
}
@@ -87,9 +81,18 @@ function usePinnedWavesQuery(
queryKey: PinnedWavesQueryKey,
isAuthenticated: boolean
) {
- const query = useQuery({
+ const query = useQuery({
queryKey,
- queryFn: pinnedWavesApi.fetchPinnedWaves,
+ queryFn: async () => {
+ const page = await fetchWavesV2Page({
+ page: 1,
+ pageSize: PINNED_WAVES_PAGE_SIZE,
+ overviewType: ApiWavesOverviewType.MostSubscribed,
+ pinned: ApiWavesPinFilter.Pinned,
+ });
+
+ return page.waves;
+ },
enabled: isAuthenticated,
staleTime: PINNED_WAVES_STALE_TIME,
gcTime: PINNED_WAVES_GC_TIME,
@@ -107,7 +110,7 @@ function usePinnedWavesQuery(
}
function usePinnedWavesBudget(
- pinnedWaves: ApiWave[],
+ pinnedWaves: SidebarWave[],
ongoingOperations: RefObject>
) {
const seizeSettings = useSeizeSettingsOptional();
@@ -161,7 +164,7 @@ function isMainWavesQueryForViewer(
): boolean {
const [key, params] = queryKey;
if (
- key !== QueryKey.WAVES_OVERVIEW ||
+ key !== QueryKey.WAVES_V2 ||
typeof params !== "object" ||
params === null
) {
@@ -193,27 +196,32 @@ function useInvalidateWavesQueries(
queryKey: pinnedWavesQueryKey,
});
void queryClient.invalidateQueries({
- queryKey: [QueryKey.WAVES_OVERVIEW],
+ queryKey: [QueryKey.WAVES_V2],
predicate: (query) =>
isMainWavesQueryForViewer(query.queryKey, viewerIdentityKey),
});
+ void queryClient.invalidateQueries({
+ queryKey: [QueryKey.WAVES_OVERVIEW],
+ });
}, [queryClient, pinnedWavesQueryKey, viewerIdentityKey]);
}
function findWaveInQueryData(
- data: ApiWave[] | InfiniteQueryData | undefined,
+ data: SidebarWave[] | InfiniteData | undefined,
waveId: string
-): ApiWave | undefined {
+): SidebarWave | undefined {
if (!data) {
return undefined;
}
if (Array.isArray(data)) {
- return data.find((wave): wave is ApiWave => wave.id === waveId);
+ return data.find((wave): wave is SidebarWave => wave.id === waveId);
}
for (const page of data.pages) {
- const wave = page.find((item): item is ApiWave => item.id === waveId);
+ const wave = page.waves.find(
+ (item): item is SidebarWave => item.id === waveId
+ );
if (wave) {
return wave;
}
@@ -225,11 +233,11 @@ function findWaveInQueryData(
function findWaveForOptimisticPin(
queryClient: QueryClient,
waveId: string
-): ApiWave | undefined {
+): SidebarWave | undefined {
const wavesQueries = queryClient.getQueriesData<
- ApiWave[] | InfiniteQueryData
+ SidebarWave[] | InfiniteData
>({
- queryKey: [QueryKey.WAVES_OVERVIEW],
+ queryKey: [QueryKey.WAVES_V2],
});
for (const [, data] of wavesQueries) {
@@ -243,10 +251,10 @@ function findWaveForOptimisticPin(
}
function createOptimisticPinnedWaves(
- previousPinnedWaves: ApiWave[] | undefined,
- waveToPin: ApiWave | undefined,
+ previousPinnedWaves: SidebarWave[] | undefined,
+ waveToPin: SidebarWave | undefined,
waveId: string
-): ApiWave[] | undefined {
+): SidebarWave[] | undefined {
if (!waveToPin || !previousPinnedWaves) {
return undefined;
}
@@ -268,7 +276,7 @@ async function optimisticallyPinWave(
): Promise {
await queryClient.cancelQueries({ queryKey });
- const previousPinnedWaves = queryClient.getQueryData(queryKey);
+ const previousPinnedWaves = queryClient.getQueryData(queryKey);
const waveToPin = findWaveForOptimisticPin(queryClient, waveId);
const optimisticPinnedWaves = createOptimisticPinnedWaves(
previousPinnedWaves,
@@ -290,7 +298,7 @@ async function optimisticallyUnpinWave(
): Promise {
await queryClient.cancelQueries({ queryKey });
- const previousPinnedWaves = queryClient.getQueryData(queryKey);
+ const previousPinnedWaves = queryClient.getQueryData(queryKey);
if (previousPinnedWaves) {
queryClient.setQueryData(
@@ -349,12 +357,24 @@ function usePinnedWaveMutations(
}
export function usePinnedWavesServer(): UsePinnedWavesServerReturn {
- const { connectedProfile, activeProfileProxy } = useContext(AuthContext);
+ const { connectedProfile, activeProfileProxy } = useAuth();
const { address } = useSeizeConnectContext();
const queryClient = useQueryClient();
const ongoingOperations = useRef>(new Set());
const isAuthenticated = !!connectedProfile?.handle && !activeProfileProxy;
- const viewerIdentityKey = address?.toLowerCase() ?? null;
+ const activeProfileProxyId = activeProfileProxy?.id ?? null;
+ const viewerIdentityKey = useMemo(() => {
+ if (!address) {
+ return null;
+ }
+
+ const normalizedAddress = address.toLowerCase();
+ if (activeProfileProxyId !== null) {
+ return `${normalizedAddress}:proxy:${activeProfileProxyId}`;
+ }
+
+ return `${normalizedAddress}:primary`;
+ }, [address, activeProfileProxyId]);
const pinnedWavesQueryKey = usePinnedWavesQueryKey(viewerIdentityKey);
const { data, isLoading, isError, error, refetch } = usePinnedWavesQuery(
queryClient,
diff --git a/hooks/useUnreadNotifications.ts b/hooks/useUnreadNotifications.ts
index 73ec8c4dbc..d3b530ef45 100644
--- a/hooks/useUnreadNotifications.ts
+++ b/hooks/useUnreadNotifications.ts
@@ -2,7 +2,7 @@
import { useQuery } from "@tanstack/react-query";
import { useState, useEffect } from "react";
-import type { ApiNotificationsResponse } from "@/generated/models/ApiNotificationsResponse";
+import type { ApiNotificationsResponseV2 } from "@/generated/models/ApiNotificationsResponseV2";
import { commonApiFetch } from "@/services/api/common-api";
import useCapacitor from "./useCapacitor";
import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
@@ -10,14 +10,14 @@ import { getDefaultQueryRetry } from "@/components/react-query-wrapper/utils/que
export function useUnreadNotifications(handle: string | null) {
const { isCapacitor } = useCapacitor();
- const { data: notifications } = useQuery({
+ const { data: notifications } = useQuery({
queryKey: [
QueryKey.IDENTITY_NOTIFICATIONS,
- { identity: handle, limit: "1" },
+ { identity: handle, limit: "1", version: "v2" },
],
queryFn: async () =>
- await commonApiFetch({
- endpoint: `notifications`,
+ await commonApiFetch({
+ endpoint: `v2/notifications`,
params: {
limit: "1",
},
diff --git a/hooks/useVirtualizedWaveDrops.ts b/hooks/useVirtualizedWaveDrops.ts
index d48de3e810..ce8d8cd9c3 100644
--- a/hooks/useVirtualizedWaveDrops.ts
+++ b/hooks/useVirtualizedWaveDrops.ts
@@ -5,6 +5,7 @@ import { useVirtualizedWaveMessages } from "./useVirtualizedWaveMessages";
import { useMyStream } from "@/contexts/wave/MyStreamContext";
import type { NextPageProps } from "@/contexts/wave/hooks/useWavePagination";
import { DropSize } from "@/helpers/waves/drop.helpers";
+import type { ApiWave } from "@/generated/models/ApiWave";
/**
* Hook that adapts the useVirtualizedWaveMessages hook to match the
@@ -17,6 +18,7 @@ import { DropSize } from "@/helpers/waves/drop.helpers";
export function useVirtualizedWaveDrops(
waveId: string,
dropId: string | null,
+ wave?: ApiWave,
pageSize: number = 50
) {
// Original implementation - would be imported from useMyStream
@@ -26,7 +28,8 @@ export function useVirtualizedWaveDrops(
const virtualizedWaveMessages = useVirtualizedWaveMessages(
waveId,
dropId,
- pageSize
+ pageSize,
+ wave
);
// Create a wrapper for fetchNextPageForWave that first tries to get data locally
const fetchNextPageForWave = useCallback(
diff --git a/hooks/useVirtualizedWaveMessages.ts b/hooks/useVirtualizedWaveMessages.ts
index e47f5bb5d8..1cea23af20 100644
--- a/hooks/useVirtualizedWaveMessages.ts
+++ b/hooks/useVirtualizedWaveMessages.ts
@@ -6,6 +6,7 @@ import { useMyStreamWaveMessages } from "@/contexts/wave/MyStreamContext";
import type { Drop } from "@/helpers/waves/drop.helpers";
import type { WaveMessages } from "@/contexts/wave/hooks/types";
import { useDropMessages } from "./useDropMessages";
+import type { ApiWave } from "@/generated/models/ApiWave";
interface VirtualizedWaveMessages extends Omit {
readonly drops: Drop[];
@@ -142,10 +143,11 @@ const getNextVirtualLimitAfterAppend = ({
export function useVirtualizedWaveMessages(
waveId: string,
dropId: string | null,
- pageSize: number = 50
+ pageSize: number = 50,
+ wave?: ApiWave
): VirtualizedWaveMessages | undefined {
const fullWaveMessages = useMyStreamWaveMessages(waveId);
- const fullWaveMessagesForDrop = useDropMessages(waveId, dropId);
+ const fullWaveMessagesForDrop = useDropMessages(waveId, dropId, wave);
const fetchNextPageForDrop = fullWaveMessagesForDrop.fetchNextPage;
const scopeKey = getScopeKey({ waveId, dropId, pageSize });
diff --git a/hooks/useWaveActivityLogs.ts b/hooks/useWaveActivityLogs.ts
index 0b7579944e..a2c9944b8e 100644
--- a/hooks/useWaveActivityLogs.ts
+++ b/hooks/useWaveActivityLogs.ts
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import {
keepPreviousData,
useInfiniteQuery,
@@ -20,6 +20,7 @@ interface UseWaveActivityLogsProps {
readonly reverse: boolean;
readonly dropId: string | null;
readonly logTypes: string[];
+ readonly enabled?: boolean | undefined;
}
export function useWaveActivityLogs({
@@ -28,22 +29,32 @@ export function useWaveActivityLogs({
reverse,
dropId,
logTypes,
+ enabled = true,
}: UseWaveActivityLogsProps) {
const queryClient = useQueryClient();
const [logs, setLogs] = useState([]);
+ const canFetch = enabled && !!connectedProfileHandle;
+ const serializedLogTypes = logTypes.join(",");
- const queryKey = [
- QueryKey.WAVE_LOGS,
- {
- waveId,
- limit: WAVE_LOGS_PARAMS.limit,
- dropId,
- logTypes,
- },
- ];
+ const queryKey = useMemo(
+ () => [
+ QueryKey.WAVE_LOGS,
+ {
+ waveId,
+ limit: WAVE_LOGS_PARAMS.limit,
+ dropId,
+ logTypes: serializedLogTypes,
+ },
+ ],
+ [waveId, dropId, serializedLogTypes]
+ );
useEffect(() => {
+ if (!canFetch) {
+ return;
+ }
+
queryClient.prefetchInfiniteQuery({
queryKey,
queryFn: async ({ pageParam }: { pageParam: number | null }) => {
@@ -53,8 +64,8 @@ export function useWaveActivityLogs({
if (dropId) {
params["drop_id"] = dropId;
}
- if (logTypes) {
- params["log_types"] = logTypes.join(",");
+ if (serializedLogTypes) {
+ params["log_types"] = serializedLogTypes;
}
if (pageParam) {
params["offset"] = `${pageParam}`;
@@ -73,7 +84,7 @@ export function useWaveActivityLogs({
staleTime: 60000,
...getDefaultQueryRetry(),
});
- }, [waveId]);
+ }, [canFetch, queryKey, waveId, dropId, serializedLogTypes, queryClient]);
const { data, fetchNextPage, hasNextPage, isLoading, isFetchingNextPage } =
useInfiniteQuery({
@@ -85,8 +96,8 @@ export function useWaveActivityLogs({
if (dropId) {
params["drop_id"] = dropId;
}
- if (logTypes) {
- params["log_types"] = logTypes.join(",");
+ if (serializedLogTypes) {
+ params["log_types"] = serializedLogTypes;
}
if (pageParam !== null) {
params["offset"] = `${pageParam}`;
@@ -105,7 +116,7 @@ export function useWaveActivityLogs({
? allPages.length * WAVE_LOGS_PARAMS.limit
: null,
placeholderData: keepPreviousData,
- enabled: !!connectedProfileHandle,
+ enabled: canFetch,
staleTime: 60000,
refetchInterval: 30000,
...getDefaultQueryRetry(),
diff --git a/hooks/useWaveBoostedDrops.ts b/hooks/useWaveBoostedDrops.ts
index d5b2d9499e..f7ed550560 100644
--- a/hooks/useWaveBoostedDrops.ts
+++ b/hooks/useWaveBoostedDrops.ts
@@ -4,11 +4,14 @@ import { useQuery } from "@tanstack/react-query";
import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
import { commonApiFetch } from "@/services/api/common-api";
import type { ApiDrop } from "@/generated/models/ApiDrop";
+import type { ApiWave } from "@/generated/models/ApiWave";
import type { Page } from "@/helpers/Types";
import { TimeWindow, TIME_WINDOW_MS } from "@/types/boosted-drops.types";
+import { fetchBoostedDropsV2 } from "@/services/api/wave-drops-v2-api";
interface UseWaveBoostedDropsProps {
readonly waveId: string;
+ readonly wave?: ApiWave | undefined;
readonly limit?: number;
readonly timeWindow?: TimeWindow;
}
@@ -19,6 +22,7 @@ const REFETCH_INTERVAL = 30000; // 30 seconds
export function useWaveBoostedDrops({
waveId,
+ wave,
limit = DEFAULT_LIMIT,
timeWindow = TimeWindow.DAY,
}: UseWaveBoostedDropsProps) {
@@ -26,6 +30,15 @@ export function useWaveBoostedDrops({
queryKey: [QueryKey.BOOSTED_DROPS, { waveId, limit, timeWindow }],
queryFn: async () => {
const countOnlyBoostsAfter = Date.now() - TIME_WINDOW_MS[timeWindow];
+ if (wave) {
+ return await fetchBoostedDropsV2({
+ waveId,
+ wave,
+ limit,
+ countOnlyBoostsAfter,
+ });
+ }
+
const response = await commonApiFetch>({
endpoint: "boosted-drops",
params: {
diff --git a/hooks/useWaveCurationDrops.ts b/hooks/useWaveCurationDrops.ts
index 30dce186b2..f4bfdb8deb 100644
--- a/hooks/useWaveCurationDrops.ts
+++ b/hooks/useWaveCurationDrops.ts
@@ -6,7 +6,6 @@ import {
updateDropInCachedDrops,
} from "@/components/react-query-wrapper/utils/updateAttachmentInCachedDrops";
import type { ApiAttachment } from "@/generated/models/ApiAttachment";
-import type { ApiCurationDropsPage } from "@/generated/models/ApiCurationDropsPage";
import type { ApiDropWithoutWave } from "@/generated/models/ApiDropWithoutWave";
import type { ApiWave } from "@/generated/models/ApiWave";
import type { WsDropUpdateMessage } from "@/helpers/Types";
@@ -17,7 +16,7 @@ import {
generateUniqueKeys,
mapToExtendedDrops,
} from "@/helpers/waves/wave-drops.helpers";
-import { commonApiFetch } from "@/services/api/common-api";
+import { fetchWaveCurationDropsV2 } from "@/services/api/wave-curation-drops-v2-api";
import { useWebSocketMessage } from "@/services/websocket/useWebSocketMessage";
import {
keepPreviousData,
@@ -72,18 +71,17 @@ export function useWaveCurationDrops({
} = useInfiniteQuery({
queryKey,
queryFn: async ({ pageParam }: { pageParam: number }) => {
- if (!waveId || !normalizedCurationId) {
+ if (!wave || !normalizedCurationId) {
throw new Error(
"Wave and curation are required to load curation drops"
);
}
- return await commonApiFetch({
- endpoint: `waves/${waveId}/curations/${normalizedCurationId}/drops`,
- params: {
- page: String(pageParam),
- page_size: String(pageSize),
- },
+ return await fetchWaveCurationDropsV2({
+ wave,
+ curationId: normalizedCurationId,
+ page: pageParam,
+ pageSize,
});
},
enabled: enabled && !!waveId && normalizedCurationId.length > 0,
diff --git a/hooks/useWaveDrops.ts b/hooks/useWaveDrops.ts
index 53d8bb98fd..327fcf3b86 100644
--- a/hooks/useWaveDrops.ts
+++ b/hooks/useWaveDrops.ts
@@ -13,11 +13,12 @@ import {
} from "@/components/react-query-wrapper/utils/updateAttachmentInCachedDrops";
import type { ApiAttachment } from "@/generated/models/ApiAttachment";
import type { ApiDrop } from "@/generated/models/ApiDrop";
+import { ApiDropSearchStrategy } from "@/generated/models/ApiDropSearchStrategy";
import type { ApiDropType } from "@/generated/models/ApiDropType";
import { DropSize, type ExtendedDrop } from "@/helpers/waves/drop.helpers";
import type { WsDropUpdateMessage } from "@/helpers/Types";
import { WsMessageType } from "@/helpers/Types";
-import { commonApiFetch } from "@/services/api/common-api";
+import { fetchWaveDropsFeedV2 } from "@/services/api/wave-drops-v2-api";
import { useWebSocketMessage } from "@/services/websocket/useWebSocketMessage";
import { useDebouncedQueryRefetch } from "./useDebouncedQueryRefetch";
@@ -32,12 +33,20 @@ interface UseWaveDropsProps {
readonly enabled?: boolean | undefined;
}
-const processDrops = (pages: ApiDrop[][] | undefined): ExtendedDrop[] => {
+interface WaveDropsPage {
+ readonly drops: ApiDrop[];
+ readonly nextSerialNo?: number | undefined;
+}
+
+const hasMedia = (drop: ApiDrop): boolean =>
+ drop.parts.some((part) => part.media.length > 0);
+
+const processDrops = (pages: WaveDropsPage[] | undefined): ExtendedDrop[] => {
if (!pages) {
return [];
}
- const allDrops = pages.flat();
+ const allDrops = pages.flatMap((page) => page.drops);
const extendedDrops: ExtendedDrop[] = allDrops.map((drop) => ({
...drop,
type: DropSize.FULL,
@@ -97,35 +106,50 @@ export function useWaveDrops({
} = useInfiniteQuery({
queryKey,
queryFn: async ({ pageParam }: { pageParam: number | null }) => {
- const params: Record = {
- wave_id: waveId,
- limit: limit.toString(),
- };
+ const collectedDrops: ApiDrop[] = [];
+ let nextSerialNo: number | undefined;
+ let serialNoLimit = pageParam;
- if (containsMedia) {
- params["contains_media"] = "true";
- }
-
- if (dropType !== undefined) {
- params["drop_type"] = dropType;
- }
+ for (let shouldFetch = true; shouldFetch; ) {
+ const feed = await fetchWaveDropsFeedV2({
+ waveId,
+ limit,
+ serialNoLimit,
+ searchStrategy:
+ typeof serialNoLimit === "number"
+ ? ApiDropSearchStrategy.Older
+ : undefined,
+ dropType,
+ });
+ const pageDrops = feed.drops as ApiDrop[];
+
+ if (pageDrops.length === 0) {
+ return { drops: collectedDrops };
+ }
- if (curationId) {
- params["curation_id"] = curationId;
- }
+ nextSerialNo = pageDrops.at(-1)?.serial_no;
+ collectedDrops.push(
+ ...(containsMedia ? pageDrops.filter(hasMedia) : pageDrops)
+ );
+
+ if (
+ !containsMedia ||
+ collectedDrops.length > 0 ||
+ nextSerialNo === undefined ||
+ nextSerialNo === serialNoLimit
+ ) {
+ shouldFetch = false;
+ continue;
+ }
- if (typeof pageParam === "number") {
- params["serial_no_less_than"] = `${pageParam}`;
+ serialNoLimit = nextSerialNo;
}
- return await commonApiFetch({
- endpoint: "drops",
- params,
- });
+ return { drops: collectedDrops, nextSerialNo };
},
enabled: enabled && !!waveId,
initialPageParam: null,
- getNextPageParam: (lastPage) => lastPage.at(-1)?.serial_no ?? undefined,
+ getNextPageParam: (lastPage) => lastPage.nextSerialNo,
placeholderData: keepPreviousData,
staleTime: 60000,
refetchOnWindowFocus: true,
diff --git a/hooks/useWaveDropsLeaderboard.ts b/hooks/useWaveDropsLeaderboard.ts
index d73d982438..3fc53cd417 100644
--- a/hooks/useWaveDropsLeaderboard.ts
+++ b/hooks/useWaveDropsLeaderboard.ts
@@ -10,7 +10,7 @@ import {
generateUniqueKeys,
mapToExtendedDrops,
} from "@/helpers/waves/wave-drops.helpers";
-import { commonApiFetch } from "@/services/api/common-api";
+import { fetchWaveLeaderboardV2 } from "@/services/api/wave-drops-v2-api";
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useMemo } from "react";
@@ -185,8 +185,8 @@ export function useWaveDropsLeaderboard({
readonly targetSort: WaveDropsLeaderboardSort;
readonly targetSortDirection: "ASC" | "DESC" | undefined;
}) =>
- await commonApiFetch({
- endpoint: `waves/${waveId}/leaderboard`,
+ await fetchWaveLeaderboardV2({
+ waveId,
params: buildLeaderboardParams({
pageParam,
pageSize,
diff --git a/hooks/useWaveDropsSearch.ts b/hooks/useWaveDropsSearch.ts
index 562c67d080..8476549e03 100644
--- a/hooks/useWaveDropsSearch.ts
+++ b/hooks/useWaveDropsSearch.ts
@@ -1,7 +1,6 @@
"use client";
import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
-import type { ApiDropWithoutWavesPageWithoutCount } from "@/generated/models/ApiDropWithoutWavesPageWithoutCount";
import type { ApiWave } from "@/generated/models/ApiWave";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import { toApiWaveMin } from "@/helpers/waves/wave.helpers";
@@ -9,7 +8,7 @@ import {
generateUniqueKeys,
mapToExtendedDrops,
} from "@/helpers/waves/wave-drops.helpers";
-import { commonApiFetch } from "@/services/api/common-api";
+import { fetchWaveDropsSearchV2 } from "@/services/api/wave-drops-v2-api";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useMemo } from "react";
@@ -39,14 +38,16 @@ export function useWaveDropsSearch({
],
enabled: enabled && wave !== null && trimmedTerm.length > 0,
initialPageParam: 1,
- queryFn: async ({ pageParam }) => {
- return await commonApiFetch({
- endpoint: `waves/${wave!.id}/search`,
- params: {
- term: trimmedTerm,
- page: String(pageParam),
- size: String(size),
- },
+ queryFn: async ({ pageParam }: { pageParam: number }) => {
+ if (!wave) {
+ throw new Error("Wave is required to search drops");
+ }
+
+ return await fetchWaveDropsSearchV2({
+ wave,
+ term: trimmedTerm,
+ page: pageParam,
+ size,
});
},
getNextPageParam: (lastPage) =>
diff --git a/hooks/useWaveTopVoters.ts b/hooks/useWaveTopVoters.ts
index e8c4e0aca7..2e2cdfb5e1 100644
--- a/hooks/useWaveTopVoters.ts
+++ b/hooks/useWaveTopVoters.ts
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import {
keepPreviousData,
useInfiniteQuery,
@@ -20,6 +20,7 @@ interface UseWaveTopVotersProps {
readonly sortDirection?: "ASC" | "DESC" | undefined;
readonly sort?: "ABSOLUTE" | "POSITIVE" | "NEGATIVE" | undefined;
readonly refetchInterval?: number | undefined;
+ readonly enabled?: boolean | undefined;
}
export function useWaveTopVoters({
@@ -30,21 +31,30 @@ export function useWaveTopVoters({
sortDirection = "ASC",
sort = "ABSOLUTE",
refetchInterval = Infinity,
+ enabled = true,
}: UseWaveTopVotersProps) {
const queryClient = useQueryClient();
const [voters, setVoters] = useState([]);
+ const canFetch = enabled && !!connectedProfileHandle;
- const queryKey = [
- QueryKey.WAVE_VOTERS,
- {
- waveId,
- dropId,
- sortDirection,
- sort,
- },
- ];
+ const queryKey = useMemo(
+ () => [
+ QueryKey.WAVE_VOTERS,
+ {
+ waveId,
+ dropId,
+ sortDirection,
+ sort,
+ },
+ ],
+ [waveId, dropId, sortDirection, sort]
+ );
useEffect(() => {
+ if (!canFetch) {
+ return;
+ }
+
queryClient.prefetchInfiniteQuery({
queryKey,
queryFn: async ({ pageParam }: { pageParam: number | null }) => {
@@ -79,7 +89,7 @@ export function useWaveTopVoters({
staleTime: 60000,
...getDefaultQueryRetry(),
});
- }, [waveId, dropId, sortDirection, sort]);
+ }, [canFetch, queryKey, queryClient, waveId, dropId, sortDirection, sort]);
const {
data,
@@ -118,7 +128,7 @@ export function useWaveTopVoters({
return currentPage + 1;
},
placeholderData: keepPreviousData,
- enabled: !!connectedProfileHandle,
+ enabled: canFetch,
staleTime: 60000,
refetchInterval,
...getDefaultQueryRetry(),
diff --git a/hooks/useWavesList.ts b/hooks/useWavesList.ts
index bb821a1252..53d531f8cc 100644
--- a/hooks/useWavesList.ts
+++ b/hooks/useWavesList.ts
@@ -1,32 +1,29 @@
"use client";
-import { useContext, useMemo, useCallback } from "react";
-import { AuthContext } from "@/components/auth/Auth";
+import { useMemo, useCallback } from "react";
+import { useAuth } from "@/components/auth/Auth";
import { useSeizeConnectContext } from "@/components/auth/SeizeConnectContext";
import { useSeizeSettings } from "@/contexts/SeizeSettingsContext";
import { normalizeOptionalWaveId } from "@/helpers/waves/wave.helpers";
-import { useWavesOverview } from "./useWavesOverview";
+import { mapApiWaveToSidebarWave, useWavesV2 } from "./useWavesV2";
import {
SIDEBAR_WAVES_OVERVIEW_REFETCH_INTERVAL_MS,
WAVE_FOLLOWING_WAVES_PARAMS,
} from "@/components/react-query-wrapper/utils/query-utils";
import { usePinnedWavesServer } from "./usePinnedWavesServer";
import { useWaveById } from "./useWaveById";
-import type { ApiWave } from "@/generated/models/ApiWave";
import { useShowFollowingWaves } from "./useShowFollowingWaves";
-import { ApiWaveType } from "@/generated/models/ApiWaveType";
+import type { SidebarWave } from "@/types/waves.types";
// Enhanced wave interface with isPinned field and newDropsCount
-interface EnhancedWave extends ApiWave {
- isPinned: boolean;
-}
+type EnhancedWave = SidebarWave & { readonly isPinned: boolean };
/**
* Hook for managing and fetching waves list including pinned waves
* @returns Wave list data and loading states
*/
const useWavesList = () => {
- const { connectedProfile, activeProfileProxy } = useContext(AuthContext);
+ const { connectedProfile, activeProfileProxy } = useAuth();
const { address } = useSeizeConnectContext();
const { seizeSettings, isAnnouncementsWave } = useSeizeSettings();
const {
@@ -69,10 +66,11 @@ const useWavesList = () => {
fetchNextPage,
status: mainWavesStatus,
refetch: mainWavesRefetch,
- } = useWavesOverview({
- type: WAVE_FOLLOWING_WAVES_PARAMS.initialWavesOverviewType,
- limit: WAVE_FOLLOWING_WAVES_PARAMS.limit,
+ } = useWavesV2({
+ overviewType: WAVE_FOLLOWING_WAVES_PARAMS.initialWavesOverviewType,
+ pageSize: WAVE_FOLLOWING_WAVES_PARAMS.limit,
following: isConnectedIdentity && following,
+ directMessage: false,
viewerIdentityKey,
refetchInterval: SIDEBAR_WAVES_OVERVIEW_REFETCH_INTERVAL_MS,
refetchIntervalInBackground: false,
@@ -101,17 +99,25 @@ const useWavesList = () => {
const announcementQueryError = shouldFetchAnnouncementWave
? rawAnnouncementQueryError
: null;
+ const fetchedAnnouncementSidebarWave = useMemo(
+ () =>
+ fetchedAnnouncementWave
+ ? mapApiWaveToSidebarWave(fetchedAnnouncementWave)
+ : null,
+ [fetchedAnnouncementWave]
+ );
const announcementWave = useMemo(() => {
- const resolvedWave = trackedAnnouncementWave ?? fetchedAnnouncementWave;
- if (!resolvedWave || waveIsDm(resolvedWave)) {
+ const resolvedWave =
+ trackedAnnouncementWave ?? fetchedAnnouncementSidebarWave;
+ if (!resolvedWave || resolvedWave.isDirectMessage) {
return null;
}
return resolvedWave;
- }, [trackedAnnouncementWave, fetchedAnnouncementWave]);
+ }, [trackedAnnouncementWave, fetchedAnnouncementSidebarWave]);
// Create a map of mainWaves by ID for easy lookup
const mainWavesMap = useMemo(() => {
- const map = new Map();
+ const map = new Map();
mainWaves.forEach((wave) => {
if (!isAnnouncementsWave(wave.id)) {
map.set(wave.id, wave);
@@ -157,7 +163,7 @@ const useWavesList = () => {
// Add all server-provided pinned waves, filtering out DMs
serverPinnedWaves.forEach((wave) => {
- if (!waveIsDm(wave) && !isAnnouncementsWave(wave.id)) {
+ if (!wave.isDirectMessage && !isAnnouncementsWave(wave.id)) {
result.push({ ...wave, isPinned: true });
}
});
@@ -175,7 +181,7 @@ const useWavesList = () => {
const allWavesArray: EnhancedWave[] = [];
[...mainWaves, ...separatelyFetchedPinnedWaves].forEach((wave) => {
- if (waveIsDm(wave) || isAnnouncementsWave(wave.id)) {
+ if (wave.isDirectMessage || isAnnouncementsWave(wave.id)) {
return;
}
@@ -186,8 +192,7 @@ const useWavesList = () => {
});
const sortedNonAnnouncementWaves = [...allWavesMap.values()].sort(
- (a, b) =>
- b.metrics.latest_drop_timestamp - a.metrics.latest_drop_timestamp
+ (a, b) => (b.latestDropTimestamp ?? 0) - (a.latestDropTimestamp ?? 0)
);
if (announcementWave) {
@@ -282,8 +287,4 @@ const useWavesList = () => {
);
};
-const waveIsDm = (w: ApiWave) =>
- w.wave.type === ApiWaveType.Chat &&
- (w.chat as any)?.scope?.group?.is_direct_message === true;
-
export default useWavesList;
diff --git a/hooks/useWavesOverview.ts b/hooks/useWavesOverview.ts
deleted file mode 100644
index 2bed810c5c..0000000000
--- a/hooks/useWavesOverview.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-"use client";
-
-import { useCallback, useEffect, useMemo, useState } from "react";
-import { useInfiniteQuery } from "@tanstack/react-query";
-
-import type { WavesOverviewParams } from "@/types/waves.types";
-import type { ApiWavesOverviewType } from "@/generated/models/ApiWavesOverviewType";
-import { commonApiFetch } from "@/services/api/common-api";
-import type { ApiWave } from "@/generated/models/ApiWave";
-import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
-import { getDefaultQueryRetry } from "@/components/react-query-wrapper/utils/query-utils";
-
-interface UseWavesOverviewProps {
- readonly type: ApiWavesOverviewType;
- readonly limit?: number | undefined;
- readonly following?: boolean | undefined;
- readonly viewerIdentityKey?: string | null | undefined;
- /**
- * If true, fetch only direct message waves. If false, exclude them. Undefined -> no filter.
- */
- readonly directMessage?: boolean | undefined;
- readonly refetchInterval?: number | undefined;
- readonly refetchIntervalInBackground?: boolean | undefined;
-}
-
-export const useWavesOverview = ({
- type,
- limit = 20,
- following = false,
- viewerIdentityKey,
- directMessage,
- refetchInterval = Infinity,
- refetchIntervalInBackground = false,
-}: UseWavesOverviewProps) => {
- const params: Omit = {
- limit,
- type,
- only_waves_followed_by_authenticated_user: following,
- ...(directMessage !== undefined ? { direct_message: directMessage } : {}),
- };
- const normalizedViewerIdentityKey =
- viewerIdentityKey?.trim().toLowerCase() ?? null;
- const queryKeyParams = useMemo(() => {
- if (!normalizedViewerIdentityKey) {
- return params;
- }
-
- return {
- ...params,
- viewer_identity: normalizedViewerIdentityKey,
- };
- }, [normalizedViewerIdentityKey, params]);
-
- const [lastErrorTimestamp, setLastErrorTimestamp] = useState(
- null
- );
-
- const query = useInfiniteQuery({
- queryKey: [QueryKey.WAVES_OVERVIEW, queryKeyParams],
- queryFn: async ({ pageParam }: { pageParam: number }) => {
- const queryParams: Record = {
- limit: `${params.limit}`,
- offset: `${pageParam}`,
- type: params.type,
- only_waves_followed_by_authenticated_user: `${params.only_waves_followed_by_authenticated_user}`,
- };
-
- if (params.direct_message !== undefined) {
- queryParams["direct_message"] = `${params.direct_message}`;
- }
-
- return await commonApiFetch({
- endpoint: `waves-overview`,
- params: queryParams,
- });
- },
- initialPageParam: 0,
- getNextPageParam: (_, allPages) =>
- allPages.at(-1)?.length === params.limit ? allPages.flat().length : null,
- placeholderData: (previousData, previousQuery) => {
- const previousParams = previousQuery?.queryKey?.[1] as
- | { viewer_identity?: string | null }
- | undefined;
- const previousViewerIdentity =
- typeof previousParams?.viewer_identity === "string"
- ? previousParams.viewer_identity
- : null;
-
- if (previousViewerIdentity === normalizedViewerIdentityKey) {
- return previousData;
- }
-
- return undefined;
- },
- refetchInterval,
- refetchIntervalInBackground,
- ...getDefaultQueryRetry(() => setLastErrorTimestamp(Date.now())),
- });
-
- const getWaves = (): ApiWave[] => query.data?.pages.flat() ?? [];
-
- const [waves, setWaves] = useState(getWaves());
- useEffect(() => {
- setWaves(getWaves());
- }, [query.data]);
-
- const fetchNextPage = useCallback(() => {
- if (lastErrorTimestamp && Date.now() - lastErrorTimestamp < 30000) {
- setTimeout(() => {
- void query.fetchNextPage();
- }, 30000);
- return;
- }
- void query.fetchNextPage();
- }, [lastErrorTimestamp, query]);
-
- const refetch = useCallback(() => {
- if (lastErrorTimestamp && Date.now() - lastErrorTimestamp < 30000) {
- setTimeout(() => {
- void query.refetch();
- }, 30000);
- return;
- }
- void query.refetch();
- }, [lastErrorTimestamp, query]);
-
- const returnValue = useMemo(() => {
- return {
- waves,
- isFetching: query.isFetching,
- isFetchingNextPage: query.isFetchingNextPage,
- hasNextPage: query.hasNextPage,
- fetchNextPage,
- status: query.status,
- refetch,
- };
- }, [waves, query, fetchNextPage, refetch]);
-
- return returnValue;
-};
diff --git a/hooks/useWavesV2.ts b/hooks/useWavesV2.ts
new file mode 100644
index 0000000000..e6a6e1ed6d
--- /dev/null
+++ b/hooks/useWavesV2.ts
@@ -0,0 +1,145 @@
+"use client";
+
+import { useCallback, useMemo, useState } from "react";
+import { useInfiniteQuery } from "@tanstack/react-query";
+
+import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
+import { getDefaultQueryRetry } from "@/components/react-query-wrapper/utils/query-utils";
+import type { ApiWavesOverviewType } from "@/generated/models/ApiWavesOverviewType";
+import type { ApiWavesPinFilter } from "@/generated/models/ApiWavesPinFilter";
+import {
+ fetchWavesV2Page,
+ getWavesV2OverviewQueryKeyParams,
+} from "@/services/api/waves-v2-api";
+
+export { mapApiWaveToSidebarWave } from "@/services/api/waves-v2-api";
+
+interface UseWavesV2Props {
+ readonly overviewType: ApiWavesOverviewType;
+ readonly pageSize?: number | undefined;
+ readonly following?: boolean | undefined;
+ readonly viewerIdentityKey?: string | null | undefined;
+ readonly directMessage?: boolean | undefined;
+ readonly pinned?: ApiWavesPinFilter | undefined;
+ readonly refetchInterval?: number | undefined;
+ readonly refetchIntervalInBackground?: boolean | undefined;
+}
+
+export const useWavesV2 = ({
+ overviewType,
+ pageSize = 20,
+ following = false,
+ viewerIdentityKey,
+ directMessage,
+ pinned,
+ refetchInterval = Infinity,
+ refetchIntervalInBackground = false,
+}: UseWavesV2Props) => {
+ const queryKeyParams = useMemo(
+ () =>
+ getWavesV2OverviewQueryKeyParams({
+ overviewType,
+ pageSize,
+ following,
+ directMessage,
+ pinned,
+ viewerIdentityKey,
+ }),
+ [
+ overviewType,
+ pageSize,
+ following,
+ directMessage,
+ pinned,
+ viewerIdentityKey,
+ ]
+ );
+
+ const [lastErrorTimestamp, setLastErrorTimestamp] = useState(
+ null
+ );
+ const handleRetryFailure = useCallback(() => {
+ setLastErrorTimestamp(Date.now());
+ }, []);
+
+ const query = useInfiniteQuery({
+ queryKey: [QueryKey.WAVES_V2, queryKeyParams],
+ queryFn: async ({ pageParam }: { pageParam: number }) =>
+ await fetchWavesV2Page({
+ page: pageParam,
+ pageSize,
+ overviewType,
+ following,
+ directMessage,
+ pinned,
+ }),
+ initialPageParam: 1,
+ getNextPageParam: (lastPage) => (lastPage.next ? lastPage.page + 1 : null),
+ placeholderData: (previousData, previousQuery) => {
+ const previousParams =
+ previousQuery === undefined
+ ? undefined
+ : (previousQuery.queryKey[1] as
+ | { viewer_identity?: string | null }
+ | undefined);
+ const previousViewerIdentity =
+ typeof previousParams?.viewer_identity === "string"
+ ? previousParams.viewer_identity
+ : null;
+ const currentViewerIdentity = queryKeyParams.viewer_identity ?? null;
+
+ if (previousViewerIdentity === currentViewerIdentity) {
+ return previousData;
+ }
+
+ return undefined;
+ },
+ refetchInterval,
+ refetchIntervalInBackground,
+ ...getDefaultQueryRetry(handleRetryFailure),
+ });
+
+ const waves = useMemo(
+ () => query.data?.pages.flatMap((page) => page.waves) ?? [],
+ [query.data]
+ );
+
+ const fetchNextPage = useCallback(() => {
+ if (
+ lastErrorTimestamp !== null &&
+ Date.now() - lastErrorTimestamp < 30000
+ ) {
+ setTimeout(() => {
+ query.fetchNextPage().catch(() => undefined);
+ }, 30000);
+ return;
+ }
+ query.fetchNextPage().catch(() => undefined);
+ }, [lastErrorTimestamp, query]);
+
+ const refetch = useCallback(() => {
+ if (
+ lastErrorTimestamp !== null &&
+ Date.now() - lastErrorTimestamp < 30000
+ ) {
+ setTimeout(() => {
+ query.refetch().catch(() => undefined);
+ }, 30000);
+ return;
+ }
+ query.refetch().catch(() => undefined);
+ }, [lastErrorTimestamp, query]);
+
+ return useMemo(
+ () => ({
+ waves,
+ isFetching: query.isFetching,
+ isFetchingNextPage: query.isFetchingNextPage,
+ hasNextPage: query.hasNextPage,
+ fetchNextPage,
+ status: query.status,
+ refetch,
+ }),
+ [waves, query, fetchNextPage, refetch]
+ );
+};
diff --git a/hooks/waves/useWaveDecisions.ts b/hooks/waves/useWaveDecisions.ts
index 8f86076cdb..768012952f 100644
--- a/hooks/waves/useWaveDecisions.ts
+++ b/hooks/waves/useWaveDecisions.ts
@@ -1,13 +1,15 @@
import { useEffect, useMemo } from "react";
import { useInfiniteQuery } from "@tanstack/react-query";
-import { commonApiFetch } from "@/services/api/common-api";
import type { ApiWaveDecision } from "@/generated/models/ApiWaveDecision";
-import type { ApiWaveDecisionsPage } from "@/generated/models/ApiWaveDecisionsPage";
import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
import { prepareWaveDecisionPoint } from "@/helpers/waves/wave-decision.helpers";
+import type { ApiWave } from "@/generated/models/ApiWave";
+import type { ApiWaveMin } from "@/generated/models/ApiWaveMin";
+import { fetchWaveDecisionsV2 } from "@/services/api/wave-decisions-v2-api";
interface UseWaveDecisionsProps {
readonly waveId: string;
+ readonly wave?: ApiWave | ApiWaveMin | undefined;
readonly enabled?: boolean | undefined;
readonly loadAllPages?: boolean | undefined;
readonly pageSize?: number | undefined;
@@ -35,6 +37,7 @@ const getValidPageSize = (pageSize: number | undefined): number => {
export function useWaveDecisions({
waveId,
+ wave,
enabled = true,
loadAllPages = false,
pageSize,
@@ -53,17 +56,25 @@ export function useWaveDecisions({
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: [QueryKey.WAVE_DECISIONS, { waveId, pageSize: resolvedPageSize }],
- queryFn: async ({ pageParam }: { pageParam?: number | undefined }) => {
+ queryFn: async ({
+ pageParam,
+ signal,
+ }: {
+ pageParam?: number | undefined;
+ signal?: AbortSignal | undefined;
+ }) => {
const currentPage = pageParam ?? DEFAULT_PAGE;
- return await commonApiFetch({
- endpoint: `waves/${waveId}/decisions`,
+ return await fetchWaveDecisionsV2({
+ waveId,
+ wave,
params: {
sort_direction: "DESC",
sort: "decision_time",
page: currentPage.toString(),
page_size: resolvedPageSize.toString(),
},
+ signal,
});
},
initialPageParam: DEFAULT_PAGE,
diff --git a/hooks/waves/useWaveSalesDecisions.ts b/hooks/waves/useWaveSalesDecisions.ts
index 9e5f50d405..c61de7e117 100644
--- a/hooks/waves/useWaveSalesDecisions.ts
+++ b/hooks/waves/useWaveSalesDecisions.ts
@@ -1,12 +1,14 @@
import { useInfiniteQuery } from "@tanstack/react-query";
-import { commonApiFetch } from "@/services/api/common-api";
import type { ApiWaveDecision } from "@/generated/models/ApiWaveDecision";
-import type { ApiWaveDecisionsPage } from "@/generated/models/ApiWaveDecisionsPage";
import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
import { useMemo } from "react";
+import type { ApiWave } from "@/generated/models/ApiWave";
+import type { ApiWaveMin } from "@/generated/models/ApiWaveMin";
+import { fetchWaveDecisionsV2 } from "@/services/api/wave-decisions-v2-api";
interface UseWaveSalesDecisionsProps {
readonly waveId: string;
+ readonly wave?: ApiWave | ApiWaveMin | undefined;
readonly enabled?: boolean | undefined;
}
@@ -23,6 +25,7 @@ const sortDecisionPoint = (
export function useWaveSalesDecisions({
waveId,
+ wave,
enabled = true,
}: UseWaveSalesDecisionsProps) {
const {
@@ -39,8 +42,9 @@ export function useWaveSalesDecisions({
queryFn: async ({ pageParam }: { pageParam?: number | undefined }) => {
const currentPage = pageParam ?? DEFAULT_PAGE;
- return await commonApiFetch({
- endpoint: `waves/${waveId}/decisions`,
+ return await fetchWaveDecisionsV2({
+ waveId,
+ wave,
params: {
sort_direction: "DESC",
sort: "decision_time",
diff --git a/openapi.yaml b/openapi.yaml
index fe7677edf3..dff9c21f39 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -996,11 +996,13 @@ paths:
tags:
- Drops
summary: Get curated profile wave drops
+ deprecated: true
description: >-
- Returns drops from effective profile-wave curations across all public
- profile waves, newest drops first. If a profile wave has an explicit
- profile curation it is used; otherwise the oldest curation in the
- profile wave is used. Reply drops are included when curated.
+ Deprecated. Use GET /v2/curated-profile-wave-drops instead. Returns
+ drops from effective profile-wave curations across all public profile
+ waves, newest drops first. If a profile wave has an explicit profile
+ curation it is used; otherwise the oldest curation in the profile wave
+ is used. Reply drops are included when curated.
operationId: getCuratedProfileWaveDrops
parameters:
- name: page
@@ -1032,6 +1034,8 @@ paths:
tags:
- Drops
summary: Get latest drops.
+ deprecated: true
+ description: Deprecated. Use GET /v2/drops instead.
operationId: getLatestDrops
parameters:
- name: limit
@@ -1197,6 +1201,8 @@ paths:
tags:
- Drops
summary: Get boosted drops.
+ deprecated: true
+ description: Deprecated. Use GET /v2/boosted-drops instead.
operationId: getBoostedDrops
parameters:
- name: author
@@ -1277,6 +1283,8 @@ paths:
tags:
- Drops
summary: Get drop boosts by Drop ID.
+ deprecated: true
+ description: Deprecated. Use GET /v2/drops/{id}/boosts instead.
operationId: getDropBoostsById
parameters:
- name: dropId
@@ -1450,6 +1458,8 @@ paths:
tags:
- Drops
summary: Get drop by ID.
+ deprecated: true
+ description: Deprecated. Use GET /v2/drops/{id} instead.
operationId: getDropById
parameters:
- name: dropId
@@ -2076,6 +2086,79 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ApiDropV2Page"
+ /v2/curated-profile-wave-drops:
+ get:
+ tags:
+ - DropsV2
+ summary: Get V2 curated profile wave drops
+ description: >-
+ Returns V2 drops from effective profile-wave curations across all public
+ profile waves, newest drops first. If a profile wave has an explicit
+ profile curation it is used; otherwise the oldest curation in the
+ profile wave is used. Reply drops are included when curated.
+ operationId: getCuratedProfileWaveDropsV2
+ parameters:
+ - name: page
+ in: query
+ required: false
+ schema:
+ type: integer
+ format: int64
+ minimum: 1
+ default: 1
+ - name: page_size
+ in: query
+ required: false
+ schema:
+ type: integer
+ format: int64
+ minimum: 1
+ maximum: 2000
+ default: 50
+ responses:
+ "200":
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ApiDropV2PageWithoutCount"
+ /v2/drops:
+ get:
+ tags:
+ - DropsV2
+ summary: Get V2 drops.
+ operationId: getDropsV2
+ parameters:
+ - name: parent_drop_id
+ in: query
+ description: Return replies under this parent drop.
+ required: false
+ schema:
+ type: string
+ - name: page
+ in: query
+ required: false
+ schema:
+ type: integer
+ format: int64
+ minimum: 1
+ default: 1
+ - name: page_size
+ in: query
+ required: false
+ schema:
+ type: integer
+ format: int64
+ minimum: 1
+ maximum: 100
+ default: 50
+ responses:
+ "200":
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ApiDropV2PageWithoutCount"
/v2/drops/{id}:
get:
tags:
@@ -3351,6 +3434,8 @@ paths:
tags:
- Notifications
summary: Get notifications for authenticated user.
+ deprecated: true
+ description: Deprecated. Use GET /v2/notifications instead.
operationId: getNotifications
parameters:
- name: limit
@@ -5719,6 +5804,8 @@ paths:
tags:
- Waves
summary: Get already decided wave decision outcomes
+ deprecated: true
+ description: Deprecated. Use GET /v2/waves/{id}/decisions instead.
operationId: getWaveDecisions
parameters:
- name: id
@@ -5834,6 +5921,8 @@ paths:
tags:
- Waves
summary: Search for drops in wave by content
+ deprecated: true
+ description: Deprecated. Use GET /v2/waves/{waveId}/search instead.
operationId: searchDropsInWave
parameters:
- name: waveId
@@ -5899,6 +5988,8 @@ paths:
tags:
- Waves
summary: Get overview of waves by different criteria.
+ deprecated: true
+ description: Deprecated. Use GET /v2/waves?view=OVERVIEW instead.
operationId: getWavesOverview
parameters:
- name: limit
@@ -5959,10 +6050,12 @@ paths:
tags:
- Waves
summary: Get waves where a profile has written the most messages.
+ deprecated: true
description: >-
- Returns non-DM waves ranked by how many chat messages the resolved
- profile has written in them. Results only include waves the requester
- can access.
+ Deprecated. Use GET /v2/waves?view=FAVOURITES&identity={identityKey}
+ instead. Returns non-DM waves ranked by how many chat messages the
+ resolved profile has written in them. Results only include waves the
+ requester can access.
operationId: getFavouriteWavesOfIdentity
parameters:
- name: identityKey
@@ -6003,7 +6096,10 @@ paths:
tags:
- Waves
summary: Get hot waves overview.
- description: Returns up to 25 public waves ranked by activity in the last 24 hours.
+ deprecated: true
+ description: >-
+ Deprecated. Use GET /v2/waves?view=HOT instead. Returns up to 25 public
+ waves ranked by activity in the last 24 hours.
operationId: getHotWavesOverview
parameters:
- name: exclude_followed
@@ -6028,6 +6124,8 @@ paths:
tags:
- Waves
summary: Get waves.
+ deprecated: true
+ description: Deprecated. Use GET /v2/waves?view=SEARCH instead.
operationId: getWaves
parameters:
- name: name
@@ -6196,6 +6294,8 @@ paths:
tags:
- Waves
summary: Get drops related to wave or parent drop
+ deprecated: true
+ description: Deprecated. Use GET /v2/waves/{id}/drops instead.
operationId: getDropsOfWave
parameters:
- name: id
@@ -6412,6 +6512,8 @@ paths:
tags:
- Waves
summary: Get waves leaderboard
+ deprecated: true
+ description: Deprecated. Use GET /v2/waves/{id}/leaderboard instead.
operationId: getWaveLeaderboard
parameters:
- name: id
@@ -6558,6 +6660,10 @@ paths:
tags:
- Waves
summary: List drops in a curation for wave
+ deprecated: true
+ description: >-
+ Deprecated. Use GET /v2/waves/{id}/curations/{curation_id}/drops
+ instead.
operationId: listWaveCurationDrops
parameters:
- name: id
@@ -11740,6 +11846,8 @@ components:
type: array
items:
$ref: "#/components/schemas/ApiDropV2"
+ related_wave:
+ $ref: "#/components/schemas/ApiWaveOverview"
additional_context:
$ref: "#/components/schemas/ApiNotificationAdditionalContextV2"
ApiOutgoingIdentitySubscriptionsPage:
@@ -13824,6 +13932,9 @@ components:
- subscribers_count
- has_competition
- is_dm_wave
+ - description_drop
+ - total_drops_count
+ - is_private
properties:
id:
type: string
@@ -13844,6 +13955,17 @@ components:
type: boolean
is_dm_wave:
type: boolean
+ description_drop:
+ $ref: "#/components/schemas/ApiWaveOverviewDescriptionDrop"
+ total_drops_count:
+ type: integer
+ format: int64
+ is_private:
+ type: boolean
+ contributors:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiWaveOverviewContributor"
context_profile_context:
$ref: "#/components/schemas/ApiWaveOverviewContextProfileContext"
ApiWaveOverviewContextProfileContext:
@@ -13864,8 +13986,32 @@ components:
unread_drops:
type: integer
format: int64
+ first_unread_drop_serial_no:
+ type: integer
+ format: int64
muted:
type: boolean
+ ApiWaveOverviewContributor:
+ type: object
+ required:
+ - handle
+ - pfp
+ properties:
+ handle:
+ type: string
+ nullable: true
+ pfp:
+ type: string
+ nullable: true
+ ApiWaveOverviewDescriptionDrop:
+ type: object
+ properties:
+ contents:
+ type: string
+ media:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiDropMedia"
ApiWaveOverviewPage:
type: object
required:
diff --git a/services/api/drop-api.ts b/services/api/drop-api.ts
index afcd12bdb3..a598cd93ac 100644
--- a/services/api/drop-api.ts
+++ b/services/api/drop-api.ts
@@ -1,9 +1,7 @@
import type { QueryClient } from "@tanstack/react-query";
import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
import type { ApiDrop } from "@/generated/models/ApiDrop";
-import { commonApiFetch } from "@/services/api/common-api";
-
-const DROP_BATCH_SIZE = 20;
+import { fetchDropV2ById } from "@/services/api/wave-drops-v2-api";
export const DROP_DETAIL_STALE_TIME_MS = 60 * 1000;
export const DROP_BATCH_STALE_TIME_MS = 5 * 60 * 1000;
@@ -32,14 +30,6 @@ const getUniqueDropIds = (dropIds: readonly string[]): string[] => {
return uniqueDropIds;
};
-const chunkDropIds = (dropIds: readonly string[]): string[][] => {
- const chunks: string[][] = [];
- for (let index = 0; index < dropIds.length; index += DROP_BATCH_SIZE) {
- chunks.push(dropIds.slice(index, index + DROP_BATCH_SIZE));
- }
- return chunks;
-};
-
export const orderDropsByIds = (
dropIds: readonly string[],
drops: readonly ApiDrop[]
@@ -50,29 +40,65 @@ export const orderDropsByIds = (
.filter((drop): drop is ApiDrop => !!drop);
};
-export const fetchDropsByIds = async (
+type DropFetchResult =
+ | {
+ readonly dropId: string;
+ readonly status: "fulfilled";
+ readonly drop: ApiDrop;
+ }
+ | {
+ readonly dropId: string;
+ readonly status: "rejected";
+ readonly error: unknown;
+ };
+
+type FulfilledDropFetchResult = Extract<
+ DropFetchResult,
+ { readonly status: "fulfilled" }
+>;
+
+const fetchDropResultsByIds = async (
dropIds: readonly string[]
-): Promise => {
+): Promise => {
const uniqueDropIds = getUniqueDropIds(dropIds);
if (uniqueDropIds.length === 0) {
return [];
}
- const dropChunks = chunkDropIds(uniqueDropIds);
- const dropPages = await Promise.all(
- dropChunks.map((chunk) =>
- commonApiFetch({
- endpoint: "drops",
- params: {
- ids: chunk.join(","),
- limit: `${chunk.length}`,
- include_replies: "true",
- },
- })
- )
+ const results = await Promise.allSettled(
+ uniqueDropIds.map((dropId) => fetchDropV2ById(dropId))
);
- return orderDropsByIds(uniqueDropIds, dropPages.flat());
+ return results.map((result, index) => {
+ const dropId = uniqueDropIds[index]!;
+ if (result.status === "fulfilled") {
+ return {
+ dropId,
+ status: "fulfilled",
+ drop: result.value,
+ };
+ }
+
+ return {
+ dropId,
+ status: "rejected",
+ error: result.reason as unknown,
+ };
+ });
+};
+
+export const fetchDropsByIds = async (
+ dropIds: readonly string[]
+): Promise => {
+ const results = await fetchDropResultsByIds(dropIds);
+ const drops = results
+ .filter(
+ (result): result is FulfilledDropFetchResult =>
+ result.status === "fulfilled"
+ )
+ .map((result) => result.drop);
+
+ return orderDropsByIds(dropIds, drops);
};
export const seedDropCache = (
@@ -100,18 +126,15 @@ const flushPendingDropRequests = async () => {
const dropIds = [...currentRequests.keys()];
try {
- const drops = await fetchDropsByIds(dropIds);
- const dropsById = new Map(drops.map((drop) => [drop.id, drop]));
-
- for (const dropId of dropIds) {
- const drop = dropsById.get(dropId);
- const requests = currentRequests.get(dropId) ?? [];
- if (!drop) {
- const error = new Error(`Drop ${dropId} not found`);
- requests.forEach((request) => request.reject(error));
+ const results = await fetchDropResultsByIds(dropIds);
+
+ for (const result of results) {
+ const requests = currentRequests.get(result.dropId) ?? [];
+ if (result.status === "rejected") {
+ requests.forEach((request) => request.reject(result.error));
continue;
}
- requests.forEach((request) => request.resolve(drop));
+ requests.forEach((request) => request.resolve(result.drop));
}
} catch (error) {
currentRequests.forEach((requests) => {
diff --git a/services/api/drop-v2-mappers.ts b/services/api/drop-v2-mappers.ts
new file mode 100644
index 0000000000..ee2bc14104
--- /dev/null
+++ b/services/api/drop-v2-mappers.ts
@@ -0,0 +1,262 @@
+import type { ApiDropContextProfileContext } from "@/generated/models/ApiDropContextProfileContext";
+import type { ApiDropPart } from "@/generated/models/ApiDropPart";
+import type { ApiDropPartV2 } from "@/generated/models/ApiDropPartV2";
+import type { ApiDropReaction } from "@/generated/models/ApiDropReaction";
+import type { ApiDropWithoutWave } from "@/generated/models/ApiDropWithoutWave";
+import { ApiDropType } from "@/generated/models/ApiDropType";
+import type { ApiDropV2 } from "@/generated/models/ApiDropV2";
+import type { ApiIdentityOverview } from "@/generated/models/ApiIdentityOverview";
+import type { ApiIdentityOverviewBadges } from "@/generated/models/ApiIdentityOverviewBadges";
+import type { ApiMentionedWave } from "@/generated/models/ApiMentionedWave";
+import { ApiProfileClassification } from "@/generated/models/ApiProfileClassification";
+import type { ApiProfileMin } from "@/generated/models/ApiProfileMin";
+import type { ApiReplyToDropResponse } from "@/generated/models/ApiReplyToDropResponse";
+import type { ApiReplyToDropV2 } from "@/generated/models/ApiReplyToDropV2";
+import type { ApiWave } from "@/generated/models/ApiWave";
+import { ApiWaveCreditType } from "@/generated/models/ApiWaveCreditType";
+import type { ApiWaveMin } from "@/generated/models/ApiWaveMin";
+import type { ApiWaveOverview } from "@/generated/models/ApiWaveOverview";
+import { toApiWaveMin } from "@/helpers/waves/wave.helpers";
+
+type ApiProfileMinWithBadges = ApiProfileMin & {
+ readonly badges?: ApiIdentityOverviewBadges;
+};
+
+export const mapIdentityOverviewToProfileMin = (
+ identity: ApiIdentityOverview
+): ApiProfileMinWithBadges => {
+ const profileWaveId = identity.badges.profile_wave_id ?? null;
+
+ return {
+ id: identity.id,
+ handle: identity.handle ?? null,
+ pfp: identity.pfp ?? null,
+ banner1_color: null,
+ banner2_color: null,
+ cic: 0,
+ rep: 0,
+ tdh: 0,
+ tdh_rate: 0,
+ xtdh: 0,
+ xtdh_rate: 0,
+ level: identity.level,
+ classification: identity.classification,
+ sub_classification: null,
+ primary_address: identity.primary_address,
+ subscribed_actions: [],
+ archived: false,
+ active_main_stage_submission_ids: [],
+ winner_main_stage_drop_ids: [],
+ artist_of_prevote_cards: [],
+ profile_wave_id: profileWaveId,
+ is_wave_creator: profileWaveId !== null,
+ badges: identity.badges,
+ };
+};
+
+export const mapApiWaveOverviewToApiWaveMin = (
+ wave: ApiWaveOverview
+): ApiWaveMin => ({
+ id: wave.id,
+ name: wave.name,
+ picture: wave.pfp ?? null,
+ description_drop_id: "",
+ last_drop_time: wave.last_drop_time,
+ submission_type: null,
+ authenticated_user_eligible_to_vote: true,
+ authenticated_user_eligible_to_participate: true,
+ authenticated_user_eligible_to_chat:
+ wave.context_profile_context?.can_chat ?? true,
+ authenticated_user_admin: false,
+ visibility_group_id: wave.is_private ? "private" : null,
+ participation_group_id: null,
+ chat_group_id: null,
+ voting_group_id: null,
+ admin_group_id: null,
+ voting_period_start: null,
+ voting_period_end: null,
+ voting_credit_type: ApiWaveCreditType.Tdh,
+ voting_credit_nfts: null,
+ admin_drop_deletion_enabled: false,
+ forbid_negative_votes: false,
+ pinned: wave.context_profile_context?.pinned ?? false,
+ identity_wave: false,
+});
+
+export const createFallbackWaveMin = (waveId: string): ApiWaveMin => ({
+ id: waveId,
+ name: waveId,
+ picture: null,
+ description_drop_id: "",
+ last_drop_time: 0,
+ submission_type: null,
+ authenticated_user_eligible_to_vote: true,
+ authenticated_user_eligible_to_participate: true,
+ authenticated_user_eligible_to_chat: true,
+ authenticated_user_admin: false,
+ visibility_group_id: null,
+ participation_group_id: null,
+ chat_group_id: null,
+ voting_group_id: null,
+ admin_group_id: null,
+ voting_period_start: null,
+ voting_period_end: null,
+ voting_credit_type: ApiWaveCreditType.Tdh,
+ voting_credit_nfts: null,
+ admin_drop_deletion_enabled: false,
+ forbid_negative_votes: false,
+ pinned: false,
+ identity_wave: false,
+});
+
+export const normalizeWaveMin = (wave: ApiWave | ApiWaveMin): ApiWaveMin =>
+ "description_drop" in wave ? toApiWaveMin(wave) : wave;
+
+export const getWaveMin = (
+ wave: ApiWave | ApiWaveMin | undefined,
+ fallbackWaveId: string
+): ApiWaveMin =>
+ wave ? normalizeWaveMin(wave) : createFallbackWaveMin(fallbackWaveId);
+
+export const createBasePart = (drop: ApiDropV2): ApiDropPart => ({
+ part_id: 1,
+ content: drop.content ?? null,
+ media: drop.media ?? [],
+ attachments: drop.attachments ?? [],
+ quoted_drop: null,
+});
+
+export const mapDropPartV2ToApiDropPart = (
+ part: ApiDropPartV2
+): ApiDropPart => ({
+ part_id: part.part_no,
+ content: part.content ?? null,
+ media: part.media ?? [],
+ attachments: part.attachments ?? [],
+ quoted_drop: part.quoted_drop
+ ? {
+ drop_id: part.quoted_drop.drop_id,
+ drop_part_id: part.quoted_drop.drop_part_id,
+ }
+ : null,
+});
+
+export const mapDropReactionCountersV2 = (drop: ApiDropV2): ApiDropReaction[] =>
+ drop.reactions
+ ?.filter((reaction) => reaction.count > 0)
+ .map((reaction) => ({
+ reaction: reaction.reaction,
+ profiles: [],
+ count: reaction.count,
+ })) ?? [];
+
+export const mapMentionedWaves = (
+ drop: ApiDropV2,
+ fallbackWave: ApiWaveMin
+): ApiMentionedWave[] =>
+ drop.mentioned_waves?.map((wave) => ({
+ wave_name_in_content: wave.in_content,
+ wave_id: wave.id,
+ wave: {
+ ...fallbackWave,
+ id: wave.id,
+ name: wave.name ?? wave.in_content,
+ picture: wave.pfp ?? null,
+ },
+ })) ?? [];
+
+const mapReplyToDropPreview = (
+ replyToDrop: ApiReplyToDropV2
+): ApiDropWithoutWave => ({
+ id: replyToDrop.id,
+ serial_no: replyToDrop.serial_no ?? 0,
+ drop_type: ApiDropType.Chat,
+ rank: null,
+ author: {
+ id: replyToDrop.author?.handle ?? "",
+ handle: replyToDrop.author?.handle ?? null,
+ pfp: replyToDrop.author?.pfp ?? null,
+ banner1_color: null,
+ banner2_color: null,
+ cic: 0,
+ rep: 0,
+ tdh: 0,
+ tdh_rate: 0,
+ xtdh: 0,
+ xtdh_rate: 0,
+ level: 0,
+ classification: ApiProfileClassification.Pseudonym,
+ sub_classification: null,
+ primary_address: "",
+ subscribed_actions: [],
+ archived: false,
+ active_main_stage_submission_ids: [],
+ winner_main_stage_drop_ids: [],
+ artist_of_prevote_cards: [],
+ profile_wave_id: null,
+ is_wave_creator: false,
+ },
+ created_at: 0,
+ updated_at: null,
+ title: null,
+ parts: [
+ {
+ part_id: 1,
+ content: replyToDrop.content ?? null,
+ media: [],
+ attachments: [],
+ quoted_drop: null,
+ },
+ ],
+ parts_count: 1,
+ referenced_nfts: [],
+ mentioned_users: [],
+ mentioned_groups: [],
+ mentioned_waves: [],
+ metadata: [],
+ rating: 0,
+ realtime_rating: 0,
+ rating_prediction: 0,
+ top_raters: [],
+ raters_count: 0,
+ context_profile_context: null,
+ subscribed_actions: [],
+ is_signed: false,
+ reactions: [],
+ boosts: 0,
+ hide_link_preview: false,
+ nft_links: [],
+});
+
+export const mapReplyToDrop = (
+ drop: ApiDropV2
+): ApiReplyToDropResponse | undefined => {
+ if (!drop.reply_to_drop) {
+ return undefined;
+ }
+
+ return {
+ drop_id: drop.reply_to_drop.id,
+ drop_part_id: 1,
+ is_deleted: false,
+ drop: mapReplyToDropPreview(drop.reply_to_drop),
+ };
+};
+
+export const getContextProfileContext = (
+ drop: ApiDropV2
+): ApiDropContextProfileContext => {
+ const votingContext = drop.submission_context?.voting.context_profile_context;
+ const dropContext = drop.context_profile_context;
+
+ return {
+ rating: votingContext?.current ?? 0,
+ min_rating: votingContext?.min ?? 0,
+ max_rating: votingContext?.max ?? 0,
+ reaction: dropContext?.reaction ?? null,
+ boosted: dropContext?.boosted ?? false,
+ bookmarked: dropContext?.bookmarked ?? false,
+ curatable: false,
+ curated: false,
+ };
+};
diff --git a/services/api/notifications-v2-api.ts b/services/api/notifications-v2-api.ts
new file mode 100644
index 0000000000..40a212ca3c
--- /dev/null
+++ b/services/api/notifications-v2-api.ts
@@ -0,0 +1,358 @@
+import type { ApiDrop } from "@/generated/models/ApiDrop";
+import type { ApiDropV2 } from "@/generated/models/ApiDropV2";
+import type { ApiNotificationAdditionalContextV2 } from "@/generated/models/ApiNotificationAdditionalContextV2";
+import { ApiNotificationCause } from "@/generated/models/ApiNotificationCause";
+import type { ApiNotificationDropReactedReactor } from "@/generated/models/ApiNotificationDropReactedReactor";
+import type { ApiNotificationV2 } from "@/generated/models/ApiNotificationV2";
+import type { ApiNotificationsResponseV2 } from "@/generated/models/ApiNotificationsResponseV2";
+import { ApiProfileClassification } from "@/generated/models/ApiProfileClassification";
+import type { ApiProfileMin } from "@/generated/models/ApiProfileMin";
+import type { ApiWaveMin } from "@/generated/models/ApiWaveMin";
+import type { ApiWaveOverview } from "@/generated/models/ApiWaveOverview";
+import { commonApiFetch } from "@/services/api/common-api";
+import {
+ mapApiWaveOverviewToApiWaveMin,
+ mapIdentityOverviewToProfileMin,
+} from "@/services/api/drop-v2-mappers";
+import { mapLeaderboardDropV2 } from "@/services/api/wave-drops-v2-api";
+import type {
+ INotificationDropReacted,
+ TypedNotification,
+ TypedNotificationsResponse,
+} from "@/types/feed.types";
+
+type NotificationWaveMin = ApiWaveMin & {
+ readonly is_direct_message?: boolean;
+};
+
+const knownNotificationCauses = new Set(
+ Object.values(ApiNotificationCause)
+);
+
+type FetchNotificationsV2Params = {
+ readonly limit: string;
+ readonly cause?: ApiNotificationCause[] | null | undefined;
+ readonly pageParam?: number | null | undefined;
+ readonly signal?: AbortSignal | undefined;
+ readonly headers?: Record | undefined;
+};
+
+const toStringValue = (value: string | number | undefined): string =>
+ value === undefined ? "" : String(value);
+
+const mapWaveOverviewToNotificationWaveMin = (
+ wave: ApiWaveOverview
+): NotificationWaveMin => ({
+ ...mapApiWaveOverviewToApiWaveMin(wave),
+ is_direct_message: wave.is_dm_wave,
+});
+
+const mapWaveOverviewToNotificationRelatedWave = (
+ wave: ApiWaveOverview
+): ApiWaveOverview & NotificationWaveMin => ({
+ ...wave,
+ ...mapWaveOverviewToNotificationWaveMin(wave),
+});
+
+const mapDropV2ToApiDrop = ({
+ drop,
+ wave,
+}: {
+ readonly drop: ApiDropV2;
+ readonly wave: NotificationWaveMin;
+}): ApiDrop => ({
+ ...mapLeaderboardDropV2({ drop, wave }),
+ wave,
+});
+
+const mapRelatedDrops = (notification: ApiNotificationV2): ApiDrop[] => {
+ if (!notification.related_wave) {
+ return [];
+ }
+
+ const wave = mapWaveOverviewToNotificationWaveMin(notification.related_wave);
+
+ return notification.related_drops.map((drop) =>
+ mapDropV2ToApiDrop({ drop, wave })
+ );
+};
+
+const emptyProfile = ({
+ id,
+ handle,
+ pfp,
+}: {
+ readonly id: string;
+ readonly handle: string | null;
+ readonly pfp: string | null;
+}): ApiProfileMin => ({
+ id,
+ handle,
+ pfp,
+ banner1_color: null,
+ banner2_color: null,
+ cic: 0,
+ rep: 0,
+ tdh: 0,
+ tdh_rate: 0,
+ xtdh: 0,
+ xtdh_rate: 0,
+ level: 0,
+ classification: ApiProfileClassification.Pseudonym,
+ sub_classification: null,
+ primary_address: "",
+ subscribed_actions: [],
+ archived: false,
+ active_main_stage_submission_ids: [],
+ winner_main_stage_drop_ids: [],
+ artist_of_prevote_cards: [],
+ profile_wave_id: null,
+ is_wave_creator: false,
+});
+
+const mapReactorToProfileMin = (
+ reactor: ApiNotificationDropReactedReactor,
+ fallbackIndex: number
+): ApiProfileMin => {
+ const trimmedHandle = reactor.handle?.trim();
+ const handle =
+ trimmedHandle === undefined || trimmedHandle === "" ? null : trimmedHandle;
+
+ return emptyProfile({
+ id: handle ?? `reactor-${fallbackIndex}`,
+ handle,
+ pfp: reactor.pfp ?? null,
+ });
+};
+
+const mapBaseNotification = (notification: ApiNotificationV2) => ({
+ id: notification.id,
+ cause: notification.cause,
+ created_at: notification.created_at,
+ read_at: notification.read_at,
+ related_identity: mapIdentityOverviewToProfileMin(
+ notification.related_identity
+ ),
+});
+
+const mapDropReactedNotification = (
+ notification: ApiNotificationV2,
+ relatedDrops: ApiDrop[]
+): INotificationDropReacted[] => {
+ const reaction = notification.additional_context.reaction ?? "";
+ const reactors = notification.additional_context.reactors ?? [];
+ const base = {
+ ...mapBaseNotification(notification),
+ cause: ApiNotificationCause.DropReacted,
+ related_drops: relatedDrops,
+ additional_context: {
+ reaction,
+ },
+ } satisfies INotificationDropReacted;
+
+ if (!reactors.length) {
+ return [base];
+ }
+
+ return reactors.map((reactor, index) => ({
+ ...base,
+ related_identity: mapReactorToProfileMin(reactor, index),
+ }));
+};
+
+const handleUnknownNotificationCause = (
+ notification: ApiNotificationV2
+): TypedNotification[] => {
+ const cause = String(notification.cause);
+ const knownCauses = [...knownNotificationCauses].join(", ");
+ console.error(
+ `Unsupported notification cause "${cause}". Known ApiNotificationCause values: ${knownCauses}`
+ );
+ return [];
+};
+
+const mapNotificationV2 = (
+ notification: ApiNotificationV2
+): TypedNotification[] => {
+ const base = mapBaseNotification(notification);
+ const relatedDrops = mapRelatedDrops(notification);
+ const context: ApiNotificationAdditionalContextV2 =
+ notification.additional_context;
+
+ switch (notification.cause) {
+ case ApiNotificationCause.IdentitySubscribed:
+ return [
+ {
+ ...base,
+ cause: ApiNotificationCause.IdentitySubscribed,
+ },
+ ];
+ case ApiNotificationCause.IdentityRep:
+ return [
+ {
+ ...base,
+ cause: ApiNotificationCause.IdentityRep,
+ additional_context: {
+ amount: context.amount ?? 0,
+ total: context.total ?? 0,
+ category: context.category ?? "",
+ },
+ },
+ ];
+ case ApiNotificationCause.IdentityNic:
+ return [
+ {
+ ...base,
+ cause: ApiNotificationCause.IdentityNic,
+ additional_context: {
+ amount: context.amount ?? 0,
+ total: context.total ?? 0,
+ },
+ },
+ ];
+ case ApiNotificationCause.IdentityMentioned:
+ return [
+ {
+ ...base,
+ cause: ApiNotificationCause.IdentityMentioned,
+ related_drops: relatedDrops,
+ },
+ ];
+ case ApiNotificationCause.DropVoted:
+ return [
+ {
+ ...base,
+ cause: ApiNotificationCause.DropVoted,
+ related_drops: relatedDrops,
+ additional_context: {
+ vote: context.vote ?? 0,
+ },
+ },
+ ];
+ case ApiNotificationCause.DropReacted:
+ return mapDropReactedNotification(notification, relatedDrops);
+ case ApiNotificationCause.DropBoosted:
+ return [
+ {
+ ...base,
+ cause: ApiNotificationCause.DropBoosted,
+ related_drops: relatedDrops,
+ additional_context: { ...context },
+ },
+ ];
+ case ApiNotificationCause.DropQuoted:
+ return [
+ {
+ ...base,
+ cause: ApiNotificationCause.DropQuoted,
+ related_drops: relatedDrops,
+ additional_context: {
+ quote_drop_id: context.quote_drop_id ?? "",
+ quote_drop_part: toStringValue(context.quote_drop_part),
+ quoted_drop_id: context.quoted_drop_id ?? "",
+ quoted_drop_part: toStringValue(context.quoted_drop_part),
+ },
+ },
+ ];
+ case ApiNotificationCause.DropReplied:
+ return [
+ {
+ ...base,
+ cause: ApiNotificationCause.DropReplied,
+ related_drops: relatedDrops,
+ additional_context: {
+ reply_drop_id: context.reply_drop_id ?? "",
+ replied_drop_id: context.replied_drop_id ?? "",
+ replied_drop_part: toStringValue(context.replied_drop_part),
+ },
+ },
+ ];
+ case ApiNotificationCause.WaveCreated:
+ return [
+ {
+ ...base,
+ cause: ApiNotificationCause.WaveCreated,
+ ...(notification.related_wave
+ ? {
+ related_wave: mapWaveOverviewToNotificationRelatedWave(
+ notification.related_wave
+ ),
+ }
+ : {}),
+ additional_context: {
+ wave_id:
+ notification.related_wave?.id ??
+ notification.additional_context.wave_id ??
+ "",
+ },
+ },
+ ];
+ case ApiNotificationCause.AllDrops:
+ return [
+ {
+ ...base,
+ cause: ApiNotificationCause.AllDrops,
+ related_drops: relatedDrops,
+ additional_context: {
+ vote: context.vote ?? 0,
+ },
+ },
+ ];
+ case ApiNotificationCause.PriorityAlert:
+ return [
+ {
+ ...base,
+ cause: ApiNotificationCause.PriorityAlert,
+ related_drops: relatedDrops,
+ additional_context: { ...context },
+ },
+ ];
+ default:
+ return handleUnknownNotificationCause(notification);
+ }
+};
+
+const mapNotificationsV2Response = (
+ response: ApiNotificationsResponseV2
+): TypedNotificationsResponse => ({
+ unread_count: response.unread_count,
+ notifications: response.notifications.flatMap(mapNotificationV2),
+});
+
+const buildNotificationsV2Params = ({
+ limit,
+ cause,
+ pageParam,
+}: Pick): Record<
+ string,
+ string
+> => {
+ const params: Record = { limit };
+
+ if (pageParam !== null && pageParam !== undefined) {
+ params["id_less_than"] = String(pageParam);
+ }
+
+ if (cause !== null && cause !== undefined && cause.length > 0) {
+ params["cause"] = cause.join(",");
+ }
+
+ return params;
+};
+
+export const fetchNotificationsV2 = async ({
+ limit,
+ cause,
+ pageParam,
+ signal,
+ headers,
+}: FetchNotificationsV2Params): Promise => {
+ const response = await commonApiFetch({
+ endpoint: "v2/notifications",
+ params: buildNotificationsV2Params({ limit, cause, pageParam }),
+ signal,
+ headers,
+ });
+
+ return mapNotificationsV2Response(response);
+};
diff --git a/services/api/pinned-waves-api.ts b/services/api/pinned-waves-api.ts
index 09abad78f7..324d806717 100644
--- a/services/api/pinned-waves-api.ts
+++ b/services/api/pinned-waves-api.ts
@@ -1,32 +1,14 @@
-import { commonApiFetchWithRetry, commonApiPostWithoutBodyAndResponse, commonApiDelete } from './common-api';
-import type { ApiWave } from '@/generated/models/ApiWave';
-import { ApiWavesPinFilter } from '@/generated/models/ApiWavesPinFilter';
-import { ApiWavesOverviewType } from '@/generated/models/ApiWavesOverviewType';
+import {
+ commonApiDelete,
+ commonApiPostWithoutBodyAndResponse,
+} from "./common-api";
interface PinnedWavesService {
- fetchPinnedWaves: () => Promise;
pinWave: (waveId: string) => Promise;
unpinWave: (waveId: string) => Promise;
}
export const pinnedWavesApi: PinnedWavesService = {
- fetchPinnedWaves: async (): Promise => {
- return await commonApiFetchWithRetry({
- endpoint: 'waves-overview',
- params: {
- pinned: ApiWavesPinFilter.Pinned,
- type: ApiWavesOverviewType.MostSubscribed,
- limit: '20',
- },
- retryOptions: {
- maxRetries: 2,
- initialDelayMs: 1000,
- backoffFactor: 2,
- jitter: 0.1,
- },
- });
- },
-
pinWave: async (waveId: string): Promise => {
await commonApiPostWithoutBodyAndResponse({
endpoint: `waves/${waveId}/pins`,
diff --git a/services/api/quorum-participation-drop-preview-v2-api.ts b/services/api/quorum-participation-drop-preview-v2-api.ts
new file mode 100644
index 0000000000..677feb6d87
--- /dev/null
+++ b/services/api/quorum-participation-drop-preview-v2-api.ts
@@ -0,0 +1,41 @@
+import type { ApiDrop } from "@/generated/models/ApiDrop";
+import { ApiDropSearchStrategy } from "@/generated/models/ApiDropSearchStrategy";
+import type { ApiWaveDropsFeedV2 } from "@/generated/models/ApiWaveDropsFeedV2";
+import { commonApiFetch } from "@/services/api/common-api";
+import { mapApiWaveOverviewToApiWaveMin } from "@/services/api/drop-v2-mappers";
+import { mapLeaderboardDropV2 } from "@/services/api/wave-drops-v2-api";
+
+interface FetchQuorumParticipationDropPreviewBySerialNoV2Props {
+ readonly waveId: string;
+ readonly serialNo: number;
+ readonly signal?: AbortSignal | undefined;
+}
+
+export async function fetchQuorumParticipationDropPreviewBySerialNoV2({
+ waveId,
+ serialNo,
+ signal,
+}: FetchQuorumParticipationDropPreviewBySerialNoV2Props): Promise {
+ const response = await commonApiFetch({
+ endpoint: `v2/waves/${waveId}/drops`,
+ params: {
+ limit: "1",
+ serial_no_limit: `${serialNo}`,
+ search_strategy: ApiDropSearchStrategy.Both,
+ },
+ signal,
+ });
+
+ const drop = response.drops.find(
+ (candidate) => candidate.serial_no === serialNo
+ );
+ if (!drop) {
+ return null;
+ }
+
+ const wave = mapApiWaveOverviewToApiWaveMin(response.wave);
+ return {
+ ...mapLeaderboardDropV2({ drop, wave }),
+ wave,
+ };
+}
diff --git a/services/api/wave-curation-drops-v2-api.ts b/services/api/wave-curation-drops-v2-api.ts
new file mode 100644
index 0000000000..f7ba714562
--- /dev/null
+++ b/services/api/wave-curation-drops-v2-api.ts
@@ -0,0 +1,66 @@
+import type { ApiCurationDrop } from "@/generated/models/ApiCurationDrop";
+import type { ApiCurationDropsPage } from "@/generated/models/ApiCurationDropsPage";
+import type { ApiDropContextProfileContext } from "@/generated/models/ApiDropContextProfileContext";
+import type { ApiDropV2PageWithoutCount } from "@/generated/models/ApiDropV2PageWithoutCount";
+import type { ApiWave } from "@/generated/models/ApiWave";
+import type { ApiWaveMin } from "@/generated/models/ApiWaveMin";
+import { commonApiFetch } from "@/services/api/common-api";
+import { normalizeWaveMin } from "@/services/api/drop-v2-mappers";
+import { mapLeaderboardDropV2 } from "@/services/api/wave-drops-v2-api";
+
+interface FetchWaveCurationDropsV2Props {
+ readonly wave: ApiWave | ApiWaveMin;
+ readonly curationId: string;
+ readonly page: number;
+ readonly pageSize: number;
+ readonly signal?: AbortSignal | undefined;
+}
+
+const FALLBACK_CONTEXT_PROFILE_CONTEXT: ApiDropContextProfileContext = {
+ rating: 0,
+ min_rating: 0,
+ max_rating: 0,
+ reaction: null,
+ boosted: false,
+ bookmarked: false,
+ curatable: false,
+ curated: false,
+};
+
+const mapCurationDropV2 = (
+ drop: ApiDropV2PageWithoutCount["data"][number],
+ wave: ApiWaveMin
+): ApiCurationDrop => {
+ const mappedDrop = mapLeaderboardDropV2({ drop, wave });
+
+ return {
+ drop_priority_order: null,
+ ...mappedDrop,
+ context_profile_context:
+ mappedDrop.context_profile_context ?? FALLBACK_CONTEXT_PROFILE_CONTEXT,
+ };
+};
+
+export async function fetchWaveCurationDropsV2({
+ wave,
+ curationId,
+ page,
+ pageSize,
+ signal,
+}: FetchWaveCurationDropsV2Props): Promise {
+ const waveMin = normalizeWaveMin(wave);
+ const response = await commonApiFetch({
+ endpoint: `v2/waves/${waveMin.id}/curations/${curationId}/drops`,
+ params: {
+ page: page.toString(),
+ page_size: pageSize.toString(),
+ },
+ signal,
+ });
+
+ return {
+ data: response.data.map((drop) => mapCurationDropV2(drop, waveMin)),
+ page: response.page,
+ next: response.next,
+ };
+}
diff --git a/services/api/wave-decisions-v2-api.ts b/services/api/wave-decisions-v2-api.ts
new file mode 100644
index 0000000000..e2e2072d94
--- /dev/null
+++ b/services/api/wave-decisions-v2-api.ts
@@ -0,0 +1,161 @@
+import type { ApiDrop } from "@/generated/models/ApiDrop";
+import type { ApiDropWinningContext } from "@/generated/models/ApiDropWinningContext";
+import { ApiDropType } from "@/generated/models/ApiDropType";
+import type { ApiDropV2 } from "@/generated/models/ApiDropV2";
+import type { ApiWave } from "@/generated/models/ApiWave";
+import type { ApiWaveDecision } from "@/generated/models/ApiWaveDecision";
+import type { ApiWaveDecisionAward } from "@/generated/models/ApiWaveDecisionAward";
+import type { ApiWaveDecisionV2 } from "@/generated/models/ApiWaveDecisionV2";
+import type { ApiWaveDecisionWinner } from "@/generated/models/ApiWaveDecisionWinner";
+import type { ApiWaveDecisionWinnerV2 } from "@/generated/models/ApiWaveDecisionWinnerV2";
+import type { ApiWaveDecisionsPage } from "@/generated/models/ApiWaveDecisionsPage";
+import type { ApiWaveDecisionsPageV2 } from "@/generated/models/ApiWaveDecisionsPageV2";
+import type { ApiWaveMin } from "@/generated/models/ApiWaveMin";
+import { commonApiFetch } from "@/services/api/common-api";
+import {
+ createBasePart,
+ getContextProfileContext,
+ getWaveMin,
+ mapDropReactionCountersV2,
+ mapIdentityOverviewToProfileMin,
+ mapMentionedWaves,
+ mapReplyToDrop,
+} from "@/services/api/drop-v2-mappers";
+
+interface FetchWaveDecisionsV2Props {
+ readonly waveId: string;
+ readonly params: Record;
+ readonly wave?: ApiWave | ApiWaveMin | undefined;
+ readonly signal?: AbortSignal | undefined;
+}
+
+const getDecisionWinningContext = ({
+ awards,
+ decisionTime,
+ place,
+}: {
+ readonly awards: ApiWaveDecisionAward[];
+ readonly decisionTime: number;
+ readonly place: number;
+}): ApiDropWinningContext => ({
+ place,
+ awards,
+ decision_time: decisionTime,
+ sale_time: null,
+ sale_price: null,
+ sale_price_currency: null,
+});
+
+const mapDecisionDropV2 = ({
+ awards,
+ decisionTime,
+ drop,
+ place,
+ wave,
+}: {
+ readonly awards: ApiWaveDecisionAward[];
+ readonly decisionTime: number;
+ readonly drop: ApiDropV2;
+ readonly place: number;
+ readonly wave: ApiWaveMin;
+}): ApiDrop => {
+ const voting = drop.submission_context?.voting;
+ const replyTo = mapReplyToDrop(drop);
+
+ return {
+ id: drop.id,
+ serial_no: drop.serial_no,
+ drop_type: ApiDropType.Winner,
+ rank: place,
+ winning_context: getDecisionWinningContext({
+ awards,
+ decisionTime,
+ place,
+ }),
+ wave,
+ ...(replyTo ? { reply_to: replyTo } : {}),
+ author: mapIdentityOverviewToProfileMin(drop.author),
+ created_at: drop.created_at,
+ updated_at: drop.updated_at ?? null,
+ title: drop.title ?? null,
+ parts: [createBasePart(drop)],
+ parts_count: 1,
+ referenced_nfts: drop.referenced_nfts ?? [],
+ mentioned_users: drop.mentioned_users ?? [],
+ mentioned_groups: drop.mentioned_groups ?? [],
+ mentioned_waves: mapMentionedWaves(drop, wave),
+ metadata: [],
+ rating: voting?.current_calculated_vote ?? 0,
+ realtime_rating: voting?.current_calculated_vote ?? 0,
+ rating_prediction: voting?.predicted_final_vote ?? 0,
+ top_raters: [],
+ raters_count: voting?.voters_count ?? 0,
+ context_profile_context: getContextProfileContext(drop),
+ subscribed_actions: [],
+ is_signed: drop.is_signed,
+ reactions: mapDropReactionCountersV2(drop),
+ boosts: drop.boosts,
+ hide_link_preview: drop.hide_link_preview,
+ nft_links: drop.nft_links ?? [],
+ };
+};
+
+const mapDecisionWinnerV2 = ({
+ decisionTime,
+ wave,
+ winner,
+}: {
+ readonly decisionTime: number;
+ readonly wave: ApiWaveMin;
+ readonly winner: ApiWaveDecisionWinnerV2;
+}): ApiWaveDecisionWinner => ({
+ place: winner.place,
+ awards: winner.awards,
+ drop: mapDecisionDropV2({
+ awards: winner.awards,
+ decisionTime,
+ drop: winner.drop,
+ place: winner.place,
+ wave,
+ }),
+});
+
+const mapDecisionPointV2 = ({
+ decision,
+ wave,
+}: {
+ readonly decision: ApiWaveDecisionV2;
+ readonly wave: ApiWaveMin;
+}): ApiWaveDecision => ({
+ decision_time: decision.decision_time,
+ winners: decision.winners.map((winner) =>
+ mapDecisionWinnerV2({
+ decisionTime: decision.decision_time,
+ wave,
+ winner,
+ })
+ ),
+});
+
+export async function fetchWaveDecisionsV2({
+ waveId,
+ params,
+ wave,
+ signal,
+}: FetchWaveDecisionsV2Props): Promise {
+ const data = await commonApiFetch({
+ endpoint: `v2/waves/${waveId}/decisions`,
+ params,
+ ...(signal ? { signal } : {}),
+ });
+ const waveMin = getWaveMin(wave, waveId);
+
+ return {
+ data: data.data.map((decision) =>
+ mapDecisionPointV2({ decision, wave: waveMin })
+ ),
+ count: data.count,
+ page: data.page,
+ next: data.next,
+ };
+}
diff --git a/services/api/wave-drops-v2-api.ts b/services/api/wave-drops-v2-api.ts
new file mode 100644
index 0000000000..bbae88f1b2
--- /dev/null
+++ b/services/api/wave-drops-v2-api.ts
@@ -0,0 +1,583 @@
+import type { ApiDrop } from "@/generated/models/ApiDrop";
+import type { ApiDropAndWave } from "@/generated/models/ApiDropAndWave";
+import type { ApiDropsLeaderboardPage } from "@/generated/models/ApiDropsLeaderboardPage";
+import type { ApiDropsLeaderboardPageV2 } from "@/generated/models/ApiDropsLeaderboardPageV2";
+import type { ApiDropMetadataResponse } from "@/generated/models/ApiDropMetadataResponse";
+import type { ApiDropWithoutWave } from "@/generated/models/ApiDropWithoutWave";
+import type { ApiDropPart } from "@/generated/models/ApiDropPart";
+import type { ApiDropPartV2 } from "@/generated/models/ApiDropPartV2";
+import type { ApiDropRater } from "@/generated/models/ApiDropRater";
+import type { ApiDropReaction } from "@/generated/models/ApiDropReaction";
+import type { ApiDropReactionV2 } from "@/generated/models/ApiDropReactionV2";
+import type { ApiDropSearchStrategy } from "@/generated/models/ApiDropSearchStrategy";
+import { ApiDropType } from "@/generated/models/ApiDropType";
+import type { ApiDropV2 } from "@/generated/models/ApiDropV2";
+import type { ApiDropV2Page } from "@/generated/models/ApiDropV2Page";
+import type { ApiDropV2PageWithoutCount } from "@/generated/models/ApiDropV2PageWithoutCount";
+import type { ApiDropVotersPage } from "@/generated/models/ApiDropVotersPage";
+import type { ApiDropWithoutWavesPageWithoutCount } from "@/generated/models/ApiDropWithoutWavesPageWithoutCount";
+import { ApiSubmissionDropStatus } from "@/generated/models/ApiSubmissionDropStatus";
+import type { ApiWaveDropsFeed } from "@/generated/models/ApiWaveDropsFeed";
+import type { ApiWaveMin } from "@/generated/models/ApiWaveMin";
+import type { ApiWaveDropsFeedV2 } from "@/generated/models/ApiWaveDropsFeedV2";
+import type { ApiWave } from "@/generated/models/ApiWave";
+import { ApiDropMainType } from "@/generated/models/ApiDropMainType";
+import {
+ commonApiFetch,
+ commonApiFetchWithRetry,
+} from "@/services/api/common-api";
+import {
+ createBasePart,
+ getContextProfileContext,
+ mapApiWaveOverviewToApiWaveMin,
+ mapDropPartV2ToApiDropPart,
+ mapDropReactionCountersV2,
+ mapIdentityOverviewToProfileMin,
+ mapMentionedWaves,
+ mapReplyToDrop,
+ normalizeWaveMin,
+} from "@/services/api/drop-v2-mappers";
+
+const DEFAULT_RETRY_OPTIONS = {
+ maxRetries: 2,
+ initialDelayMs: 300,
+ backoffFactor: 1.5,
+ jitter: 0.1,
+} as const;
+
+interface FetchWaveDropsV2Props {
+ readonly waveId: string;
+ readonly limit: number;
+ readonly serialNoLimit?: number | null | undefined;
+ readonly searchStrategy?: ApiDropSearchStrategy | undefined;
+ readonly dropType?: ApiDropType | undefined;
+ readonly signal?: AbortSignal | undefined;
+ readonly headers?: Record | undefined;
+ readonly withRetry?: boolean | undefined;
+}
+
+interface FetchBoostedDropsV2Props {
+ readonly waveId: string;
+ readonly wave: ApiWave | ApiWaveMin;
+ readonly limit: number;
+ readonly sortDirection?: string | undefined;
+ readonly sort?: string | undefined;
+ readonly countOnlyBoostsAfter?: number | undefined;
+}
+
+interface FetchDropRepliesV2Props {
+ readonly parentDropId: string;
+ readonly page: number;
+ readonly pageSize: number;
+ readonly wave?: ApiWave | ApiWaveMin | undefined;
+ readonly signal?: AbortSignal | undefined;
+}
+
+interface FetchWaveLeaderboardV2Props {
+ readonly waveId: string;
+ readonly params: Record;
+ readonly signal?: AbortSignal | undefined;
+}
+
+interface FetchWaveDropsSearchV2Props {
+ readonly wave: ApiWave | ApiWaveMin;
+ readonly term: string;
+ readonly page: number;
+ readonly size: number;
+ readonly signal?: AbortSignal | undefined;
+}
+
+export type ApiWaveDropsV2PageFeed = ApiWaveDropsFeed & {
+ readonly count: number;
+ readonly page: number;
+ readonly next: boolean;
+};
+
+const getDropEndpointId = (dropId: string): string =>
+ encodeURIComponent(dropId);
+
+const isAbortFetchError = (error: unknown): boolean => {
+ if (error instanceof DOMException && error.name === "AbortError") {
+ return true;
+ }
+
+ if (error instanceof Error && error.name === "AbortError") {
+ return true;
+ }
+
+ const maybeAbortError = error as
+ | { readonly code?: unknown; readonly name?: unknown }
+ | null
+ | undefined;
+
+ return (
+ maybeAbortError?.name === "AbortError" ||
+ maybeAbortError?.code === "ERR_CANCELED"
+ );
+};
+
+const rethrowAbortFetchError = (error: unknown) => {
+ if (isAbortFetchError(error)) {
+ throw error;
+ }
+};
+
+const fetchDropPartV2 = async ({
+ dropId,
+ partNo,
+ signal,
+}: {
+ readonly dropId: string;
+ readonly partNo: number;
+ readonly signal?: AbortSignal | undefined;
+}): Promise => {
+ try {
+ return await commonApiFetch({
+ endpoint: `v2/drops/${getDropEndpointId(dropId)}/parts/${partNo}`,
+ signal,
+ });
+ } catch (error) {
+ rethrowAbortFetchError(error);
+ return null;
+ }
+};
+
+const hydrateDropParts = async (
+ drop: ApiDropV2,
+ signal?: AbortSignal
+): Promise => {
+ const basePart = createBasePart(drop);
+ const partsCount = Math.max(1, drop.parts_count || 1);
+
+ if (partsCount <= 1) {
+ return [basePart];
+ }
+
+ const fetchedParts = await Promise.all(
+ Array.from({ length: partsCount - 1 }, (_, index) => {
+ const partNo = index + 2;
+ return fetchDropPartV2({ dropId: drop.id, partNo, signal });
+ })
+ );
+
+ const extraParts = fetchedParts
+ .map((part) => (part ? mapDropPartV2ToApiDropPart(part) : null))
+ .filter((part): part is ApiDropPart => !!part);
+
+ return [basePart, ...extraParts];
+};
+
+export const fetchDropReactionDetailsV2 = async (
+ dropId: string,
+ signal?: AbortSignal
+): Promise => {
+ const normalizedDropId = dropId.trim();
+ if (!normalizedDropId) {
+ return [];
+ }
+
+ try {
+ const reactions = await commonApiFetch({
+ endpoint: `v2/drops/${getDropEndpointId(normalizedDropId)}/reactions`,
+ signal,
+ });
+
+ return reactions.map((reaction) => ({
+ reaction: reaction.reaction,
+ profiles: reaction.reactors.map(mapIdentityOverviewToProfileMin),
+ }));
+ } catch (error) {
+ rethrowAbortFetchError(error);
+ return [];
+ }
+};
+
+const fetchDropMetadataV2 = async (
+ drop: ApiDropV2,
+ signal?: AbortSignal
+): Promise => {
+ if (!drop.submission_context?.has_metadata) {
+ return [];
+ }
+
+ try {
+ return await commonApiFetch({
+ endpoint: `v2/drops/${getDropEndpointId(drop.id)}/metadata`,
+ signal,
+ });
+ } catch (error) {
+ rethrowAbortFetchError(error);
+ return [];
+ }
+};
+
+const fetchTopRatersV2 = async (
+ drop: ApiDropV2,
+ signal?: AbortSignal
+): Promise => {
+ const votersCount = drop.submission_context?.voting.voters_count ?? 0;
+ if (votersCount <= 0) {
+ return [];
+ }
+
+ try {
+ const voters = await commonApiFetch({
+ endpoint: `v2/drops/${getDropEndpointId(drop.id)}/votes`,
+ params: {
+ page_size: "5",
+ page: "1",
+ sort_direction: "DESC",
+ },
+ signal,
+ });
+
+ return voters.data.map((voter) => ({
+ profile: mapIdentityOverviewToProfileMin(voter.voter),
+ rating: voter.vote,
+ }));
+ } catch (error) {
+ rethrowAbortFetchError(error);
+ return [];
+ }
+};
+
+const getDropType = (drop: ApiDropV2): ApiDropType => {
+ if (drop.drop_type === ApiDropMainType.Chat) {
+ return ApiDropType.Chat;
+ }
+
+ if (drop.submission_context?.status === ApiSubmissionDropStatus.Winner) {
+ return ApiDropType.Winner;
+ }
+
+ return ApiDropType.Participatory;
+};
+
+const getWinningContext = (drop: ApiDropV2) => {
+ const voting = drop.submission_context?.voting;
+ if (drop.submission_context?.status !== ApiSubmissionDropStatus.Winner) {
+ return undefined;
+ }
+
+ return {
+ place: voting?.place ?? 0,
+ awards: [],
+ decision_time: 0,
+ sale_time: null,
+ sale_price: null,
+ sale_price_currency: null,
+ };
+};
+
+const hydrateDropV2 = async ({
+ drop,
+ wave,
+ signal,
+ includeTopRaters = true,
+}: {
+ readonly drop: ApiDropV2;
+ readonly wave: ApiWaveMin;
+ readonly signal?: AbortSignal | undefined;
+ readonly includeTopRaters?: boolean | undefined;
+}): Promise => {
+ const [parts, metadata, topRaters] = await Promise.all([
+ hydrateDropParts(drop, signal),
+ fetchDropMetadataV2(drop, signal),
+ includeTopRaters ? fetchTopRatersV2(drop, signal) : Promise.resolve([]),
+ ]);
+ const voting = drop.submission_context?.voting;
+ const dropType = getDropType(drop);
+ const winningContext = getWinningContext(drop);
+ const replyTo = mapReplyToDrop(drop);
+
+ return {
+ id: drop.id,
+ serial_no: drop.serial_no,
+ drop_type: dropType,
+ rank: voting?.place ?? null,
+ ...(winningContext ? { winning_context: winningContext } : {}),
+ wave,
+ ...(replyTo ? { reply_to: replyTo } : {}),
+ author: mapIdentityOverviewToProfileMin(drop.author),
+ created_at: drop.created_at,
+ updated_at: drop.updated_at ?? null,
+ title: drop.title ?? null,
+ parts,
+ parts_count: drop.parts_count,
+ referenced_nfts: drop.referenced_nfts ?? [],
+ mentioned_users: drop.mentioned_users ?? [],
+ mentioned_groups: drop.mentioned_groups ?? [],
+ mentioned_waves: mapMentionedWaves(drop, wave),
+ metadata,
+ rating: voting?.current_calculated_vote ?? 0,
+ realtime_rating: voting?.current_calculated_vote ?? 0,
+ rating_prediction: voting?.predicted_final_vote ?? 0,
+ top_raters: topRaters,
+ raters_count: voting?.voters_count ?? 0,
+ context_profile_context: getContextProfileContext(drop),
+ subscribed_actions: [],
+ is_signed: drop.is_signed,
+ reactions: mapDropReactionCountersV2(drop),
+ boosts: drop.boosts,
+ hide_link_preview: drop.hide_link_preview,
+ nft_links: drop.nft_links ?? [],
+ };
+};
+
+export const mapLeaderboardDropV2 = ({
+ drop,
+ wave,
+}: {
+ readonly drop: ApiDropV2;
+ readonly wave: ApiWaveMin;
+}): ApiDropWithoutWave => {
+ const voting = drop.submission_context?.voting;
+ const dropType = getDropType(drop);
+ const winningContext = getWinningContext(drop);
+ const replyTo = mapReplyToDrop(drop);
+
+ return {
+ id: drop.id,
+ serial_no: drop.serial_no,
+ drop_type: dropType,
+ rank: voting?.place ?? null,
+ ...(winningContext ? { winning_context: winningContext } : {}),
+ ...(replyTo ? { reply_to: replyTo } : {}),
+ author: mapIdentityOverviewToProfileMin(drop.author),
+ created_at: drop.created_at,
+ updated_at: drop.updated_at ?? null,
+ title: drop.title ?? null,
+ parts: [createBasePart(drop)],
+ parts_count: 1,
+ referenced_nfts: drop.referenced_nfts ?? [],
+ mentioned_users: drop.mentioned_users ?? [],
+ mentioned_groups: drop.mentioned_groups ?? [],
+ mentioned_waves: mapMentionedWaves(drop, wave),
+ metadata: [],
+ rating: voting?.current_calculated_vote ?? 0,
+ realtime_rating: voting?.current_calculated_vote ?? 0,
+ rating_prediction: voting?.predicted_final_vote ?? 0,
+ top_raters: [],
+ raters_count: voting?.voters_count ?? 0,
+ context_profile_context: getContextProfileContext(drop),
+ subscribed_actions: [],
+ is_signed: drop.is_signed,
+ reactions: mapDropReactionCountersV2(drop),
+ boosts: drop.boosts,
+ hide_link_preview: drop.hide_link_preview,
+ nft_links: drop.nft_links ?? [],
+ };
+};
+
+const hydrateDropsV2 = async ({
+ drops,
+ wave,
+ signal,
+}: {
+ readonly drops: ApiDropV2[];
+ readonly wave: ApiWaveMin;
+ readonly signal?: AbortSignal | undefined;
+}): Promise =>
+ Promise.all(drops.map((drop) => hydrateDropV2({ drop, wave, signal })));
+
+const getNormalizedDropId = (dropId: string): string => {
+ const normalizedDropId = dropId.trim();
+ if (!normalizedDropId) {
+ throw new Error("Cannot fetch drop without a drop id");
+ }
+ return normalizedDropId;
+};
+
+const fetchDropAndWaveV2 = async (
+ dropId: string,
+ signal?: AbortSignal
+): Promise =>
+ commonApiFetch({
+ endpoint: `v2/drops/${getDropEndpointId(getNormalizedDropId(dropId))}`,
+ signal,
+ });
+
+export async function fetchWaveDropsFeedV2({
+ waveId,
+ limit,
+ serialNoLimit,
+ searchStrategy,
+ dropType,
+ signal,
+ headers,
+ withRetry = false,
+}: FetchWaveDropsV2Props): Promise {
+ const params: Record = {
+ limit: limit.toString(),
+ };
+
+ if (typeof serialNoLimit === "number") {
+ params["serial_no_limit"] = `${serialNoLimit}`;
+ }
+
+ if (searchStrategy !== undefined) {
+ params["search_strategy"] = searchStrategy;
+ }
+
+ if (dropType !== undefined) {
+ params["drop_type"] = dropType;
+ }
+
+ const request = {
+ endpoint: `v2/waves/${waveId}/drops`,
+ params,
+ signal,
+ headers,
+ };
+
+ const data = withRetry
+ ? await commonApiFetchWithRetry({
+ ...request,
+ retryOptions: DEFAULT_RETRY_OPTIONS,
+ })
+ : await commonApiFetch(request);
+
+ const wave = mapApiWaveOverviewToApiWaveMin(data.wave);
+ const drops = await hydrateDropsV2({
+ drops: data.drops,
+ wave,
+ signal,
+ });
+
+ return {
+ wave,
+ drops,
+ };
+}
+
+export async function fetchWaveLeaderboardV2({
+ waveId,
+ params,
+ signal,
+}: FetchWaveLeaderboardV2Props): Promise {
+ const data = await commonApiFetch({
+ endpoint: `v2/waves/${waveId}/leaderboard`,
+ params,
+ signal,
+ });
+
+ return {
+ wave: data.wave,
+ drops: data.drops.map((drop) =>
+ mapLeaderboardDropV2({ drop, wave: data.wave })
+ ),
+ count: data.count,
+ page: data.page,
+ next: data.next,
+ };
+}
+
+export async function fetchWaveDropsSearchV2({
+ wave,
+ term,
+ page,
+ size,
+ signal,
+}: FetchWaveDropsSearchV2Props): Promise {
+ const waveMin = normalizeWaveMin(wave);
+ const response = await commonApiFetch({
+ endpoint: `v2/waves/${waveMin.id}/search`,
+ params: {
+ term,
+ page: page.toString(),
+ size: size.toString(),
+ },
+ signal,
+ });
+
+ return {
+ data: response.data.map((drop) =>
+ mapLeaderboardDropV2({ drop, wave: waveMin })
+ ),
+ page: response.page,
+ next: response.next,
+ };
+}
+
+export async function fetchDropV2ById(
+ dropId: string,
+ signal?: AbortSignal,
+ options?: { readonly includeTopRaters?: boolean | undefined }
+): Promise {
+ const data = await fetchDropAndWaveV2(dropId, signal);
+ const wave = mapApiWaveOverviewToApiWaveMin(data.wave);
+ return hydrateDropV2({
+ drop: data.drop,
+ wave,
+ signal,
+ includeTopRaters: options?.includeTopRaters,
+ });
+}
+
+export async function fetchDropRepliesV2({
+ parentDropId,
+ page,
+ pageSize,
+ wave,
+ signal,
+}: FetchDropRepliesV2Props): Promise {
+ const normalizedParentDropId = getNormalizedDropId(parentDropId);
+ const response = await commonApiFetch({
+ endpoint: "v2/drops",
+ params: {
+ parent_drop_id: normalizedParentDropId,
+ page: page.toString(),
+ page_size: pageSize.toString(),
+ },
+ signal,
+ });
+
+ const waveMin = wave
+ ? normalizeWaveMin(wave)
+ : mapApiWaveOverviewToApiWaveMin(
+ (await fetchDropAndWaveV2(normalizedParentDropId, signal)).wave
+ );
+ const drops = await hydrateDropsV2({
+ drops: response.data,
+ wave: waveMin,
+ signal,
+ });
+
+ return {
+ wave: waveMin,
+ drops,
+ count: response.count,
+ page: response.page,
+ next: response.next,
+ };
+}
+
+export async function fetchBoostedDropsV2({
+ waveId,
+ wave,
+ limit,
+ sortDirection = "DESC",
+ sort = "boosts",
+ countOnlyBoostsAfter,
+}: FetchBoostedDropsV2Props): Promise {
+ const params: Record = {
+ wave_id: waveId,
+ sort,
+ sort_direction: sortDirection,
+ page_size: limit.toString(),
+ };
+
+ if (countOnlyBoostsAfter !== undefined) {
+ params["count_only_boosts_after"] = countOnlyBoostsAfter.toString();
+ }
+
+ const response = await commonApiFetch({
+ endpoint: "v2/boosted-drops",
+ params,
+ });
+
+ return hydrateDropsV2({
+ drops: response.data,
+ wave: normalizeWaveMin(wave),
+ });
+}
diff --git a/services/api/waves-v2-api.ts b/services/api/waves-v2-api.ts
new file mode 100644
index 0000000000..a5d6fd7f9c
--- /dev/null
+++ b/services/api/waves-v2-api.ts
@@ -0,0 +1,197 @@
+import type { ApiWave } from "@/generated/models/ApiWave";
+import type { ApiDropMedia } from "@/generated/models/ApiDropMedia";
+import type { ApiWaveOverview } from "@/generated/models/ApiWaveOverview";
+import type { ApiWaveOverviewPage } from "@/generated/models/ApiWaveOverviewPage";
+import { ApiWaveType } from "@/generated/models/ApiWaveType";
+import { ApiWavesV2ListType } from "@/generated/models/ApiWavesV2ListType";
+import type { ApiWavesOverviewType } from "@/generated/models/ApiWavesOverviewType";
+import type { ApiWavesPinFilter } from "@/generated/models/ApiWavesPinFilter";
+import type { SidebarWave, SidebarWavesPage } from "@/types/waves.types";
+import { commonApiFetch } from "./common-api";
+
+interface FetchWavesV2PageProps {
+ readonly page: number;
+ readonly pageSize: number;
+ readonly view?: ApiWavesV2ListType | undefined;
+ readonly overviewType?: ApiWavesOverviewType | undefined;
+ readonly following?: boolean | undefined;
+ readonly directMessage?: boolean | undefined;
+ readonly pinned?: ApiWavesPinFilter | undefined;
+ readonly excludeFollowed?: boolean | undefined;
+ readonly identity?: string | undefined;
+ readonly headers?: Record | undefined;
+}
+
+export interface WavesV2OverviewQueryKeyParams {
+ readonly view: ApiWavesV2ListType.Overview;
+ readonly page_size: number;
+ readonly overview_type: ApiWavesOverviewType;
+ readonly only_waves_followed_by_authenticated_user: boolean;
+ readonly direct_message?: boolean | undefined;
+ readonly pinned?: ApiWavesPinFilter | undefined;
+ readonly viewer_identity?: string | undefined;
+}
+
+export function getWavesV2OverviewQueryKeyParams({
+ overviewType,
+ pageSize,
+ following = false,
+ directMessage,
+ pinned,
+ viewerIdentityKey,
+}: {
+ readonly overviewType: ApiWavesOverviewType;
+ readonly pageSize: number;
+ readonly following?: boolean | undefined;
+ readonly directMessage?: boolean | undefined;
+ readonly pinned?: ApiWavesPinFilter | undefined;
+ readonly viewerIdentityKey?: string | null | undefined;
+}): WavesV2OverviewQueryKeyParams {
+ const normalizedViewerIdentityKey =
+ viewerIdentityKey?.trim().toLowerCase() ?? null;
+
+ return {
+ view: ApiWavesV2ListType.Overview,
+ page_size: pageSize,
+ overview_type: overviewType,
+ only_waves_followed_by_authenticated_user: following,
+ ...(directMessage === undefined ? {} : { direct_message: directMessage }),
+ ...(pinned === undefined ? {} : { pinned }),
+ ...(normalizedViewerIdentityKey
+ ? { viewer_identity: normalizedViewerIdentityKey }
+ : {}),
+ };
+}
+
+const getWaveOverviewContext = (wave: ApiWaveOverview) =>
+ wave.context_profile_context;
+
+const mapApiWaveOverviewToSidebarWave = (
+ wave: ApiWaveOverview
+): SidebarWave => {
+ const context = getWaveOverviewContext(wave);
+
+ return {
+ id: wave.id,
+ name: wave.name,
+ type: wave.has_competition ? ApiWaveType.Rank : ApiWaveType.Chat,
+ picture: wave.pfp ?? null,
+ contributors:
+ wave.contributors?.map((contributor) => ({
+ pfp: contributor.pfp ?? "",
+ identity: contributor.handle ?? null,
+ })) ?? [],
+ isDirectMessage: wave.is_dm_wave,
+ hasCompetition: wave.has_competition,
+ descriptionDrop: {
+ contents: wave.description_drop.contents ?? null,
+ media: wave.description_drop.media ?? [],
+ },
+ totalDropsCount: wave.total_drops_count,
+ isPrivate: wave.is_private,
+ latestDropTimestamp: wave.last_drop_time,
+ firstUnreadDropSerialNo: context?.first_unread_drop_serial_no ?? null,
+ unreadDropsCount: context?.unread_drops ?? 0,
+ latestReadTimestamp: 0,
+ pinned: context?.pinned ?? false,
+ muted: context?.muted ?? false,
+ subscribed: context?.subscribed ?? false,
+ };
+};
+
+const getApiWaveDescriptionDrop = (
+ wave: ApiWave
+): { contents: string | null; media: ApiDropMedia[] } => {
+ const descriptionParts = wave.description_drop.parts;
+ const contents =
+ descriptionParts
+ .map((part) => part.content?.trim())
+ .filter((content): content is string => Boolean(content))
+ .join("\n\n") || null;
+
+ return {
+ contents,
+ media: descriptionParts.flatMap((part) => part.media),
+ };
+};
+
+export const mapApiWaveToSidebarWave = (wave: ApiWave): SidebarWave => {
+ const isDirectMessage =
+ wave.wave.type === ApiWaveType.Chat &&
+ Boolean(wave.chat.scope.group?.is_direct_message);
+
+ return {
+ id: wave.id,
+ name: wave.name,
+ type: wave.wave.type,
+ picture: wave.picture,
+ contributors: wave.contributors_overview.map((contributor) => ({
+ pfp: contributor.contributor_pfp,
+ identity: contributor.contributor_identity,
+ })),
+ isDirectMessage,
+ hasCompetition: wave.wave.type !== ApiWaveType.Chat,
+ descriptionDrop: getApiWaveDescriptionDrop(wave),
+ totalDropsCount: wave.metrics.drops_count,
+ isPrivate: Boolean(wave.visibility.scope.group) && !isDirectMessage,
+ latestDropTimestamp: wave.metrics.latest_drop_timestamp,
+ firstUnreadDropSerialNo: wave.metrics.first_unread_drop_serial_no ?? null,
+ unreadDropsCount: wave.metrics.your_unread_drops_count,
+ latestReadTimestamp: wave.metrics.your_latest_read_timestamp,
+ pinned: wave.pinned,
+ muted: wave.metrics.muted,
+ subscribed: wave.subscribed_actions.length > 0,
+ };
+};
+
+export async function fetchWavesV2Page({
+ page,
+ pageSize,
+ view = ApiWavesV2ListType.Overview,
+ overviewType,
+ following = false,
+ directMessage,
+ pinned,
+ excludeFollowed,
+ identity,
+ headers,
+}: FetchWavesV2PageProps): Promise {
+ const params: Record = {
+ view,
+ page: `${page}`,
+ page_size: `${pageSize}`,
+ };
+
+ if (view === ApiWavesV2ListType.Overview && overviewType !== undefined) {
+ params["overview_type"] = overviewType;
+ params["only_waves_followed_by_authenticated_user"] = `${following}`;
+ }
+
+ if (directMessage !== undefined) {
+ params["direct_message"] = `${directMessage}`;
+ }
+
+ if (pinned !== undefined) {
+ params["pinned"] = pinned;
+ }
+
+ if (excludeFollowed !== undefined) {
+ params["exclude_followed"] = `${excludeFollowed}`;
+ }
+
+ if (identity !== undefined) {
+ params["identity"] = identity;
+ }
+
+ const response = await commonApiFetch({
+ endpoint: "v2/waves",
+ params,
+ headers,
+ });
+
+ return {
+ waves: response.data.map(mapApiWaveOverviewToSidebarWave),
+ page: response.page,
+ next: response.next,
+ };
+}
diff --git a/types/feed.types.ts b/types/feed.types.ts
index 81d24e4f10..041e34825a 100644
--- a/types/feed.types.ts
+++ b/types/feed.types.ts
@@ -4,6 +4,11 @@ import type { ApiNotificationCause } from "@/generated/models/ApiNotificationCau
import type { ApiNotificationsResponse } from "@/generated/models/ApiNotificationsResponse";
import type { ApiProfileMin } from "@/generated/models/ApiProfileMin";
import type { ApiWave } from "@/generated/models/ApiWave";
+import type { ApiWaveOverview } from "@/generated/models/ApiWaveOverview";
+
+type NotificationWaveOverview = ApiWaveOverview & {
+ readonly is_direct_message?: boolean;
+};
type IFeedItemWaveCreated = {
readonly serial_no: number;
@@ -118,6 +123,7 @@ export type INotificationDropReplied = NotificationBase &
export type INotificationWaveCreated = NotificationBase & {
readonly cause: ApiNotificationCause.WaveCreated;
+ readonly related_wave?: NotificationWaveOverview;
readonly additional_context: {
readonly wave_id: string;
};
diff --git a/types/waves.types.ts b/types/waves.types.ts
index de4b884494..8f69328d29 100644
--- a/types/waves.types.ts
+++ b/types/waves.types.ts
@@ -3,8 +3,8 @@ import type { ApiWaveMetadataType } from "@/generated/models/ApiWaveMetadataType
import type { ApiWaveParticipationRequirement } from "@/generated/models/ApiWaveParticipationRequirement";
import type { ApiWaveParticipationSubmissionStrategy } from "@/generated/models/ApiWaveParticipationSubmissionStrategy";
import type { ApiWaveOutcomeDistributionItem } from "@/generated/models/ApiWaveOutcomeDistributionItem";
-import type { ApiWavesOverviewType } from "@/generated/models/ApiWavesOverviewType";
import type { ApiWaveType } from "@/generated/models/ApiWaveType";
+import type { ApiDropMedia } from "@/generated/models/ApiDropMedia";
export enum MyStreamWaveTab {
CHAT = "CHAT",
@@ -153,13 +153,38 @@ export enum CreateWaveStepStatus {
PENDING = "PENDING",
}
-export interface WavesOverviewParams {
- limit: number;
- offset: number;
- type: ApiWavesOverviewType;
- only_waves_followed_by_authenticated_user?: boolean | undefined;
- /**
- * Filter waves by direct message flag. true -> only DMs, false -> exclude DMs.
- */
- direct_message?: boolean | undefined;
+export interface SidebarWaveContributor {
+ readonly pfp: string;
+ readonly identity: string | null;
+}
+
+export interface SidebarWaveDescriptionDrop {
+ readonly contents: string | null;
+ readonly media: readonly ApiDropMedia[];
+}
+
+export interface SidebarWave {
+ readonly id: string;
+ readonly name: string;
+ readonly type: ApiWaveType;
+ readonly picture: string | null;
+ readonly contributors: readonly SidebarWaveContributor[];
+ readonly isDirectMessage: boolean;
+ readonly hasCompetition: boolean;
+ readonly descriptionDrop: SidebarWaveDescriptionDrop;
+ readonly totalDropsCount: number;
+ readonly isPrivate: boolean;
+ readonly latestDropTimestamp: number | null;
+ readonly firstUnreadDropSerialNo: number | null;
+ readonly unreadDropsCount: number;
+ readonly latestReadTimestamp: number;
+ readonly pinned: boolean;
+ readonly muted: boolean;
+ readonly subscribed: boolean;
+}
+
+export interface SidebarWavesPage {
+ readonly waves: SidebarWave[];
+ readonly page: number;
+ readonly next: boolean;
}