Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
1import { useApolloClient } from "@apollo/client";
2import {
3 BellIcon as BellOutline,
4 BookmarkIcon as BookmarkOutline,
5 GlobeAltIcon as GlobeOutline,
6 HomeIcon as HomeOutline,
7 UserCircleIcon,
8 UserGroupIcon as UserGroupOutline
9} from "@heroicons/react/24/outline";
10import {
11 BellIcon as BellSolid,
12 BookmarkIcon as BookmarkSolid,
13 GlobeAltIcon as GlobeSolid,
14 HomeIcon as HomeSolid,
15 UserGroupIcon as UserGroupSolid
16} from "@heroicons/react/24/solid";
17import { STATIC_IMAGES_URL } from "@hey/data/constants";
18import {
19 GroupsDocument,
20 NotificationIndicatorDocument,
21 NotificationsDocument,
22 PostBookmarksDocument,
23 PostsExploreDocument,
24 PostsForYouDocument,
25 TimelineDocument,
26 TimelineHighlightsDocument
27} from "@hey/indexer";
28import {
29 type MouseEvent,
30 memo,
31 type ReactNode,
32 useCallback,
33 useState
34} from "react";
35import { Link, useLocation } from "react-router";
36import Pro from "@/components/Shared/Navbar/NavItems/Pro";
37import { Image, Spinner, Tooltip } from "@/components/Shared/UI";
38import useHasNewNotifications from "@/hooks/useHasNewNotifications";
39import { useAuthModalStore } from "@/store/non-persisted/modal/useAuthModalStore";
40import { useAccountStore } from "@/store/persisted/useAccountStore";
41import SignedAccount from "./SignedAccount";
42
43const navigationItems = {
44 "/": {
45 outline: <HomeOutline className="size-6" />,
46 refreshDocs: [
47 TimelineDocument,
48 TimelineHighlightsDocument,
49 PostsForYouDocument
50 ],
51 solid: <HomeSolid className="size-6" />,
52 title: "Home"
53 },
54 "/bookmarks": {
55 outline: <BookmarkOutline className="size-6" />,
56 refreshDocs: [PostBookmarksDocument],
57 solid: <BookmarkSolid className="size-6" />,
58 title: "Bookmarks"
59 },
60 "/explore": {
61 outline: <GlobeOutline className="size-6" />,
62 refreshDocs: [PostsExploreDocument],
63 solid: <GlobeSolid className="size-6" />,
64 title: "Explore"
65 },
66 "/groups": {
67 outline: <UserGroupOutline className="size-6" />,
68 refreshDocs: [GroupsDocument],
69 solid: <UserGroupSolid className="size-6" />,
70 title: "Groups"
71 },
72 "/notifications": {
73 outline: <BellOutline className="size-6" />,
74 refreshDocs: [NotificationsDocument, NotificationIndicatorDocument],
75 solid: <BellSolid className="size-6" />,
76 title: "Notifications"
77 }
78};
79
80interface NavItemProps {
81 url: string;
82 icon: ReactNode;
83 onClick?: (e: MouseEvent<HTMLAnchorElement>) => void;
84}
85
86const NavItem = memo(({ icon, onClick, url }: NavItemProps) => (
87 <Tooltip content={navigationItems[url as keyof typeof navigationItems].title}>
88 <Link onClick={onClick} to={url}>
89 {icon}
90 </Link>
91 </Tooltip>
92));
93
94const NavItems = memo(({ isLoggedIn }: { isLoggedIn: boolean }) => {
95 const { pathname } = useLocation();
96 const hasNewNotifications = useHasNewNotifications();
97 const client = useApolloClient();
98 const [refreshingRoute, setRefreshingRoute] = useState<string | null>(null);
99 const routes = [
100 "/",
101 "/explore",
102 ...(isLoggedIn ? ["/notifications", "/groups", "/bookmarks"] : [])
103 ];
104
105 return (
106 <>
107 {routes.map((route) => {
108 let icon =
109 pathname === route
110 ? navigationItems[route as keyof typeof navigationItems].solid
111 : navigationItems[route as keyof typeof navigationItems].outline;
112
113 if (refreshingRoute === route) {
114 icon = <Spinner className="my-0.5" size="sm" />;
115 }
116
117 const iconWithIndicator =
118 route === "/notifications" ? (
119 <span className="relative">
120 {icon}
121 {hasNewNotifications && (
122 <span className="-right-1 -top-1 absolute size-2 rounded-full bg-red-500" />
123 )}
124 </span>
125 ) : (
126 icon
127 );
128
129 const handleClick = async (e: MouseEvent<HTMLAnchorElement>) => {
130 const item = navigationItems[route as keyof typeof navigationItems];
131 const isSameRoute = pathname === route;
132 if (!isSameRoute || !("refreshDocs" in item) || !item.refreshDocs) {
133 return;
134 }
135 e.preventDefault();
136 window.scrollTo(0, 0);
137 setRefreshingRoute(route);
138 try {
139 await client.refetchQueries({ include: item.refreshDocs });
140 } finally {
141 setRefreshingRoute(null);
142 }
143 };
144
145 return (
146 <NavItem
147 icon={iconWithIndicator}
148 key={route}
149 onClick={handleClick}
150 url={route}
151 />
152 );
153 })}
154 </>
155 );
156});
157
158const Navbar = () => {
159 const { pathname } = useLocation();
160 const { currentAccount } = useAccountStore();
161 const { setShowAuthModal } = useAuthModalStore();
162
163 const handleLogoClick = useCallback(
164 (e: MouseEvent<HTMLAnchorElement>) => {
165 if (pathname === "/") {
166 e.preventDefault();
167 window.scrollTo(0, 0);
168 }
169 },
170 [pathname]
171 );
172
173 const handleAuthClick = useCallback(() => {
174 setShowAuthModal(true);
175 }, []);
176
177 return (
178 <aside className="sticky top-5 mt-5 hidden w-10 shrink-0 flex-col items-center gap-y-5 md:flex">
179 <Link onClick={handleLogoClick} to="/">
180 <Image
181 alt="Logo"
182 className="size-8"
183 height={32}
184 src={`${STATIC_IMAGES_URL}/app-icon/0.png`}
185 width={32}
186 />
187 </Link>
188 <NavItems isLoggedIn={!!currentAccount} />
189 {currentAccount ? (
190 <>
191 <Pro />
192 <SignedAccount />
193 </>
194 ) : (
195 <button onClick={handleAuthClick} type="button">
196 <Tooltip content="Login">
197 <UserCircleIcon className="size-6" />
198 </Tooltip>
199 </button>
200 )}
201 </aside>
202 );
203};
204
205export default memo(Navbar);