Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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 @@ -2,34 +2,35 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import BrainLeftSidebarWave from '@/components/brain/left-sidebar/waves/BrainLeftSidebarWave';
import { ApiWaveType } from '@/generated/models/ApiWaveType';
import { useRouter, useSearchParams } from 'next/navigation';
import { usePrefetchWaveData } from '@/hooks/usePrefetchWaveData';
import { useMyStream } from '@/contexts/wave/MyStreamContext';

jest.mock('next/link', () => ({
__esModule: true,
default: ({ href, children, onMouseEnter, onClick, className }: any) => (
<a href={href} onMouseEnter={onMouseEnter} onClick={onClick} className={className}>{children}</a>
),
}));
jest.mock('next/navigation', () => ({
useRouter: jest.fn(),
useSearchParams: jest.fn(),
usePathname: jest.fn(),
jest.mock('@/hooks/useDeviceInfo', () => ({
__esModule: true,
default: () => ({ isApp: false }),
}));
jest.mock('@/contexts/wave/MyStreamContext', () => ({
useMyStream: jest.fn(),
}));
jest.mock('@/hooks/usePrefetchWaveData');
jest.mock('@/components/waves/WavePicture', () => (props: any) => <img data-testid="wave-picture" alt={props.name} />);
jest.mock('@/components/brain/left-sidebar/waves/BrainLeftSidebarWaveDropTime', () => (props: any) => <span data-testid="drop-time">{props.time}</span>);
jest.mock('@/components/brain/left-sidebar/waves/BrainLeftSidebarWavePin', () => (props: any) => <div data-testid="pin">{String(props.isPinned)}</div>);

const mockedUseRouter = useRouter as jest.Mock;
const mockedUseSearchParams = useSearchParams as jest.Mock;
const mockedPrefetch = usePrefetchWaveData as jest.Mock;
const mockedUseMyStream = useMyStream as jest.Mock;

describe('BrainLeftSidebarWave', () => {
const prefetch = jest.fn();
const onHover = jest.fn();
const router = { push: jest.fn() };
const searchParams = new URLSearchParams();
const setActiveWave = jest.fn();
let activeWaveId: string | null = null;

const baseWave = {
id: '1',
Expand All @@ -43,10 +44,11 @@ describe('BrainLeftSidebarWave', () => {

beforeEach(() => {
jest.clearAllMocks();
mockedUseRouter.mockReturnValue(router);
mockedUseSearchParams.mockReturnValue(searchParams);
mockedPrefetch.mockReturnValue(prefetch);
searchParams.delete('wave'); // Clear any previous wave param
activeWaveId = null;
mockedUseMyStream.mockImplementation(() => ({
activeWave: { id: activeWaveId, set: setActiveWave },
}));
});

it('prefetches wave data on hover when not active', async () => {
Expand All @@ -58,7 +60,10 @@ describe('BrainLeftSidebarWave', () => {
});

it('does not prefetch when hovering active wave', async () => {
searchParams.set('wave', '1');
activeWaveId = '1';
mockedUseMyStream.mockImplementation(() => ({
activeWave: { id: activeWaveId, set: setActiveWave },
}));
render(<BrainLeftSidebarWave wave={baseWave} onHover={onHover} />);
await userEvent.hover(screen.getByRole('link'));
expect(onHover).not.toHaveBeenCalled();
Expand All @@ -68,7 +73,10 @@ describe('BrainLeftSidebarWave', () => {
it('computes href based on current wave', () => {
const { rerender } = render(<BrainLeftSidebarWave wave={baseWave} onHover={onHover} />);
expect(screen.getByRole('link')).toHaveAttribute('href', '/waves?wave=1');
searchParams.set('wave', '1');
activeWaveId = '1';
mockedUseMyStream.mockImplementation(() => ({
activeWave: { id: activeWaveId, set: setActiveWave },
}));
rerender(<BrainLeftSidebarWave wave={baseWave} onHover={onHover} />);
expect(screen.getByRole('link')).toHaveAttribute('href', '/waves');
});
Expand All @@ -77,7 +85,7 @@ describe('BrainLeftSidebarWave', () => {
render(<BrainLeftSidebarWave wave={baseWave} onHover={onHover} />);
const link = screen.getByRole('link');
await userEvent.click(link);
expect(router.push).toHaveBeenCalledWith('/waves?wave=1');
expect(setActiveWave).toHaveBeenCalledWith('1', { isDirectMessage: false });
});

it('shows drop indicators for non-chat waves', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ it("uses app routes when running in app", () => {
render(<MyStreamNoItems />);
expect(screen.getByRole("link", { name: /Explore Waves/i })).toHaveAttribute(
"href",
"/?view=waves"
"/waves"
);
expect(screen.getByRole("link", { name: /Create a Wave/i })).toHaveAttribute(
"href",
Expand Down
9 changes: 9 additions & 0 deletions __tests__/components/header/AppHeader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jest.mock("next/navigation", () => ({
jest.mock("@/components/navigation/ViewContext", () => ({
useViewContext: jest.fn(),
}));
jest.mock("@/contexts/wave/MyStreamContext", () => ({
useMyStreamOptional: jest.fn(),
}));
jest.mock("@/hooks/useWaveById", () => ({ useWaveById: jest.fn() }));
jest.mock("@/contexts/NavigationHistoryContext", () => ({
useNavigationHistoryContext: jest.fn(),
Expand All @@ -47,6 +50,7 @@ const { useAuth } = require("@/components/auth/Auth");
const { useIdentity } = require("@/hooks/useIdentity");
const { useRouter, usePathname, useSearchParams } = require("next/navigation");
const { useViewContext } = require("@/components/navigation/ViewContext");
const { useMyStreamOptional } = require("@/contexts/wave/MyStreamContext");
const { useWaveById } = require("@/hooks/useWaveById");
const {
useNavigationHistoryContext,
Expand All @@ -62,6 +66,11 @@ function setup(opts: any) {
activeView: opts.activeView ?? null,
homeActiveTab: opts.homeActiveTab ?? "latest",
});
(useMyStreamOptional as jest.Mock).mockReturnValue(
opts.wave
? { activeWave: { id: opts.wave.id } }
: { activeWave: { id: null } }
);
(useNavigationHistoryContext as jest.Mock).mockReturnValue({
canGoBack: opts.canGoBack ?? false,
});
Expand Down
32 changes: 20 additions & 12 deletions __tests__/contexts/wave/hooks/useActiveWaveManager.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { renderHook, act } from "@testing-library/react";
import { renderHook, act, waitFor } from "@testing-library/react";
import { useActiveWaveManager } from "@/contexts/wave/hooks/useActiveWaveManager";
import { useRouter, useSearchParams } from "next/navigation";
import { useSearchParams } from "next/navigation";

jest.mock("@/hooks/useDeviceInfo", () => ({
__esModule: true,
Expand All @@ -17,33 +17,41 @@ jest.mock("next/navigation", () => ({
useSearchParams: jest.fn(),
}));

const push = jest.fn();
const router: any = { query: {}, push };
(useRouter as jest.Mock).mockReturnValue(router);

describe("useActiveWaveManager", () => {
it("reads wave from query and updates via setActiveWave", () => {
beforeEach(() => {
jest.restoreAllMocks();
globalThis.history.replaceState(null, "", "http://localhost/");
});

it("reads wave from query and updates via setActiveWave", async () => {
globalThis.history.replaceState(null, "", "http://localhost/waves?wave=abc");
(useSearchParams as jest.Mock).mockReturnValue(
new URLSearchParams({ wave: "abc" })
);

const pushStateSpy = jest.spyOn(globalThis.history, "pushState");
const replaceStateSpy = jest.spyOn(globalThis.history, "replaceState");

const { result, rerender } = renderHook(() => useActiveWaveManager());
expect(result.current.activeWaveId).toBe("abc");
await waitFor(() => expect(result.current.activeWaveId).toBe("abc"));

replaceStateSpy.mockClear();
act(() => {
result.current.setActiveWave("def");
});
expect(push).toHaveBeenLastCalledWith('/waves?wave=def');
expect(pushStateSpy).toHaveBeenLastCalledWith(null, "", "/waves?wave=def");

// Simulate router query change to trigger state update
globalThis.history.replaceState(null, "", "http://localhost/waves?wave=def");
(useSearchParams as jest.Mock).mockReturnValue(
new URLSearchParams({ wave: "def" })
);
rerender();
expect(result.current.activeWaveId).toBe("def");
await waitFor(() => expect(result.current.activeWaveId).toBe("def"));

pushStateSpy.mockClear();
act(() => {
result.current.setActiveWave(null);
});
expect(push).toHaveBeenLastCalledWith('/waves');
expect(pushStateSpy).toHaveBeenLastCalledWith(null, "", "/waves");
});
});
3 changes: 2 additions & 1 deletion app/access/page.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ export default function AccessPage() {
alert("Access Denied!");
} else {
alert("gm!");
const isSecure = globalThis.location?.protocol === "https:";
Cookies.set(API_AUTH_COOKIE, pass, {
expires: 7,
secure: true,
secure: isSecure,
sameSite: "strict",
});
window.location.href = "/";
Expand Down
3 changes: 0 additions & 3 deletions app/waves/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import WavesPageClient from "./page.client";
import { Time } from "@/helpers/time";
import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
import { cookies } from "next/headers";
import { unstable_noStore as noStore } from "next/cache";
import type { Metadata } from "next";
import { formatAddress } from "@/helpers/Helpers";
import { formatCount } from "@/helpers/format.helpers";
Expand Down Expand Up @@ -57,7 +56,6 @@ export default async function WavesPage({
}: {
readonly searchParams: Promise<{ wave?: string; drop?: string }>;
}) {
noStore();
const resolvedParams = await searchParams;
const cookieStore = await cookies();
const context = await fetchWaveContext(resolvedParams.wave ?? null, cookieStore);
Expand Down Expand Up @@ -97,7 +95,6 @@ export async function generateMetadata({
}: {
readonly searchParams: Promise<{ wave?: string }>;
}): Promise<Metadata> {
noStore();
const resolvedParams = await searchParams;
const waveId = resolvedParams.wave ?? null;

Expand Down
5 changes: 4 additions & 1 deletion components/brain/BrainMobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { getHomeFeedRoute } from "@/helpers/navigation.helpers";
import CreateWaveModal from "@/components/waves/create-wave/CreateWaveModal";
import CreateDirectMessageModal from "@/components/waves/create-dm/CreateDirectMessageModal";
import { useAuth } from "@/components/auth/Auth";
import { useMyStreamOptional } from "@/contexts/wave/MyStreamContext";

export enum BrainView {
DEFAULT = "DEFAULT",
Expand All @@ -53,6 +54,7 @@ const BrainMobile: React.FC<Props> = ({ children }) => {
const { isApp } = useDeviceInfo();
const { connectedProfile } = useAuth();
const [hydrated, setHydrated] = useState(false);
const myStream = useMyStreamOptional();

useEffect(() => {
setHydrated(true);
Expand All @@ -70,7 +72,8 @@ const BrainMobile: React.FC<Props> = ({ children }) => {
enabled: !!dropId,
});

const waveId = searchParams?.get('wave') ?? null;
// Use MyStreamContext for waveId to support client-side navigation via pushState
const waveId = myStream?.activeWave.id ?? searchParams?.get('wave') ?? null;
const { data: wave } = useWaveData({
waveId: waveId,
onWaveNotFound: () => {
Expand Down
1 change: 1 addition & 0 deletions components/brain/direct-messages/DirectMessagesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ const DirectMessagesList: React.FC<DirectMessagesListProps> = ({
hidePin
hideHeaders
scrollContainerRef={scrollContainerRef}
isDirectMessage
/>

<UnifiedWavesListLoader
Expand Down
56 changes: 32 additions & 24 deletions components/brain/left-sidebar/waves/BrainLeftSidebarWave.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"use client";

import React, { useMemo } from "react";
import React, { useMemo, useCallback } from "react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { usePrefetchWaveData } from "@/hooks/usePrefetchWaveData";
import { ApiWaveType } from "@/generated/models/ApiWaveType";
import WavePicture from "@/components/waves/WavePicture";
Expand All @@ -11,6 +10,7 @@ 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 {
getWaveHomeRoute,
getWaveRoute,
Expand All @@ -29,10 +29,9 @@ const BrainLeftSidebarWave: React.FC<BrainLeftSidebarWaveProps> = ({
showPin = true,
isDirectMessage = false,
}) => {
const router = useRouter();
const searchParams = useSearchParams();
const { activeWave } = useMyStream();
const prefetchWaveData = usePrefetchWaveData();
const { isApp } = useDeviceInfo();
const { isApp, hasTouchScreen } = useDeviceInfo();
const isDropWave = wave.type !== ApiWaveType.Chat;

const formattedWaveName = useMemo(() => {
Expand All @@ -55,30 +54,39 @@ const BrainLeftSidebarWave: React.FC<BrainLeftSidebarWaveProps> = ({
return wave.name;
}, [wave.name, wave.type]);

const getHref = (waveId: string) => {
const currentWaveId = searchParams?.get("wave") ?? undefined;
if (currentWaveId === waveId) {
const activeWaveId = activeWave.id;

const href = useMemo(() => {
if (activeWaveId === wave.id) {
return getWaveHomeRoute({ isDirectMessage, isApp });
}
return getWaveRoute({ waveId, isDirectMessage, isApp });
};
return getWaveRoute({ waveId: wave.id, isDirectMessage, isApp });
}, [activeWaveId, isApp, isDirectMessage, wave.id]);

const haveNewDrops = wave.newDropsCount.count > 0;

const onWaveHover = (waveId: string) => {
const currentWaveId = searchParams?.get("wave") ?? undefined;
if (waveId !== currentWaveId) {
onHover(waveId);
prefetchWaveData(waveId);
const onWaveHover = useCallback(() => {
if (wave.id !== activeWaveId) {
onHover(wave.id);
prefetchWaveData(wave.id);
}
};
}, [activeWaveId, onHover, prefetchWaveData, wave.id]);

const isActive = wave.id === (searchParams?.get("wave") ?? undefined);
const isActive = wave.id === activeWaveId;

const onLinkClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
router.push(getHref(wave.id));
};
const handleWaveClick = useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
if (event.defaultPrevented) return;
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, { isDirectMessage });
},
[activeWave.set, activeWaveId, isDirectMessage, onWaveHover, wave.id]
);

const getAvatarRingClasses = () => {
if (isActive) return "tw-ring-1 tw-ring-offset-2 tw-ring-offset-iron-900 tw-ring-primary-400";
Expand All @@ -93,9 +101,9 @@ const BrainLeftSidebarWave: React.FC<BrainLeftSidebarWaveProps> = ({
: "desktop-hover:hover:tw-bg-iron-800/80"
}`}>
<Link
href={getHref(wave.id)}
onMouseEnter={() => onWaveHover(wave.id)}
onClick={onLinkClick}
href={href}
onMouseEnter={hasTouchScreen ? undefined : onWaveHover}
onClick={handleWaveClick}
className={`tw-flex tw-flex-1 tw-space-x-3 tw-no-underline tw-py-1 tw-transition-all tw-duration-200 tw-ease-out ${
isActive
? "tw-text-white desktop-hover:group-hover:tw-text-white"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ interface UnifiedWavesListWavesProps {
readonly hideHeaders?: boolean;
/** Reference to the scroll container for virtualization */
readonly scrollContainerRef: React.RefObject<HTMLElement | null>;
/** Whether the waves are direct messages (affects navigation route) */
readonly isDirectMessage?: boolean;
}

/**
Expand All @@ -98,7 +100,7 @@ const UnifiedWavesListWaves = forwardRef<
UnifiedWavesListWavesProps
>(
(
{ waves, onHover, scrollContainerRef, hideToggle, hidePin, hideHeaders },
{ waves, onHover, scrollContainerRef, hideToggle, hidePin, hideHeaders, isDirectMessage = false },
ref
) => {
const listContainerRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -162,6 +164,7 @@ const UnifiedWavesListWaves = forwardRef<
wave={wave}
onHover={onHover}
showPin={!hidePin}
isDirectMessage={isDirectMessage}
/>
</div>
))}
Expand Down Expand Up @@ -219,6 +222,7 @@ const UnifiedWavesListWaves = forwardRef<
wave={wave}
onHover={onHover}
showPin={!hidePin}
isDirectMessage={isDirectMessage}
/>
</div>
);
Expand Down
Loading