diff --git a/__tests__/components/navigation/BottomNavigation.test.tsx b/__tests__/components/navigation/BottomNavigation.test.tsx
index 48e0d1564f..b83d80d7f1 100644
--- a/__tests__/components/navigation/BottomNavigation.test.tsx
+++ b/__tests__/components/navigation/BottomNavigation.test.tsx
@@ -40,10 +40,11 @@ describe("BottomNavigation", () => {
const navItemCalls = (NavItem as jest.Mock).mock.calls;
expect(rendered).toHaveLength(navItemCalls.length);
- expect(navItemCalls).toHaveLength(6);
+ expect(navItemCalls).toHaveLength(7);
const passedItems = navItemCalls.map((call) => call[0].item);
expect(passedItems.map((item: { name: string }) => item.name)).toEqual([
+ "Profile",
"Waves",
"Messages",
"Home",
diff --git a/__tests__/components/navigation/NavItem.test.tsx b/__tests__/components/navigation/NavItem.test.tsx
index 8af5c6d8a0..f8e1156e85 100644
--- a/__tests__/components/navigation/NavItem.test.tsx
+++ b/__tests__/components/navigation/NavItem.test.tsx
@@ -1,80 +1,193 @@
-import { render } from '@testing-library/react';
-import NavItem from '@/components/navigation/NavItem';
-import { useViewContext } from '@/components/navigation/ViewContext';
-import { useAuth } from '@/components/auth/Auth';
-import { useTitle } from '@/contexts/TitleContext';
-import { useUnreadNotifications } from '@/hooks/useUnreadNotifications';
-import { useUnreadIndicator } from '@/hooks/useUnreadIndicator';
-import { useNotificationsContext } from '@/components/notifications/NotificationsContext';
-import { isNavItemActive } from '@/components/navigation/isNavItemActive';
-import { useWaveData } from '@/hooks/useWaveData';
-import { useWave } from '@/hooks/useWave';
-import { useRouter, useSearchParams, usePathname } from 'next/navigation';
-
-jest.mock('@/components/navigation/ViewContext', () => ({ useViewContext: jest.fn() }));
-jest.mock('@/components/auth/Auth', () => ({ useAuth: jest.fn() }));
-jest.mock('@/contexts/TitleContext', () => ({ useTitle: jest.fn() }));
-jest.mock('@/hooks/useUnreadNotifications', () => ({ useUnreadNotifications: jest.fn() }));
-jest.mock('@/hooks/useUnreadIndicator', () => ({ useUnreadIndicator: jest.fn() }));
-jest.mock('@/components/notifications/NotificationsContext', () => ({ useNotificationsContext: jest.fn() }));
-jest.mock('@/components/navigation/isNavItemActive', () => ({ isNavItemActive: jest.fn() }));
-jest.mock('@/hooks/useWaveData', () => ({ useWaveData: jest.fn() }));
-jest.mock('@/hooks/useWave', () => ({ useWave: jest.fn() }));
-jest.mock('next/navigation', () => ({
+import { fireEvent, render } from "@testing-library/react";
+import NavItem from "@/components/navigation/NavItem";
+import { useViewContext } from "@/components/navigation/ViewContext";
+import { useAuth } from "@/components/auth/Auth";
+import { useSeizeConnectContext } from "@/components/auth/SeizeConnectContext";
+import { useTitle } from "@/contexts/TitleContext";
+import { useUnreadNotifications } from "@/hooks/useUnreadNotifications";
+import { useUnreadIndicator } from "@/hooks/useUnreadIndicator";
+import { useNotificationsContext } from "@/components/notifications/NotificationsContext";
+import { isNavItemActive } from "@/components/navigation/isNavItemActive";
+import { useWaveData } from "@/hooks/useWaveData";
+import { useWave } from "@/hooks/useWave";
+import { useRouter, useSearchParams, usePathname } from "next/navigation";
+
+jest.mock("@/components/navigation/ViewContext", () => ({
+ useViewContext: jest.fn(),
+}));
+jest.mock("@/components/auth/Auth", () => ({ useAuth: jest.fn() }));
+jest.mock("@/components/auth/SeizeConnectContext", () => ({
+ useSeizeConnectContext: jest.fn(),
+}));
+jest.mock("@/contexts/TitleContext", () => ({ useTitle: jest.fn() }));
+jest.mock("@/hooks/useUnreadNotifications", () => ({
+ useUnreadNotifications: jest.fn(),
+}));
+jest.mock("@/hooks/useUnreadIndicator", () => ({
+ useUnreadIndicator: jest.fn(),
+}));
+jest.mock("@/components/notifications/NotificationsContext", () => ({
+ useNotificationsContext: jest.fn(),
+}));
+jest.mock("@/components/navigation/isNavItemActive", () => ({
+ isNavItemActive: jest.fn(),
+}));
+jest.mock("@/hooks/useWaveData", () => ({ useWaveData: jest.fn() }));
+jest.mock("@/hooks/useWave", () => ({ useWave: jest.fn() }));
+jest.mock("next/navigation", () => ({
useRouter: jest.fn(),
useSearchParams: jest.fn(),
usePathname: jest.fn(),
}));
-describe('NavItem notifications', () => {
+describe("NavItem notifications", () => {
const handleNavClick = jest.fn();
+ const seizeConnect = jest.fn();
const removeAllDeliveredNotifications = jest.fn();
const setTitle = jest.fn();
beforeEach(() => {
+ jest.clearAllMocks();
(useViewContext as jest.Mock).mockReturnValue({
- activeView: 'home',
+ activeView: "home",
handleNavClick,
});
(useRouter as jest.Mock).mockReturnValue({ push: jest.fn() });
(useSearchParams as jest.Mock).mockReturnValue(new URLSearchParams());
- (usePathname as jest.Mock).mockReturnValue('/');
+ (usePathname as jest.Mock).mockReturnValue("/");
(isNavItemActive as jest.Mock).mockReturnValue(false);
(useUnreadIndicator as jest.Mock).mockReturnValue({ hasUnread: false });
- (useNotificationsContext as jest.Mock).mockReturnValue({ removeAllDeliveredNotifications });
- (useAuth as jest.Mock).mockReturnValue({ connectedProfile: { handle: 'user' } });
- (useTitle as jest.Mock).mockReturnValue({ setTitle, title: '6529.io', notificationCount: 0, setNotificationCount: jest.fn(), setWaveData: jest.fn() });
+ (useNotificationsContext as jest.Mock).mockReturnValue({
+ removeAllDeliveredNotifications,
+ });
+ (useAuth as jest.Mock).mockReturnValue({
+ connectedProfile: { handle: "user", normalised_handle: "user" },
+ });
+ (useSeizeConnectContext as jest.Mock).mockReturnValue({
+ address: "0xabc",
+ seizeConnect,
+ });
+ (useUnreadNotifications as jest.Mock).mockReturnValue({
+ notifications: { unread_count: 0 },
+ haveUnreadNotifications: false,
+ });
+ (useTitle as jest.Mock).mockReturnValue({
+ setTitle,
+ title: "6529.io",
+ notificationCount: 0,
+ setNotificationCount: jest.fn(),
+ setWaveData: jest.fn(),
+ });
(useWaveData as jest.Mock).mockReturnValue({ data: null });
(useWave as jest.Mock).mockReturnValue({ isDm: false });
});
- it('sets title and shows badge when there are unread notifications', () => {
- (useUnreadNotifications as jest.Mock).mockReturnValue({ notifications: { unread_count: 3 }, haveUnreadNotifications: true });
- const item = { name: 'Notifications', icon: '/n' } as any;
+ it("sets title and shows badge when there are unread notifications", () => {
+ (useUnreadNotifications as jest.Mock).mockReturnValue({
+ notifications: { unread_count: 3 },
+ haveUnreadNotifications: true,
+ });
+ const item = { name: "Notifications", icon: "/n" } as any;
const { container } = render();
// Title is set via TitleContext hooks
- expect(container.querySelector('.tw-bg-red')).not.toBeNull();
+ expect(container.querySelector(".tw-bg-red")).not.toBeNull();
expect(removeAllDeliveredNotifications).not.toHaveBeenCalled();
});
- it('clears delivered notifications when none unread', () => {
- (useUnreadNotifications as jest.Mock).mockReturnValue({ notifications: { unread_count: 0 }, haveUnreadNotifications: false });
- const item = { name: 'Notifications', icon: '/n' } as any;
+ it("clears delivered notifications when none unread", () => {
+ (useUnreadNotifications as jest.Mock).mockReturnValue({
+ notifications: { unread_count: 0 },
+ haveUnreadNotifications: false,
+ });
+ const item = { name: "Notifications", icon: "/n" } as any;
const { container } = render();
// Title is set via TitleContext hooks
expect(removeAllDeliveredNotifications).toHaveBeenCalled();
- expect(container.querySelector('.tw-bg-red')).toBeNull();
+ expect(container.querySelector(".tw-bg-red")).toBeNull();
});
-});
-it('renders disabled item when disabled flag set', () => {
- (useUnreadNotifications as jest.Mock).mockReturnValue({});
- const item = { name: 'Feed', icon: '/i', disabled: true } as any;
- const { getByRole } = render();
- const button = getByRole('button');
- expect(button).toBeDisabled();
+ it("prompts connect when profile item is clicked without a connected wallet", () => {
+ (useSeizeConnectContext as jest.Mock).mockReturnValue({
+ address: undefined,
+ seizeConnect,
+ });
+ const item = {
+ kind: "route",
+ name: "Profile",
+ href: "/profile",
+ icon: "profile",
+ } as any;
+
+ const { getByRole } = render();
+ fireEvent.click(getByRole("button", { name: "Profile" }));
+
+ expect(seizeConnect).toHaveBeenCalledTimes(1);
+ expect(handleNavClick).not.toHaveBeenCalled();
+ });
+
+ it("navigates to connected user profile when profile item is clicked", () => {
+ (useAuth as jest.Mock).mockReturnValue({
+ connectedProfile: { handle: "User", normalised_handle: "my-handle" },
+ });
+ const item = {
+ kind: "route",
+ name: "Profile",
+ href: "/profile",
+ icon: "profile",
+ } as any;
+
+ const { getByRole } = render();
+ fireEvent.click(getByRole("button", { name: "Profile" }));
+
+ expect(handleNavClick).toHaveBeenCalledWith(
+ expect.objectContaining({
+ kind: "route",
+ name: "Profile",
+ href: "/my-handle",
+ })
+ );
+ expect(seizeConnect).not.toHaveBeenCalled();
+ });
+
+ it("marks profile active only on own profile routes", () => {
+ (useViewContext as jest.Mock).mockReturnValue({
+ activeView: null,
+ handleNavClick,
+ });
+ (usePathname as jest.Mock).mockReturnValue("/my-handle/proxy");
+ (useAuth as jest.Mock).mockReturnValue({
+ connectedProfile: { handle: "User", normalised_handle: "my-handle" },
+ });
+ const item = {
+ kind: "route",
+ name: "Profile",
+ href: "/profile",
+ icon: "profile",
+ } as any;
+
+ const { getByRole, rerender } = render();
+ expect(getByRole("button", { name: "Profile" })).toHaveAttribute(
+ "aria-current",
+ "page"
+ );
+
+ (usePathname as jest.Mock).mockReturnValue("/other-user");
+ rerender();
+ expect(getByRole("button", { name: "Profile" })).not.toHaveAttribute(
+ "aria-current",
+ "page"
+ );
+ });
+
+ it("renders disabled item when disabled flag set", () => {
+ (useUnreadNotifications as jest.Mock).mockReturnValue({});
+ const item = { name: "Feed", icon: "/i", disabled: true } as any;
+ const { getByRole } = render();
+ const button = getByRole("button");
+ expect(button).toBeDisabled();
+ });
});
diff --git a/components/common/icons/ProfileIcon.tsx b/components/common/icons/ProfileIcon.tsx
new file mode 100644
index 0000000000..61a7cc512f
--- /dev/null
+++ b/components/common/icons/ProfileIcon.tsx
@@ -0,0 +1,22 @@
+const ProfileIcon = ({
+ className,
+}: {
+ readonly className?: string | undefined;
+}) => (
+
+);
+
+export default ProfileIcon;
diff --git a/components/navigation/BottomNavigation.tsx b/components/navigation/BottomNavigation.tsx
index 01536debdd..916d3bb17f 100644
--- a/components/navigation/BottomNavigation.tsx
+++ b/components/navigation/BottomNavigation.tsx
@@ -8,12 +8,20 @@ import BellIcon from "../common/icons/BellIcon";
import ChatBubbleIcon from "../common/icons/ChatBubbleIcon";
import LogoIcon from "../common/icons/LogoIcon";
import CollectionsMenuIcon from "../common/icons/CollectionsMenuIcon";
+import ProfileIcon from "../common/icons/ProfileIcon";
import UsersIcon from "../common/icons/UsersIcon";
import WavesIcon from "../common/icons/WavesIcon";
import NavItem from "./NavItem";
import type { NavItem as NavItemData } from "./navTypes";
const items: NavItemData[] = [
+ {
+ kind: "route",
+ name: "Profile",
+ href: "/profile",
+ icon: "profile",
+ iconComponent: ProfileIcon,
+ },
{
kind: "view",
name: "Waves",
diff --git a/components/navigation/NavItem.tsx b/components/navigation/NavItem.tsx
index 399a3156a1..842b431326 100644
--- a/components/navigation/NavItem.tsx
+++ b/components/navigation/NavItem.tsx
@@ -7,6 +7,7 @@ import { useViewContext } from "./ViewContext";
import type { NavItem as NavItemData } from "./navTypes";
import { motion } from "framer-motion";
import { useAuth } from "../auth/Auth";
+import { useSeizeConnectContext } from "../auth/SeizeConnectContext";
import { useTitle } from "@/contexts/TitleContext";
import { useUnreadNotifications } from "@/hooks/useUnreadNotifications";
import { useUnreadIndicator } from "@/hooks/useUnreadIndicator";
@@ -27,6 +28,7 @@ const NavItem = ({ item }: Props) => {
const { name } = item;
const { icon } = item;
+ const { address, seizeConnect } = useSeizeConnectContext();
const isLogoItem = name === "Home";
@@ -41,6 +43,12 @@ const NavItem = ({ item }: Props) => {
// Add unread notifications logic
const { connectedProfile } = useAuth();
+ const normalizedConnectedHandle = (
+ connectedProfile?.normalised_handle ?? connectedProfile?.handle
+ )?.toLowerCase();
+ const normalizedConnectedAddress = address?.toLowerCase();
+ const profileSlug = normalizedConnectedHandle ?? normalizedConnectedAddress;
+ const profileHref = profileSlug ? `/${profileSlug}` : null;
const { setTitle } = useTitle();
const { notifications, haveUnreadNotifications } = useUnreadNotifications(
item.name === "Notifications" ? (connectedProfile?.handle ?? null) : null
@@ -100,20 +108,48 @@ const NavItem = ({ item }: Props) => {
const iconSizeClass = item.iconSizeClass ?? "tw-size-7";
- const isActive = isNavItemActive(
- item,
- pathname ?? "",
- searchParams ?? new URLSearchParams(),
- activeView,
- isCurrentWaveDmValue
- );
+ const isProfileItem = item.kind === "route" && item.name === "Profile";
+ const normalizedPathname = pathname.toLowerCase();
+ const isProfileActive =
+ isProfileItem &&
+ activeView === null &&
+ profileHref !== null &&
+ (normalizedPathname === profileHref ||
+ normalizedPathname.startsWith(`${profileHref}/`));
+
+ const isActive = isProfileItem
+ ? isProfileActive
+ : isNavItemActive(
+ item,
+ pathname,
+ searchParams,
+ activeView,
+ isCurrentWaveDmValue
+ );
+
+ const handleClick = () => {
+ if (item.kind === "route" && item.name === "Profile") {
+ if (!address) {
+ seizeConnect();
+ return;
+ }
+
+ handleNavClick({
+ ...item,
+ href: profileHref ?? item.href,
+ });
+ return;
+ }
+
+ handleNavClick(item);
+ };
return (