1 import { myAuth, showAvatars } from "@utils/app";
2 import { isBrowser } from "@utils/browser";
3 import { numToSI, poll } from "@utils/helpers";
4 import { amAdmin, canCreateCommunity } from "@utils/roles";
5 import { Component, createRef, linkEvent } from "inferno";
6 import { NavLink } from "inferno-router";
8 GetReportCountResponse,
10 GetUnreadCountResponse,
11 GetUnreadRegistrationApplicationCountResponse,
12 } from "lemmy-js-client";
13 import { donateLemmyUrl, updateUnreadCountsInterval } from "../../config";
14 import { i18n } from "../../i18next";
15 import { UserService } from "../../services";
16 import { HttpService, RequestState } from "../../services/HttpService";
17 import { toast } from "../../toast";
18 import { Icon } from "../common/icon";
19 import { PictrsImage } from "../common/pictrs-image";
21 interface NavbarProps {
22 siteRes?: GetSiteResponse;
25 interface NavbarState {
26 unreadInboxCountRes: RequestState<GetUnreadCountResponse>;
27 unreadReportCountRes: RequestState<GetReportCountResponse>;
28 unreadApplicationCountRes: RequestState<GetUnreadRegistrationApplicationCountResponse>;
29 onSiteBanner?(url: string): any;
32 function handleCollapseClick(i: Navbar) {
34 i.collapseButtonRef.current?.attributes &&
35 i.collapseButtonRef.current?.attributes.getNamedItem("aria-expanded")
38 i.collapseButtonRef.current?.click();
42 function handleLogOut(i: Navbar) {
43 UserService.Instance.logout();
44 handleCollapseClick(i);
47 export class Navbar extends Component<NavbarProps, NavbarState> {
48 state: NavbarState = {
49 unreadInboxCountRes: { state: "empty" },
50 unreadReportCountRes: { state: "empty" },
51 unreadApplicationCountRes: { state: "empty" },
53 collapseButtonRef = createRef<HTMLButtonElement>();
54 mobileMenuRef = createRef<HTMLDivElement>();
56 constructor(props: any, context: any) {
57 super(props, context);
59 this.handleOutsideMenuClick = this.handleOutsideMenuClick.bind(this);
62 async componentDidMount() {
63 // Subscribe to jwt changes
65 // On the first load, check the unreads
66 this.requestNotificationPermission();
68 this.requestNotificationPermission();
70 document.addEventListener("mouseup", this.handleOutsideMenuClick);
74 componentWillUnmount() {
75 document.removeEventListener("mouseup", this.handleOutsideMenuClick);
78 // TODO class active corresponding to current pages
80 const siteView = this.props.siteRes?.site_view;
81 const person = UserService.Instance.myUserInfo?.local_user_view.person;
84 className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3 container-lg"
90 title={siteView?.site.description ?? siteView?.site.name}
91 className="d-flex align-items-center navbar-brand me-md-3"
92 onMouseUp={linkEvent(this, handleCollapseClick)}
94 {siteView?.site.icon && showAvatars() && (
95 <PictrsImage src={siteView.site.icon} icon />
100 <ul className="navbar-nav d-flex flex-row ms-auto d-md-none">
101 <li id="navMessages" className="nav-item nav-item-icon">
104 className="p-1 nav-link border-0 nav-messages"
105 title={i18n.t("unread_messages", {
106 count: Number(this.state.unreadApplicationCountRes.state),
107 formattedCount: numToSI(this.unreadInboxCount),
109 onMouseUp={linkEvent(this, handleCollapseClick)}
112 {this.unreadInboxCount > 0 && (
113 <span className="mx-1 badge text-bg-light">
114 {numToSI(this.unreadInboxCount)}
119 {this.moderatesSomething && (
120 <li className="nav-item nav-item-icon">
123 className="p-1 nav-link border-0"
124 title={i18n.t("unread_reports", {
125 count: Number(this.unreadReportCount),
126 formattedCount: numToSI(this.unreadReportCount),
128 onMouseUp={linkEvent(this, handleCollapseClick)}
130 <Icon icon="shield" />
131 {this.unreadReportCount > 0 && (
132 <span className="mx-1 badge text-bg-light">
133 {numToSI(this.unreadReportCount)}
140 <li className="nav-item nav-item-icon">
142 to="/registration_applications"
143 className="p-1 nav-link border-0"
144 title={i18n.t("unread_registration_applications", {
145 count: Number(this.unreadApplicationCount),
146 formattedCount: numToSI(this.unreadApplicationCount),
148 onMouseUp={linkEvent(this, handleCollapseClick)}
150 <Icon icon="clipboard" />
151 {this.unreadApplicationCount > 0 && (
152 <span className="mx-1 badge text-bg-light">
153 {numToSI(this.unreadApplicationCount)}
162 className="navbar-toggler border-0 p-1"
165 data-tippy-content={i18n.t("expand_here")}
166 data-bs-toggle="collapse"
167 data-bs-target="#navbarDropdown"
168 aria-controls="navbarDropdown"
169 aria-expanded="false"
170 ref={this.collapseButtonRef}
175 className="collapse navbar-collapse my-2"
177 ref={this.mobileMenuRef}
179 <ul id="navbarLinks" className="me-auto navbar-nav">
180 <li className="nav-item">
184 title={i18n.t("communities")}
185 onMouseUp={linkEvent(this, handleCollapseClick)}
187 {i18n.t("communities")}
190 <li className="nav-item">
191 {/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
194 pathname: "/create_post",
198 state: { prevPath: this.currentLocation },
201 title={i18n.t("create_post")}
202 onMouseUp={linkEvent(this, handleCollapseClick)}
204 {i18n.t("create_post")}
207 {this.props.siteRes && canCreateCommunity(this.props.siteRes) && (
208 <li className="nav-item">
210 to="/create_community"
212 title={i18n.t("create_community")}
213 onMouseUp={linkEvent(this, handleCollapseClick)}
215 {i18n.t("create_community")}
219 <li className="nav-item">
221 className="nav-link d-inline-flex align-items-center d-md-inline-block"
222 title={i18n.t("support_lemmy")}
223 href={donateLemmyUrl}
225 <Icon icon="heart" classes="small" />
226 <span className="d-inline ms-1 d-md-none ms-md-0">
227 {i18n.t("support_lemmy")}
232 <ul id="navbarIcons" className="navbar-nav">
233 <li id="navSearch" className="nav-item">
236 className="nav-link d-inline-flex align-items-center d-md-inline-block"
237 title={i18n.t("search")}
238 onMouseUp={linkEvent(this, handleCollapseClick)}
240 <Icon icon="search" />
241 <span className="d-inline ms-1 d-md-none ms-md-0">
247 <li id="navAdmin" className="nav-item">
250 className="nav-link d-inline-flex align-items-center d-md-inline-block"
251 title={i18n.t("admin_settings")}
252 onMouseUp={linkEvent(this, handleCollapseClick)}
254 <Icon icon="settings" />
255 <span className="d-inline ms-1 d-md-none ms-md-0">
256 {i18n.t("admin_settings")}
263 <li id="navMessages" className="nav-item">
265 className="nav-link d-inline-flex align-items-center d-md-inline-block"
267 title={i18n.t("unread_messages", {
268 count: Number(this.unreadInboxCount),
269 formattedCount: numToSI(this.unreadInboxCount),
271 onMouseUp={linkEvent(this, handleCollapseClick)}
274 <span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
275 {i18n.t("unread_messages", {
276 count: Number(this.unreadInboxCount),
277 formattedCount: numToSI(this.unreadInboxCount),
280 {this.unreadInboxCount > 0 && (
281 <span className="mx-1 badge text-bg-light">
282 {numToSI(this.unreadInboxCount)}
287 {this.moderatesSomething && (
288 <li id="navModeration" className="nav-item">
290 className="nav-link d-inline-flex align-items-center d-md-inline-block"
292 title={i18n.t("unread_reports", {
293 count: Number(this.unreadReportCount),
294 formattedCount: numToSI(this.unreadReportCount),
296 onMouseUp={linkEvent(this, handleCollapseClick)}
298 <Icon icon="shield" />
299 <span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
300 {i18n.t("unread_reports", {
301 count: Number(this.unreadReportCount),
302 formattedCount: numToSI(this.unreadReportCount),
305 {this.unreadReportCount > 0 && (
306 <span className="mx-1 badge text-bg-light">
307 {numToSI(this.unreadReportCount)}
314 <li id="navApplications" className="nav-item">
316 to="/registration_applications"
317 className="nav-link d-inline-flex align-items-center d-md-inline-block"
318 title={i18n.t("unread_registration_applications", {
319 count: Number(this.unreadApplicationCount),
320 formattedCount: numToSI(this.unreadApplicationCount),
322 onMouseUp={linkEvent(this, handleCollapseClick)}
324 <Icon icon="clipboard" />
325 <span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
326 {i18n.t("unread_registration_applications", {
327 count: Number(this.unreadApplicationCount),
328 formattedCount: numToSI(this.unreadApplicationCount),
331 {this.unreadApplicationCount > 0 && (
332 <span className="mx-1 badge text-bg-light">
333 {numToSI(this.unreadApplicationCount)}
340 <div id="dropdownUser" className="dropdown">
342 className="btn dropdown-toggle"
344 aria-expanded="false"
345 data-bs-toggle="dropdown"
347 {showAvatars() && person.avatar && (
348 <PictrsImage src={person.avatar} icon />
350 {person.display_name ?? person.name}
353 className="dropdown-menu"
354 style={{ "min-width": "fit-content" }}
358 to={`/u/${person.name}`}
359 className="dropdown-item px-2"
360 title={i18n.t("profile")}
361 onMouseUp={linkEvent(this, handleCollapseClick)}
363 <Icon icon="user" classes="me-1" />
370 className="dropdown-item px-2"
371 title={i18n.t("settings")}
372 onMouseUp={linkEvent(this, handleCollapseClick)}
374 <Icon icon="settings" classes="me-1" />
379 <hr className="dropdown-divider" />
383 className="dropdown-item btn btn-link px-2"
384 onClick={linkEvent(this, handleLogOut)}
386 <Icon icon="log-out" classes="me-1" />
396 <li className="nav-item">
400 title={i18n.t("login")}
401 onMouseUp={linkEvent(this, handleCollapseClick)}
406 <li className="nav-item">
410 title={i18n.t("sign_up")}
411 onMouseUp={linkEvent(this, handleCollapseClick)}
424 handleOutsideMenuClick(event: MouseEvent) {
425 if (!this.mobileMenuRef.current?.contains(event.target as Node | null)) {
426 handleCollapseClick(this);
430 get moderatesSomething(): boolean {
431 const mods = UserService.Instance.myUserInfo?.moderates;
432 const moderatesS = (mods && mods.length > 0) || false;
433 return amAdmin() || moderatesS;
438 if (window.document.visibilityState !== "hidden") {
439 const auth = myAuth();
442 unreadInboxCountRes: await HttpService.client.getUnreadCount({
447 if (this.moderatesSomething) {
449 unreadReportCountRes: await HttpService.client.getReportCount({
457 unreadApplicationCountRes:
458 await HttpService.client.getUnreadRegistrationApplicationCount({
465 }, updateUnreadCountsInterval);
468 get unreadInboxCount(): number {
469 if (this.state.unreadInboxCountRes.state == "success") {
470 const data = this.state.unreadInboxCountRes.data;
471 return data.replies + data.mentions + data.private_messages;
477 get unreadReportCount(): number {
478 if (this.state.unreadReportCountRes.state == "success") {
479 const data = this.state.unreadReportCountRes.data;
482 data.comment_reports +
483 (data.private_message_reports ?? 0)
490 get unreadApplicationCount(): number {
491 if (this.state.unreadApplicationCountRes.state == "success") {
492 const data = this.state.unreadApplicationCountRes.data;
493 return data.registration_applications;
499 get currentLocation() {
500 return this.context.router.history.location.pathname;
503 requestNotificationPermission() {
504 if (UserService.Instance.myUserInfo) {
505 document.addEventListener("DOMContentLoaded", function () {
507 toast(i18n.t("notifications_error"), "danger");
511 if (Notification.permission !== "granted")
512 Notification.requestPermission();