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">
227 className="nav-link d-inline-flex align-items-center d-md-inline-block"
228 title={i18n.t("support_lemmy")}
229 href={donateLemmyUrl}
231 <Icon icon="heart" classes="small" />
232 <span className="d-inline ml-1 d-md-none ml-md-0">
233 {i18n.t("support_lemmy")}
238 <ul id="navbarIcons" className="navbar-nav">
239 <li id="navSearch" className="nav-item">
242 className="nav-link d-inline-flex align-items-center d-md-inline-block"
243 title={i18n.t("search")}
244 onMouseUp={linkEvent(this, handleCollapseClick)}
246 <Icon icon="search" />
247 <span className="d-inline ml-1 d-md-none ml-md-0">
253 <li id="navAdmin" className="nav-item">
256 className="nav-link d-inline-flex align-items-center d-md-inline-block"
257 title={i18n.t("admin_settings")}
258 onMouseUp={linkEvent(this, handleCollapseClick)}
260 <Icon icon="settings" />
261 <span className="d-inline ml-1 d-md-none ml-md-0">
262 {i18n.t("admin_settings")}
269 <li id="navMessages" className="nav-item">
271 className="nav-link d-inline-flex align-items-center d-md-inline-block"
273 title={i18n.t("unread_messages", {
274 count: Number(this.unreadInboxCount),
275 formattedCount: numToSI(this.unreadInboxCount),
277 onMouseUp={linkEvent(this, handleCollapseClick)}
280 <span className="badge badge-light d-inline ml-1 d-md-none ml-md-0">
281 {i18n.t("unread_messages", {
282 count: Number(this.unreadInboxCount),
283 formattedCount: numToSI(this.unreadInboxCount),
286 {this.unreadInboxCount > 0 && (
287 <span className="mx-1 badge badge-light">
288 {numToSI(this.unreadInboxCount)}
293 {this.moderatesSomething && (
294 <li id="navModeration" className="nav-item">
296 className="nav-link d-inline-flex align-items-center d-md-inline-block"
298 title={i18n.t("unread_reports", {
299 count: Number(this.unreadReportCount),
300 formattedCount: numToSI(this.unreadReportCount),
302 onMouseUp={linkEvent(this, handleCollapseClick)}
304 <Icon icon="shield" />
305 <span className="badge badge-light d-inline ml-1 d-md-none ml-md-0">
306 {i18n.t("unread_reports", {
307 count: Number(this.unreadReportCount),
308 formattedCount: numToSI(this.unreadReportCount),
311 {this.unreadReportCount > 0 && (
312 <span className="mx-1 badge badge-light">
313 {numToSI(this.unreadReportCount)}
320 <li id="navApplications" className="nav-item">
322 to="/registration_applications"
323 className="nav-link d-inline-flex align-items-center d-md-inline-block"
324 title={i18n.t("unread_registration_applications", {
325 count: Number(this.unreadApplicationCount),
326 formattedCount: numToSI(this.unreadApplicationCount),
328 onMouseUp={linkEvent(this, handleCollapseClick)}
330 <Icon icon="clipboard" />
331 <span className="badge badge-light d-inline ml-1 d-md-none ml-md-0">
332 {i18n.t("unread_registration_applications", {
333 count: Number(this.unreadApplicationCount),
334 formattedCount: numToSI(this.unreadApplicationCount),
337 {this.unreadApplicationCount > 0 && (
338 <span className="mx-1 badge badge-light">
339 {numToSI(this.unreadApplicationCount)}
346 <div id="dropdownUser" className="dropdown">
348 className="btn dropdown-toggle"
350 aria-expanded="false"
351 data-bs-toggle="dropdown"
353 {showAvatars() && person.avatar && (
354 <PictrsImage src={person.avatar} icon />
356 {person.display_name ?? person.name}
359 className="dropdown-menu"
360 style={{ "min-width": "fit-content" }}
364 to={`/u/${person.name}`}
365 className="dropdown-item px-2"
366 title={i18n.t("profile")}
367 onMouseUp={linkEvent(this, handleCollapseClick)}
369 <Icon icon="user" classes="mr-1" />
376 className="dropdown-item px-2"
377 title={i18n.t("settings")}
378 onMouseUp={linkEvent(this, handleCollapseClick)}
380 <Icon icon="settings" classes="mr-1" />
385 <hr className="dropdown-divider" />
389 className="dropdown-item btn btn-link px-2"
390 onClick={linkEvent(this, handleLogOut)}
392 <Icon icon="log-out" classes="mr-1" />
402 <li className="nav-item">
406 title={i18n.t("login")}
407 onMouseUp={linkEvent(this, handleCollapseClick)}
412 <li className="nav-item">
416 title={i18n.t("sign_up")}
417 onMouseUp={linkEvent(this, handleCollapseClick)}
430 handleOutsideMenuClick(event: MouseEvent) {
431 if (!this.mobileMenuRef.current?.contains(event.target as Node | null)) {
432 handleCollapseClick(this);
436 get moderatesSomething(): boolean {
437 const mods = UserService.Instance.myUserInfo?.moderates;
438 const moderatesS = (mods && mods.length > 0) || false;
439 return amAdmin() || moderatesS;
444 if (window.document.visibilityState !== "hidden") {
445 const auth = myAuth();
448 unreadInboxCountRes: await HttpService.client.getUnreadCount({
453 if (this.moderatesSomething) {
455 unreadReportCountRes: await HttpService.client.getReportCount({
463 unreadApplicationCountRes:
464 await HttpService.client.getUnreadRegistrationApplicationCount({
471 }, updateUnreadCountsInterval);
474 get unreadInboxCount(): number {
475 if (this.state.unreadInboxCountRes.state == "success") {
476 const data = this.state.unreadInboxCountRes.data;
477 return data.replies + data.mentions + data.private_messages;
483 get unreadReportCount(): number {
484 if (this.state.unreadReportCountRes.state == "success") {
485 const data = this.state.unreadReportCountRes.data;
488 data.comment_reports +
489 (data.private_message_reports ?? 0)
496 get unreadApplicationCount(): number {
497 if (this.state.unreadApplicationCountRes.state == "success") {
498 const data = this.state.unreadApplicationCountRes.data;
499 return data.registration_applications;
505 get currentLocation() {
506 return this.context.router.history.location.pathname;
509 requestNotificationPermission() {
510 if (UserService.Instance.myUserInfo) {
511 document.addEventListener("DOMContentLoaded", function () {
513 toast(i18n.t("notifications_error"), "danger");
517 if (Notification.permission !== "granted")
518 Notification.requestPermission();