diff --git a/__tests__/components/brain/left-sidebar/waves/BrainLeftSidebarWave.test.tsx b/__tests__/components/brain/left-sidebar/waves/BrainLeftSidebarWave.test.tsx index 27a442e98b..03c60b8acb 100644 --- a/__tests__/components/brain/left-sidebar/waves/BrainLeftSidebarWave.test.tsx +++ b/__tests__/components/brain/left-sidebar/waves/BrainLeftSidebarWave.test.tsx @@ -89,7 +89,7 @@ describe('BrainLeftSidebarWave', () => { render(); const link = screen.getByRole('link'); await userEvent.click(link); - expect(setActiveWave).toHaveBeenCalledWith('1', { isDirectMessage: false, serialNo: null }); + expect(setActiveWave).toHaveBeenCalledWith('1', { isDirectMessage: false, serialNo: null, divider: null }); }); it('shows drop indicators for non-chat waves', () => { @@ -101,7 +101,7 @@ describe('BrainLeftSidebarWave', () => { it('includes firstUnreadDropSerialNo in href when present', () => { const waveWithUnread = { ...baseWave, id: '3', firstUnreadDropSerialNo: 42 }; render(); - expect(screen.getByRole('link')).toHaveAttribute('href', '/waves?wave=3&serialNo=42'); + expect(screen.getByRole('link')).toHaveAttribute('href', '/waves?divider=42&wave=3&serialNo=42'); }); it('does not include serialNo in href when firstUnreadDropSerialNo is null', () => { diff --git a/components/brain/left-sidebar/waves/BrainLeftSidebarWave.tsx b/components/brain/left-sidebar/waves/BrainLeftSidebarWave.tsx index 1994da7dc8..08fb5823c6 100644 --- a/components/brain/left-sidebar/waves/BrainLeftSidebarWave.tsx +++ b/components/brain/left-sidebar/waves/BrainLeftSidebarWave.tsx @@ -1,22 +1,22 @@ "use client"; -import React, { useMemo, useCallback } from "react"; -import Link from "next/link"; -import { usePrefetchWaveData } from "@/hooks/usePrefetchWaveData"; -import { ApiWaveType } from "@/generated/models/ApiWaveType"; import WavePicture from "@/components/waves/WavePicture"; -import BrainLeftSidebarWaveDropTime from "./BrainLeftSidebarWaveDropTime"; import { MinimalWave } from "@/contexts/wave/hooks/useEnhancedWavesList"; -import BrainLeftSidebarWavePin from "./BrainLeftSidebarWavePin"; -import { formatAddress, isValidEthAddress } from "../../../../helpers/Helpers"; -import useDeviceInfo from "../../../../hooks/useDeviceInfo"; import { useMyStream } from "@/contexts/wave/MyStreamContext"; +import { ApiWaveType } from "@/generated/models/ApiWaveType"; +import { usePrefetchWaveData } from "@/hooks/usePrefetchWaveData"; +import { faBellSlash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Link from "next/link"; +import React, { useCallback, useMemo } from "react"; +import { formatAddress, isValidEthAddress } from "../../../../helpers/Helpers"; import { getWaveHomeRoute, getWaveRoute, } from "../../../../helpers/navigation.helpers"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faBellSlash } from "@fortawesome/free-solid-svg-icons"; +import useDeviceInfo from "../../../../hooks/useDeviceInfo"; +import BrainLeftSidebarWaveDropTime from "./BrainLeftSidebarWaveDropTime"; +import BrainLeftSidebarWavePin from "./BrainLeftSidebarWavePin"; interface BrainLeftSidebarWaveProps { readonly wave: MinimalWave; @@ -32,6 +32,7 @@ const BrainLeftSidebarWave: React.FC = ({ isDirectMessage = false, }) => { const { activeWave } = useMyStream(); + const { id: activeWaveId, set: setActiveWave } = activeWave; const prefetchWaveData = usePrefetchWaveData(); const { isApp, hasTouchScreen } = useDeviceInfo(); const isDropWave = wave.type !== ApiWaveType.Chat; @@ -45,10 +46,15 @@ const BrainLeftSidebarWave: React.FC = ({ if (markerIndex !== -1) { const prefix = wave.name.slice(0, markerIndex + marker.length); const addressStart = markerIndex + marker.length; - const candidateAddress = wave.name.slice(addressStart, addressStart + 42); + const candidateAddress = wave.name.slice( + addressStart, + addressStart + 42 + ); if (isValidEthAddress(candidateAddress)) { - const suffix = wave.name.slice(addressStart + candidateAddress.length); + const suffix = wave.name.slice( + addressStart + candidateAddress.length + ); return `${prefix}${formatAddress(candidateAddress)}${suffix}`; } } @@ -56,8 +62,6 @@ const BrainLeftSidebarWave: React.FC = ({ return wave.name; }, [wave.name, wave.type]); - const activeWaveId = activeWave.id; - const href = useMemo(() => { if (activeWaveId === wave.id) { return getWaveHomeRoute({ isDirectMessage, isApp }); @@ -65,10 +69,19 @@ const BrainLeftSidebarWave: React.FC = ({ return getWaveRoute({ waveId: wave.id, serialNo: wave.firstUnreadDropSerialNo ?? undefined, + extraParams: wave.firstUnreadDropSerialNo + ? { divider: String(wave.firstUnreadDropSerialNo) } + : undefined, isDirectMessage, isApp, }); - }, [activeWaveId, isApp, isDirectMessage, wave.id, wave.firstUnreadDropSerialNo]); + }, [ + activeWaveId, + isApp, + isDirectMessage, + wave.id, + wave.firstUnreadDropSerialNo, + ]); const unreadCount = Math.max(wave.unreadDropsCount, wave.newDropsCount.count); const haveNewDrops = unreadCount > 0; @@ -85,22 +98,37 @@ const BrainLeftSidebarWave: React.FC = ({ const handleWaveClick = useCallback( (event: React.MouseEvent) => { if (event.defaultPrevented) return; - if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button === 1) { + if ( + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey || + event.button === 1 + ) { return; } event.preventDefault(); onWaveHover(); const nextWaveId = wave.id === activeWaveId ? null : wave.id; - activeWave.set(nextWaveId, { + setActiveWave(nextWaveId, { isDirectMessage, - serialNo: nextWaveId ? wave.firstUnreadDropSerialNo : undefined, + serialNo: nextWaveId ? wave.firstUnreadDropSerialNo : null, + divider: nextWaveId ? wave.firstUnreadDropSerialNo : null, }); }, - [activeWave.set, activeWaveId, isDirectMessage, onWaveHover, wave.id, wave.firstUnreadDropSerialNo] + [ + setActiveWave, + activeWaveId, + isDirectMessage, + onWaveHover, + wave.id, + wave.firstUnreadDropSerialNo, + ] ); const getAvatarRingClasses = () => { - if (isActive) return "tw-ring-1 tw-ring-offset-2 tw-ring-offset-iron-900 tw-ring-primary-400"; + if (isActive) + return "tw-ring-1 tw-ring-offset-2 tw-ring-offset-iron-900 tw-ring-primary-400"; return "tw-ring-1 tw-ring-iron-700"; }; @@ -123,7 +151,9 @@ const BrainLeftSidebarWave: React.FC = ({
void; readonly onHover: (waveId: string) => void; readonly prefetchWaveData: (waveId: string) => void; @@ -34,8 +38,8 @@ export const useWaveNavigation = ({ hasTouchScreen, firstUnreadDropSerialNo, }: UseWaveNavigationOptions): UseWaveNavigationResult => { - const currentWaveId = activeWaveId ?? searchParams?.get('wave') ?? undefined; - const isDirectMessage = basePath === '/messages'; + const currentWaveId = activeWaveId ?? searchParams?.get("wave") ?? undefined; + const isDirectMessage = basePath === "/messages"; const href = useMemo(() => { if (currentWaveId === waveId) { @@ -43,9 +47,10 @@ export const useWaveNavigation = ({ } const params = new URLSearchParams(); - params.set('wave', waveId); + params.set("wave", waveId); if (firstUnreadDropSerialNo) { - params.set('serialNo', String(firstUnreadDropSerialNo)); + params.set("serialNo", String(firstUnreadDropSerialNo)); + params.set("divider", String(firstUnreadDropSerialNo)); } return `${basePath}?${params.toString()}`; }, [basePath, currentWaveId, waveId, firstUnreadDropSerialNo]); @@ -79,10 +84,18 @@ export const useWaveNavigation = ({ const nextWaveId = waveId === currentWaveId ? null : waveId; setActiveWave(nextWaveId, { isDirectMessage, - serialNo: nextWaveId ? firstUnreadDropSerialNo : undefined, + serialNo: nextWaveId ? firstUnreadDropSerialNo : null, + divider: nextWaveId ? firstUnreadDropSerialNo : null, }); }, - [currentWaveId, isDirectMessage, onMouseEnter, setActiveWave, waveId, firstUnreadDropSerialNo] + [ + currentWaveId, + isDirectMessage, + onMouseEnter, + setActiveWave, + waveId, + firstUnreadDropSerialNo, + ] ); return { diff --git a/components/brain/my-stream/MyStreamWave.tsx b/components/brain/my-stream/MyStreamWave.tsx index c7902be998..70014856d3 100644 --- a/components/brain/my-stream/MyStreamWave.tsx +++ b/components/brain/my-stream/MyStreamWave.tsx @@ -45,15 +45,19 @@ const MyStreamWave: React.FC = ({ waveId }) => { }, }); - // Get new drops count from the waves list - const newDropsCount = useMemo(() => { - // Check both regular waves and direct messages + // Get enhanced data from the waves list (has correct WS-updated values) + const enhancedData = useMemo(() => { const waveFromList = waves.list.find((w) => w.id === waveId) ?? directMessages.list.find((w) => w.id === waveId); - return waveFromList?.newDropsCount.count ?? 0; + return { + newDropsCount: waveFromList?.newDropsCount.count ?? 0, + firstUnreadSerialNo: waveFromList?.firstUnreadDropSerialNo ?? null, + }; }, [waves.list, directMessages.list, waveId]); + const newDropsCount = enhancedData.newDropsCount; + // Update wave data in title context useSetWaveData( wave ? { name: wave.name, newItemsCount: newDropsCount } : null @@ -81,7 +85,12 @@ const MyStreamWave: React.FC = ({ waveId }) => { // Create component instances with wave-specific props and stable measurements const components: Record = { - [MyStreamWaveTab.CHAT]: , + [MyStreamWaveTab.CHAT]: ( + + ), [MyStreamWaveTab.LEADERBOARD]: ( ), diff --git a/components/brain/my-stream/MyStreamWaveChat.tsx b/components/brain/my-stream/MyStreamWaveChat.tsx index fa3d80d886..17c7531b62 100644 --- a/components/brain/my-stream/MyStreamWaveChat.tsx +++ b/components/brain/my-stream/MyStreamWaveChat.tsx @@ -24,13 +24,18 @@ import { useLayout } from "./layout/LayoutContext"; interface InitialDropState { readonly waveId: string; readonly serialNo: number; + readonly dividerSerialNo: number | null; } interface MyStreamWaveChatProps { readonly wave: ApiWave; + readonly firstUnreadSerialNo: number | null; } -const MyStreamWaveChat: React.FC = ({ wave }) => { +const MyStreamWaveChat: React.FC = ({ + wave, + firstUnreadSerialNo, +}) => { const router = useRouter(); const searchParams = useSearchParams(); const pathname = usePathname(); @@ -42,9 +47,14 @@ const MyStreamWaveChat: React.FC = ({ wave }) => { const { isApp } = useDeviceInfo(); const [activeDrop, setActiveDrop] = useState(null); - const initialDrop = + const scrollTarget = initialDropState?.waveId === wave.id ? initialDropState.serialNo : null; + const dividerTarget = + initialDropState?.waveId === wave.id + ? initialDropState.dividerSerialNo + : firstUnreadSerialNo; + useEffect(() => { const dropParam = searchParams?.get("serialNo"); if (!dropParam) { @@ -56,15 +66,29 @@ const MyStreamWaveChat: React.FC = ({ wave }) => { return; } - setInitialDropState({ waveId: wave.id, serialNo: parsed }); + const dividerParam = searchParams?.get("divider"); + const dividerParsed = dividerParam + ? Number.parseInt(dividerParam, 10) + : null; + const dividerSerialNo = + dividerParsed !== null && Number.isFinite(dividerParsed) + ? dividerParsed + : firstUnreadSerialNo; + + setInitialDropState({ + waveId: wave.id, + serialNo: parsed, + dividerSerialNo, + }); const params = new URLSearchParams(searchParams?.toString() || ""); params.delete("serialNo"); + params.delete("divider"); const href = params.toString() ? `${pathname}?${params.toString()}` : pathname || getHomeFeedRoute(); router.replace(href, { scroll: false }); - }, [searchParams, router, pathname, wave.id]); + }, [searchParams, router, pathname, wave.id, firstUnreadSerialNo]); const { waveViewStyle } = useLayout(); @@ -123,7 +147,8 @@ const MyStreamWaveChat: React.FC = ({ wave }) => { onReply={handleReply} onQuote={handleQuote} activeDrop={activeDrop} - initialDrop={initialDrop} + initialDrop={scrollTarget} + dividerSerialNo={dividerTarget} dropId={null} isMuted={wave.metrics?.muted ?? false} /> diff --git a/components/waves/CreateDropContent.tsx b/components/waves/CreateDropContent.tsx index 86674584da..7d030f20ef 100644 --- a/components/waves/CreateDropContent.tsx +++ b/components/waves/CreateDropContent.tsx @@ -74,30 +74,33 @@ import { } from "./utils/getMissingRequirements"; // Use next/dynamic for lazy loading with SSR support -const TermsSignatureFlow = dynamic(() => import("../terms/TermsSignatureFlow")); +const TermsSignatureFlow = dynamic( + () => import("../terms/TermsSignatureFlow"), + { loading: () => null } +); export type CreateDropMetadataType = | { - readonly id: string; - key: string; - readonly type: ApiWaveMetadataType.String; - value: string | null; - readonly required: boolean; - } + readonly id: string; + key: string; + readonly type: ApiWaveMetadataType.String; + value: string | null; + readonly required: boolean; + } | { - readonly id: string; - key: string; - readonly type: ApiWaveMetadataType.Number; - value: number | null; - readonly required: boolean; - } + readonly id: string; + key: string; + readonly type: ApiWaveMetadataType.Number; + value: number | null; + readonly required: boolean; + } | { - readonly id: string; - key: string; - readonly type: null; - value: string | null; - readonly required: boolean; - }; + readonly id: string; + key: string; + readonly type: null; + value: string | null; + readonly required: boolean; + }; interface CreateDropContentProps { readonly activeDrop: ActiveDropState | null; @@ -373,8 +376,10 @@ const getOptimisticDrop = ( author: { id: connectedProfile.id, handle: connectedProfile.handle, - active_main_stage_submission_ids: connectedProfile.active_main_stage_submission_ids, - winner_main_stage_drop_ids: connectedProfile.winner_main_stage_drop_ids ?? [], + active_main_stage_submission_ids: + connectedProfile.active_main_stage_submission_ids, + winner_main_stage_drop_ids: + connectedProfile.winner_main_stage_drop_ids ?? [], pfp: connectedProfile.pfp ?? null, banner1_color: connectedProfile.banner1 ?? null, banner2_color: connectedProfile.banner2 ?? null, @@ -401,9 +406,9 @@ const getOptimisticDrop = ( })), quoted_drop: part.quoted_drop ? { - ...part.quoted_drop, - is_deleted: false, - } + ...part.quoted_drop, + is_deleted: false, + } : null, replies_count: 0, quotes_count: 0, @@ -487,21 +492,18 @@ const CreateDropContent: React.FC = ({ requiredMetadata: wave.participation.required_metadata, }); - const hasMetadata = useMemo( - () => hasMetadataContent(metadata), - [metadata] - ); + const hasMetadata = useMemo(() => hasMetadataContent(metadata), [metadata]); const getMarkdown = useMemo( () => editorState ? exportDropMarkdown(editorState, [ - ...SAFE_MARKDOWN_TRANSFORMERS, - MENTION_TRANSFORMER, - HASHTAG_TRANSFORMER, - IMAGE_TRANSFORMER, - EMOJI_TRANSFORMER, - ]) + ...SAFE_MARKDOWN_TRANSFORMERS, + MENTION_TRANSFORMER, + HASHTAG_TRANSFORMER, + IMAGE_TRANSFORMER, + EMOJI_TRANSFORMER, + ]) : null, [editorState] ); @@ -619,9 +621,9 @@ const CreateDropContent: React.FC = ({ quoted_drop: activeDrop?.action === ActiveDropAction.QUOTE ? { - drop_id: activeDrop.drop.id, - drop_part_id: activeDrop.partId, - } + drop_id: activeDrop.drop.id, + drop_part_id: activeDrop.partId, + } : null, media: files, }, @@ -643,22 +645,23 @@ const CreateDropContent: React.FC = ({ const hasPartsInDrop = (drop?.parts.length ?? 0) > 0; const hasCurrentContent = !!(markdown?.trim().length || files.length); - const newParts = hasPartsInDrop && !hasCurrentContent - ? drop?.parts ?? [] - : [ - ...(drop?.parts ?? []), - { - content: markdown?.length ? markdown : null, - quoted_drop: - activeDrop?.action === ActiveDropAction.QUOTE - ? { - drop_id: activeDrop.drop.id, - drop_part_id: activeDrop.partId, - } - : null, - media: files, - }, - ]; + const newParts = + hasPartsInDrop && !hasCurrentContent + ? drop?.parts ?? [] + : [ + ...(drop?.parts ?? []), + { + content: markdown?.length ? markdown : null, + quoted_drop: + activeDrop?.action === ActiveDropAction.QUOTE + ? { + drop_id: activeDrop.drop.id, + drop_part_id: activeDrop.partId, + } + : null, + media: files, + }, + ]; const parts = ensurePartsWithFallback(newParts, hasMetadata); @@ -852,7 +855,7 @@ const CreateDropContent: React.FC = ({ import("@capacitor/core").then(({ Capacitor }) => { if (Capacitor.getPlatform() === "android") { import("@capacitor/keyboard").then(({ Keyboard }) => { - Keyboard.hide().catch(() => { }); + Keyboard.hide().catch(() => {}); }); } }); @@ -972,8 +975,9 @@ const CreateDropContent: React.FC = ({ updatedFiles = updatedFiles.slice(-MAX_DROP_UPLOAD_FILES); setToast({ - message: `File limit exceeded. The ${removedCount} oldest file${removedCount > 1 ? "s were" : " was" - } removed to maintain the ${MAX_DROP_UPLOAD_FILES}-file limit. New files have been added.`, + message: `File limit exceeded. The ${removedCount} oldest file${ + removedCount > 1 ? "s were" : " was" + } removed to maintain the ${MAX_DROP_UPLOAD_FILES}-file limit. New files have been added.`, type: "warning", }); } @@ -981,12 +985,15 @@ const CreateDropContent: React.FC = ({ setFiles(updatedFiles); }; - const handleEditorStateChange = useCallback((newEditorState: EditorState) => { - setEditorState(newEditorState); - if (!isWideContainer) { - setShowOptions(false); - } - }, [isWideContainer]); + const handleEditorStateChange = useCallback( + (newEditorState: EditorState) => { + setEditorState(newEditorState); + if (!isWideContainer) { + setShowOptions(false); + } + }, + [isWideContainer] + ); const removeFile = (file: File, partIndex?: number) => { if (partIndex !== undefined) { @@ -1079,7 +1086,8 @@ const CreateDropContent: React.FC = ({ } }, [isApp, editingDropId, activeDrop, onCancelReplyQuote]); - const isChatClosed = wave.wave.type === ApiWaveType.Chat && !wave.chat.enabled; + const isChatClosed = + wave.wave.type === ApiWaveType.Chat && !wave.chat.enabled; if (isChatClosed) { return ( @@ -1167,8 +1175,7 @@ const CreateDropContent: React.FC = ({ initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }} - transition={{ duration: 0.3 }} - > + transition={{ duration: 0.3 }}> = ({ removeFile={removeFile} disabled={submitting} /> - - Loading Terms...
}> - - +
); }; diff --git a/components/waves/drops/wave-drops-all/index.tsx b/components/waves/drops/wave-drops-all/index.tsx index f4272ee04a..8021b500ab 100644 --- a/components/waves/drops/wave-drops-all/index.tsx +++ b/components/waves/drops/wave-drops-all/index.tsx @@ -45,6 +45,7 @@ interface WaveDropsAllProps { }) => void; readonly activeDrop: ActiveDropState | null; readonly initialDrop: number | null; + readonly dividerSerialNo?: number | null; readonly onDropContentClick?: (drop: ExtendedDrop) => void; readonly bottomPaddingClassName?: string; readonly isMuted?: boolean; @@ -57,6 +58,7 @@ const WaveDropsAllInner: React.FC = ({ onQuote, activeDrop, initialDrop, + dividerSerialNo, onDropContentClick, bottomPaddingClassName, isMuted = false, @@ -70,8 +72,7 @@ const WaveDropsAllInner: React.FC = ({ const { waveMessages, fetchNextPage, waitAndRevealDrop } = useVirtualizedWaveDrops(waveId, dropId); - const { unreadDividerSerialNo, setUnreadDividerSerialNo } = - useUnreadDivider(); + const { setUnreadDividerSerialNo } = useUnreadDivider(); const typingMessage = useWaveIsTyping( waveId, @@ -112,12 +113,8 @@ const WaveDropsAllInner: React.FC = ({ useEffect(() => { setVisibleLatestSerial(null); prevLatestSerialNoRef.current = null; - if (initialDrop === null) { - setUnreadDividerSerialNo(null); - } else { - setUnreadDividerSerialNo(initialDrop); - } - }, [waveId, initialDrop, setUnreadDividerSerialNo]); + setUnreadDividerSerialNo(dividerSerialNo ?? null); + }, [waveId, dividerSerialNo, setUnreadDividerSerialNo]); const latestSerialNo = waveMessages?.drops?.[0]?.serial_no ?? null; @@ -139,17 +136,6 @@ const WaveDropsAllInner: React.FC = ({ } }, [latestSerialNo, isAtBottom, setUnreadDividerSerialNo]); - const wasNotAtBottomRef = useRef(false); - - useEffect(() => { - if (!isAtBottom) { - wasNotAtBottomRef.current = true; - } else if (wasNotAtBottomRef.current && unreadDividerSerialNo !== null) { - setUnreadDividerSerialNo(null); - wasNotAtBottomRef.current = false; - } - }, [isAtBottom, unreadDividerSerialNo, setUnreadDividerSerialNo]); - useEffect(() => { if (latestSerialNo === null) { return; @@ -329,13 +315,14 @@ const WaveDropsAll: React.FC = ({ onQuote, activeDrop, initialDrop, + dividerSerialNo, onDropContentClick, bottomPaddingClassName, isMuted = false, }) => { return ( = ({ onQuote={onQuote} activeDrop={activeDrop} initialDrop={initialDrop} + dividerSerialNo={dividerSerialNo} onDropContentClick={onDropContentClick} bottomPaddingClassName={bottomPaddingClassName} isMuted={isMuted} diff --git a/contexts/wave/MyStreamContext.tsx b/contexts/wave/MyStreamContext.tsx index 7f7417f8bb..9fea45a9d0 100644 --- a/contexts/wave/MyStreamContext.tsx +++ b/contexts/wave/MyStreamContext.tsx @@ -52,6 +52,7 @@ interface ActiveWaveContextData { isDirectMessage?: boolean; replace?: boolean; serialNo?: number | string | null; + divider?: number | null; } ) => void; } diff --git a/contexts/wave/hooks/useActiveWaveManager.ts b/contexts/wave/hooks/useActiveWaveManager.ts index c9b94e08b4..692214954d 100644 --- a/contexts/wave/hooks/useActiveWaveManager.ts +++ b/contexts/wave/hooks/useActiveWaveManager.ts @@ -1,17 +1,15 @@ "use client"; -import { useMemo, useCallback } from "react"; -import { useSearchParams } from "next/navigation"; -import useDeviceInfo from "@/hooks/useDeviceInfo"; +import { getWaveHomeRoute, getWaveRoute } from "@/helpers/navigation.helpers"; import { useClientNavigation } from "@/hooks/useClientNavigation"; -import { - getWaveHomeRoute, - getWaveRoute, -} from "@/helpers/navigation.helpers"; +import useDeviceInfo from "@/hooks/useDeviceInfo"; +import { useSearchParams } from "next/navigation"; +import { useCallback, useMemo } from "react"; interface WaveNavigationOptions { isDirectMessage?: boolean; serialNo?: number | string | null; + divider?: number | null; } const getWaveFromWindow = (): string | null => { @@ -47,8 +45,20 @@ export function useActiveWaveManager() { (waveId: string | null, options?: WaveNavigationOptions) => { const isDirectMessage = options?.isDirectMessage ?? false; const serialNo = options?.serialNo ?? undefined; + const divider = options?.divider; return waveId - ? getWaveRoute({ waveId, serialNo, isDirectMessage, isApp }) + ? getWaveRoute({ + waveId, + serialNo, + extraParams: { + divider: + divider !== null && divider !== undefined + ? String(divider) + : undefined, + }, + isDirectMessage, + isApp, + }) : getWaveHomeRoute({ isDirectMessage, isApp }); }, [isApp] diff --git a/contexts/wave/hooks/useEnhancedWavesListCore.ts b/contexts/wave/hooks/useEnhancedWavesListCore.ts index 9a94ef6ff1..10607ab457 100644 --- a/contexts/wave/hooks/useEnhancedWavesListCore.ts +++ b/contexts/wave/hooks/useEnhancedWavesListCore.ts @@ -123,9 +123,12 @@ function useEnhancedWavesListCore( const forcedCount = forcedUnreadCounts[wave.id]; const apiFirstUnread = wave.metrics.first_unread_drop_serial_no ?? null; const wsFirstUnread = wsData?.firstUnreadSerialNo ?? null; + const wasCleared = clearedUnreadWaveIds.has(wave.id); let firstUnreadDropSerialNo: number | null = null; if (!isCleared) { - if (apiFirstUnread !== null && wsFirstUnread !== null) { + if (wasCleared && hasNewWsDrops) { + firstUnreadDropSerialNo = wsFirstUnread; + } else if (apiFirstUnread !== null && wsFirstUnread !== null) { firstUnreadDropSerialNo = Math.min(apiFirstUnread, wsFirstUnread); } else { firstUnreadDropSerialNo = apiFirstUnread ?? wsFirstUnread; @@ -137,6 +140,8 @@ function useEnhancedWavesListCore( unreadDropsCount = 0; } else if (forcedCount !== undefined) { unreadDropsCount = forcedCount + (wsData?.count ?? 0); + } else if (wasCleared && hasNewWsDrops) { + unreadDropsCount = wsData?.count ?? 0; } else if (hasNewWsDrops) { unreadDropsCount = wave.metrics.your_unread_drops_count + (wsData?.count ?? 0);