1 import { Component, createRef, linkEvent } from "inferno";
2 import { NavLink } from "inferno-router";
4 GetReportCountResponse,
6 GetUnreadCountResponse,
7 GetUnreadRegistrationApplicationCountResponse,
8 } from "lemmy-js-client";
9 import { i18n } from "../../i18next";
10 import { UserService } from "../../services";
11 import { HttpService, RequestState } from "../../services/HttpService";
18 updateUnreadCountsInterval,
20 import { isBrowser } from "../../utils/browser/is-browser";
21 import { poll } from "../../utils/helpers/poll";
22 import { amAdmin } from "../../utils/roles/am-admin";
23 import { canCreateCommunity } from "../../utils/roles/can-create-community";
24 import { Icon } from "../common/icon";
25 import { PictrsImage } from "../common/pictrs-image";
27 interface NavbarProps {
28 siteRes?: GetSiteResponse;
31 interface NavbarState {
32 unreadInboxCountRes: RequestState<GetUnreadCountResponse>;
33 unreadReportCountRes: RequestState<GetReportCountResponse>;
34 unreadApplicationCountRes: RequestState<GetUnreadRegistrationApplicationCountResponse>;
35 onSiteBanner?(url: string): any;
38 function handleCollapseClick(i: Navbar) {
39 if (i.collapseButtonRef.current?.ariaExpanded === "true") {
40 i.collapseButtonRef.current?.click();
44 function handleLogOut(i: Navbar) {
45 UserService.Instance.logout();
46 handleCollapseClick(i);
49 export class Navbar extends Component<NavbarProps, NavbarState> {
50 state: NavbarState = {
51 unreadInboxCountRes: { state: "empty" },
52 unreadReportCountRes: { state: "empty" },
53 unreadApplicationCountRes: { state: "empty" },
55 collapseButtonRef = createRef<HTMLButtonElement>();
56 mobileMenuRef = createRef<HTMLDivElement>();
58 constructor(props: any, context: any) {
59 super(props, context);
61 this.handleOutsideMenuClick = this.handleOutsideMenuClick.bind(this);
64 async componentDidMount() {
65 // Subscribe to jwt changes
67 // On the first load, check the unreads
68 this.requestNotificationPermission();
70 this.requestNotificationPermission();
72 document.addEventListener("mouseup", this.handleOutsideMenuClick);
76 componentWillUnmount() {
77 document.removeEventListener("mouseup", this.handleOutsideMenuClick);
84 // TODO class active corresponding to current page
86 const siteView = this.props.siteRes?.site_view;
87 const person = UserService.Instance.myUserInfo?.local_user_view.person;
90 className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3 container-lg"
96 title={siteView?.site.description ?? siteView?.site.name}
97 className="d-flex align-items-center navbar-brand mr-md-3"
98 onMouseUp={linkEvent(this, handleCollapseClick)}
100 {siteView?.site.icon && showAvatars() && (
101 <PictrsImage src={siteView.site.icon} icon />
103 {siteView?.site.name}
106 <ul className="navbar-nav d-flex flex-row ml-auto d-md-none">
107 <li id="navMessages" className="nav-item nav-item-icon">
110 className="p-1 nav-link border-0 nav-messages"
111 title={i18n.t("unread_messages", {
112 count: Number(this.state.unreadApplicationCountRes.state),
113 formattedCount: numToSI(this.unreadInboxCount),
115 onMouseUp={linkEvent(this, handleCollapseClick)}
118 {this.unreadInboxCount > 0 && (
119 <span className="mx-1 badge badge-light">
120 {numToSI(this.unreadInboxCount)}
125 {this.moderatesSomething && (
126 <li className="nav-item nav-item-icon">
129 className="p-1 nav-link border-0"
130 title={i18n.t("unread_reports", {
131 count: Number(this.unreadReportCount),
132 formattedCount: numToSI(this.unreadReportCount),
134 onMouseUp={linkEvent(this, handleCollapseClick)}
136 <Icon icon="shield" />
137 {this.unreadReportCount > 0 && (
138 <span className="mx-1 badge badge-light">
139 {numToSI(this.unreadReportCount)}
146 <li className="nav-item nav-item-icon">
148 to="/registration_applications"
149 className="p-1 nav-link border-0"
150 title={i18n.t("unread_registration_applications", {
151 count: Number(this.unreadApplicationCount),
152 formattedCount: numToSI(this.unreadApplicationCount),
154 onMouseUp={linkEvent(this, handleCollapseClick)}
156 <Icon icon="clipboard" />
157 {this.unreadApplicationCount > 0 && (
158 <span className="mx-1 badge badge-light">
159 {numToSI(this.unreadApplicationCount)}
168 className="navbar-toggler border-0 p-1"
171 data-tippy-content={i18n.t("expand_here")}
172 data-bs-toggle="collapse"
173 data-bs-target="#navbarDropdown"
174 aria-controls="navbarDropdown"
175 aria-expanded="false"
176 ref={this.collapseButtonRef}
181 className="collapse navbar-collapse my-2"
183 ref={this.mobileMenuRef}
185 <ul id="navbarLinks" className="mr-auto navbar-nav">
186 <li className="nav-item">
190 title={i18n.t("communities")}
191 onMouseUp={linkEvent(this, handleCollapseClick)}
193 {i18n.t("communities")}
196 <li className="nav-item">
197 {/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
200 pathname: "/create_post",
204 state: { prevPath: this.currentLocation },
207 title={i18n.t("create_post")}
208 onMouseUp={linkEvent(this, handleCollapseClick)}
210 {i18n.t("create_post")}
213 {this.props.siteRes && canCreateCommunity(this.props.siteRes) && (
214 <li className="nav-item">
216 to="/create_community"
218 title={i18n.t("create_community")}
219 onMouseUp={linkEvent(this, handleCollapseClick)}
221 {i18n.t("create_community")}
225 <li className="nav-item">
228 title={i18n.t("support_lemmy")}
229 href={donateLemmyUrl}
231 <Icon icon="heart" classes="small" />
235 <ul id="navbarIcons" className="navbar-nav">
236 <li id="navSearch" className="nav-item">
240 title={i18n.t("search")}
241 onMouseUp={linkEvent(this, handleCollapseClick)}
243 <Icon icon="search" />
247 <li id="navAdmin" className="nav-item">
251 title={i18n.t("admin_settings")}
252 onMouseUp={linkEvent(this, handleCollapseClick)}
254 <Icon icon="settings" />
260 <li id="navMessages" className="nav-item">
264 title={i18n.t("unread_messages", {
265 count: Number(this.unreadInboxCount),
266 formattedCount: numToSI(this.unreadInboxCount),
268 onMouseUp={linkEvent(this, handleCollapseClick)}
271 {this.unreadInboxCount > 0 && (
272 <span className="mx-1 badge badge-light">
273 {numToSI(this.unreadInboxCount)}
278 {this.moderatesSomething && (
279 <li id="navModeration" className="nav-item">
283 title={i18n.t("unread_reports", {
284 count: Number(this.unreadReportCount),
285 formattedCount: numToSI(this.unreadReportCount),
287 onMouseUp={linkEvent(this, handleCollapseClick)}
289 <Icon icon="shield" />
290 {this.unreadReportCount > 0 && (
291 <span className="mx-1 badge badge-light">
292 {numToSI(this.unreadReportCount)}
299 <li id="navApplications" className="nav-item">
301 to="/registration_applications"
303 title={i18n.t("unread_registration_applications", {
304 count: Number(this.unreadApplicationCount),
305 formattedCount: numToSI(this.unreadApplicationCount),
307 onMouseUp={linkEvent(this, handleCollapseClick)}
309 <Icon icon="clipboard" />
310 {this.unreadApplicationCount > 0 && (
311 <span className="mx-1 badge badge-light">
312 {numToSI(this.unreadApplicationCount)}
319 <div id="dropdownUser" className="dropdown">
321 className="btn dropdown-toggle"
323 aria-expanded="false"
324 data-bs-toggle="dropdown"
326 {showAvatars() && person.avatar && (
327 <PictrsImage src={person.avatar} icon />
329 {person.display_name ?? person.name}
332 className="dropdown-menu"
333 style={{ "min-width": "fit-content" }}
337 to={`/u/${person.name}`}
338 className="dropdown-item px-2"
339 title={i18n.t("profile")}
340 onMouseUp={linkEvent(this, handleCollapseClick)}
342 <Icon icon="user" classes="mr-1" />
349 className="dropdown-item px-2"
350 title={i18n.t("settings")}
351 onMouseUp={linkEvent(this, handleCollapseClick)}
353 <Icon icon="settings" classes="mr-1" />
358 <hr className="dropdown-divider" />
362 className="dropdown-item btn btn-link px-2"
363 onClick={linkEvent(this, handleLogOut)}
365 <Icon icon="log-out" classes="mr-1" />
375 <li className="nav-item">
379 title={i18n.t("login")}
380 onMouseUp={linkEvent(this, handleCollapseClick)}
385 <li className="nav-item">
389 title={i18n.t("sign_up")}
390 onMouseUp={linkEvent(this, handleCollapseClick)}
403 handleOutsideMenuClick(event: MouseEvent) {
404 if (!this.mobileMenuRef.current?.contains(event.target as Node | null)) {
405 handleCollapseClick(this);
409 get moderatesSomething(): boolean {
410 const mods = UserService.Instance.myUserInfo?.moderates;
411 const moderatesS = (mods && mods.length > 0) || false;
412 return amAdmin() || moderatesS;
417 if (window.document.visibilityState !== "hidden") {
418 const auth = myAuth();
421 unreadInboxCountRes: await HttpService.client.getUnreadCount({
426 if (this.moderatesSomething) {
428 unreadReportCountRes: await HttpService.client.getReportCount({
436 unreadApplicationCountRes:
437 await HttpService.client.getUnreadRegistrationApplicationCount({
444 }, updateUnreadCountsInterval);
447 get unreadInboxCount(): number {
448 if (this.state.unreadInboxCountRes.state == "success") {
449 const data = this.state.unreadInboxCountRes.data;
450 return data.replies + data.mentions + data.private_messages;
456 get unreadReportCount(): number {
457 if (this.state.unreadReportCountRes.state == "success") {
458 const data = this.state.unreadReportCountRes.data;
461 data.comment_reports +
462 (data.private_message_reports ?? 0)
469 get unreadApplicationCount(): number {
470 if (this.state.unreadApplicationCountRes.state == "success") {
471 const data = this.state.unreadApplicationCountRes.data;
472 return data.registration_applications;
478 get currentLocation() {
479 return this.context.router.history.location.pathname;
482 requestNotificationPermission() {
483 if (UserService.Instance.myUserInfo) {
484 document.addEventListener("DOMContentLoaded", function () {
486 toast(i18n.t("notifications_error"), "danger");
490 if (Notification.permission !== "granted")
491 Notification.requestPermission();