Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('BrainLeftSidebarWave', () => {
render(<BrainLeftSidebarWave wave={baseWave} onHover={onHover} />);
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', () => {
Expand All @@ -101,7 +101,7 @@ describe('BrainLeftSidebarWave', () => {
it('includes firstUnreadDropSerialNo in href when present', () => {
const waveWithUnread = { ...baseWave, id: '3', firstUnreadDropSerialNo: 42 };
render(<BrainLeftSidebarWave wave={waveWithUnread} onHover={onHover} />);
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', () => {
Expand Down
72 changes: 51 additions & 21 deletions components/brain/left-sidebar/waves/BrainLeftSidebarWave.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -32,6 +32,7 @@ const BrainLeftSidebarWave: React.FC<BrainLeftSidebarWaveProps> = ({
isDirectMessage = false,
}) => {
const { activeWave } = useMyStream();
const { id: activeWaveId, set: setActiveWave } = activeWave;
const prefetchWaveData = usePrefetchWaveData();
const { isApp, hasTouchScreen } = useDeviceInfo();
const isDropWave = wave.type !== ApiWaveType.Chat;
Expand All @@ -45,30 +46,42 @@ const BrainLeftSidebarWave: React.FC<BrainLeftSidebarWaveProps> = ({
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}`;
}
}
}
return wave.name;
}, [wave.name, wave.type]);

const activeWaveId = activeWave.id;

const href = useMemo(() => {
if (activeWaveId === wave.id) {
return getWaveHomeRoute({ isDirectMessage, isApp });
}
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;
Expand All @@ -85,22 +98,37 @@ const BrainLeftSidebarWave: React.FC<BrainLeftSidebarWaveProps> = ({
const handleWaveClick = useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
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";
};

Expand All @@ -123,7 +151,9 @@ const BrainLeftSidebarWave: React.FC<BrainLeftSidebarWaveProps> = ({
<div className="tw-relative">
<div
className={`tw-relative tw-size-8 tw-rounded-full tw-transition tw-duration-300 desktop-hover:group-hover:tw-brightness-110 ${getAvatarRingClasses()} ${
isActive ? "tw-opacity-100" : "tw-opacity-80 desktop-hover:group-hover:tw-opacity-100"
isActive
? "tw-opacity-100"
: "tw-opacity-80 desktop-hover:group-hover:tw-opacity-100"
}`}>
<WavePicture
name={wave.name}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import React, { useCallback, useMemo } from 'react';
import type { ReadonlyURLSearchParams } from 'next/navigation';
import type { ReadonlyURLSearchParams } from "next/navigation";
import React, { useCallback, useMemo } from "react";

interface UseWaveNavigationOptions {
readonly basePath: string;
readonly activeWaveId: string | null;
readonly setActiveWave: (
waveId: string | null,
options?: { isDirectMessage?: boolean; serialNo?: number | null }
options?: {
isDirectMessage?: boolean;
serialNo?: number | null;
divider?: number | null;
}
) => void;
readonly onHover: (waveId: string) => void;
readonly prefetchWaveData: (waveId: string) => void;
Expand Down Expand Up @@ -34,18 +38,19 @@ 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) {
return basePath;
}

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]);
Expand Down Expand Up @@ -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 {
Expand Down
19 changes: 14 additions & 5 deletions components/brain/my-stream/MyStreamWave.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,19 @@ const MyStreamWave: React.FC<MyStreamWaveProps> = ({ 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
Expand Down Expand Up @@ -81,7 +85,12 @@ const MyStreamWave: React.FC<MyStreamWaveProps> = ({ waveId }) => {

// Create component instances with wave-specific props and stable measurements
const components: Record<MyStreamWaveTab, JSX.Element> = {
[MyStreamWaveTab.CHAT]: <MyStreamWaveChat wave={wave} />,
[MyStreamWaveTab.CHAT]: (
<MyStreamWaveChat
wave={wave}
firstUnreadSerialNo={enhancedData.firstUnreadSerialNo}
/>
),
[MyStreamWaveTab.LEADERBOARD]: (
<MyStreamWaveLeaderboard wave={wave} onDropClick={onDropClick} />
),
Expand Down
35 changes: 30 additions & 5 deletions components/brain/my-stream/MyStreamWaveChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<MyStreamWaveChatProps> = ({ wave }) => {
const MyStreamWaveChat: React.FC<MyStreamWaveChatProps> = ({
wave,
firstUnreadSerialNo,
}) => {
const router = useRouter();
const searchParams = useSearchParams();
const pathname = usePathname();
Expand All @@ -42,9 +47,14 @@ const MyStreamWaveChat: React.FC<MyStreamWaveChatProps> = ({ wave }) => {
const { isApp } = useDeviceInfo();
const [activeDrop, setActiveDrop] = useState<ActiveDropState | null>(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) {
Expand All @@ -56,15 +66,29 @@ const MyStreamWaveChat: React.FC<MyStreamWaveChatProps> = ({ 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();

Expand Down Expand Up @@ -123,7 +147,8 @@ const MyStreamWaveChat: React.FC<MyStreamWaveChatProps> = ({ wave }) => {
onReply={handleReply}
onQuote={handleQuote}
activeDrop={activeDrop}
initialDrop={initialDrop}
initialDrop={scrollTarget}
dividerSerialNo={dividerTarget}
dropId={null}
isMuted={wave.metrics?.muted ?? false}
/>
Expand Down
Loading