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;
89 <nav className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3 container-lg">
92 title={siteView?.site.description ?? siteView?.site.name}
93 className="d-flex align-items-center navbar-brand mr-md-3"
94 onMouseUp={linkEvent(this, handleCollapseClick)}
96 {siteView?.site.icon && showAvatars() && (
97 <PictrsImage src={siteView.site.icon} icon />
102 <ul className="navbar-nav d-flex flex-row ml-auto d-md-none">
103 <li className="nav-item">
106 className="p-1 nav-link border-0"
107 title={i18n.t("unread_messages", {
108 count: Number(this.state.unreadApplicationCountRes.state),
109 formattedCount: numToSI(this.unreadInboxCount),
111 onMouseUp={linkEvent(this, handleCollapseClick)}
114 {this.unreadInboxCount > 0 && (
115 <span className="mx-1 badge badge-light">
116 {numToSI(this.unreadInboxCount)}
121 {this.moderatesSomething && (
122 <li className="nav-item">
125 className="p-1 nav-link border-0"
126 title={i18n.t("unread_reports", {
127 count: Number(this.unreadReportCount),
128 formattedCount: numToSI(this.unreadReportCount),
130 onMouseUp={linkEvent(this, handleCollapseClick)}
132 <Icon icon="shield" />
133 {this.unreadReportCount > 0 && (
134 <span className="mx-1 badge badge-light">
135 {numToSI(this.unreadReportCount)}
142 <li className="nav-item">
144 to="/registration_applications"
145 className="p-1 nav-link border-0"
146 title={i18n.t("unread_registration_applications", {
147 count: Number(this.unreadApplicationCount),
148 formattedCount: numToSI(this.unreadApplicationCount),
150 onMouseUp={linkEvent(this, handleCollapseClick)}
152 <Icon icon="clipboard" />
153 {this.unreadApplicationCount > 0 && (
154 <span className="mx-1 badge badge-light">
155 {numToSI(this.unreadApplicationCount)}
164 className="navbar-toggler border-0 p-1"
167 data-tippy-content={i18n.t("expand_here")}
168 data-bs-toggle="collapse"
169 data-bs-target="#navbarDropdown"
170 aria-controls="navbarDropdown"
171 aria-expanded="false"
172 ref={this.collapseButtonRef}
177 className="collapse navbar-collapse my-2"
179 ref={this.mobileMenuRef}
181 <ul className="mr-auto navbar-nav">
182 <li className="nav-item">
186 title={i18n.t("communities")}
187 onMouseUp={linkEvent(this, handleCollapseClick)}
189 {i18n.t("communities")}
192 <li className="nav-item">
193 {/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
196 pathname: "/create_post",
200 state: { prevPath: this.currentLocation },
203 title={i18n.t("create_post")}
204 onMouseUp={linkEvent(this, handleCollapseClick)}
206 {i18n.t("create_post")}
209 {this.props.siteRes && canCreateCommunity(this.props.siteRes) && (
210 <li className="nav-item">
212 to="/create_community"
214 title={i18n.t("create_community")}
215 onMouseUp={linkEvent(this, handleCollapseClick)}
217 {i18n.t("create_community")}
221 <li className="nav-item">
224 title={i18n.t("support_lemmy")}
225 href={donateLemmyUrl}
227 <Icon icon="heart" classes="small" />
231 <ul className="navbar-nav">
232 <li className="nav-item">
236 title={i18n.t("search")}
237 onMouseUp={linkEvent(this, handleCollapseClick)}
239 <Icon icon="search" />
243 <li className="nav-item">
247 title={i18n.t("admin_settings")}
248 onMouseUp={linkEvent(this, handleCollapseClick)}
250 <Icon icon="settings" />
256 <li className="nav-item">
260 title={i18n.t("unread_messages", {
261 count: Number(this.unreadInboxCount),
262 formattedCount: numToSI(this.unreadInboxCount),
264 onMouseUp={linkEvent(this, handleCollapseClick)}
267 {this.unreadInboxCount > 0 && (
268 <span className="mx-1 badge badge-light">
269 {numToSI(this.unreadInboxCount)}
274 {this.moderatesSomething && (
275 <li className="nav-item">
279 title={i18n.t("unread_reports", {
280 count: Number(this.unreadReportCount),
281 formattedCount: numToSI(this.unreadReportCount),
283 onMouseUp={linkEvent(this, handleCollapseClick)}
285 <Icon icon="shield" />
286 {this.unreadReportCount > 0 && (
287 <span className="mx-1 badge badge-light">
288 {numToSI(this.unreadReportCount)}
295 <li className="nav-item">
297 to="/registration_applications"
299 title={i18n.t("unread_registration_applications", {
300 count: Number(this.unreadApplicationCount),
301 formattedCount: numToSI(this.unreadApplicationCount),
303 onMouseUp={linkEvent(this, handleCollapseClick)}
305 <Icon icon="clipboard" />
306 {this.unreadApplicationCount > 0 && (
307 <span className="mx-1 badge badge-light">
308 {numToSI(this.unreadApplicationCount)}
315 <div className="dropdown">
317 className="btn dropdown-toggle"
319 aria-expanded="false"
320 data-bs-toggle="dropdown"
322 {showAvatars() && person.avatar && (
323 <PictrsImage src={person.avatar} icon />
325 {person.display_name ?? person.name}
328 className="dropdown-menu"
329 style={{ "min-width": "fit-content" }}
333 to={`/u/${person.name}`}
334 className="dropdown-item px-2"
335 title={i18n.t("profile")}
336 onMouseUp={linkEvent(this, handleCollapseClick)}
338 <Icon icon="user" classes="mr-1" />
345 className="dropdown-item px-2"
346 title={i18n.t("settings")}
347 onMouseUp={linkEvent(this, handleCollapseClick)}
349 <Icon icon="settings" classes="mr-1" />
354 <hr className="dropdown-divider" />
358 className="dropdown-item btn btn-link px-2"
359 onClick={linkEvent(this, handleLogOut)}
361 <Icon icon="log-out" classes="mr-1" />
371 <li className="nav-item">
375 title={i18n.t("login")}
376 onMouseUp={linkEvent(this, handleCollapseClick)}
381 <li className="nav-item">
385 title={i18n.t("sign_up")}
386 onMouseUp={linkEvent(this, handleCollapseClick)}
399 handleOutsideMenuClick(event: MouseEvent) {
400 if (!this.mobileMenuRef.current?.contains(event.target as Node | null)) {
401 handleCollapseClick(this);
405 get moderatesSomething(): boolean {
406 const mods = UserService.Instance.myUserInfo?.moderates;
407 const moderatesS = (mods && mods.length > 0) || false;
408 return amAdmin() || moderatesS;
413 if (window.document.visibilityState !== "hidden") {
414 const auth = myAuth();
417 unreadInboxCountRes: await HttpService.client.getUnreadCount({
422 if (this.moderatesSomething) {
424 unreadReportCountRes: await HttpService.client.getReportCount({
432 unreadApplicationCountRes:
433 await HttpService.client.getUnreadRegistrationApplicationCount({
440 }, updateUnreadCountsInterval);
443 get unreadInboxCount(): number {
444 if (this.state.unreadInboxCountRes.state == "success") {
445 const data = this.state.unreadInboxCountRes.data;
446 return data.replies + data.mentions + data.private_messages;
452 get unreadReportCount(): number {
453 if (this.state.unreadReportCountRes.state == "success") {
454 const data = this.state.unreadReportCountRes.data;
457 data.comment_reports +
458 (data.private_message_reports ?? 0)
465 get unreadApplicationCount(): number {
466 if (this.state.unreadApplicationCountRes.state == "success") {
467 const data = this.state.unreadApplicationCountRes.data;
468 return data.registration_applications;
474 get currentLocation() {
475 return this.context.router.history.location.pathname;
478 requestNotificationPermission() {
479 if (UserService.Instance.myUserInfo) {
480 document.addEventListener("DOMContentLoaded", function () {
482 toast(i18n.t("notifications_error"), "danger");
486 if (Notification.permission !== "granted")
487 Notification.requestPermission();