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 { I18NextService, UserService } from "../../services";
15 import { HttpService, RequestState } from "../../services/HttpService";
16 import { toast } from "../../toast";
17 import { Icon } from "../common/icon";
18 import { PictrsImage } from "../common/pictrs-image";
20 interface NavbarProps {
21 siteRes?: GetSiteResponse;
24 interface NavbarState {
25 unreadInboxCountRes: RequestState<GetUnreadCountResponse>;
26 unreadReportCountRes: RequestState<GetReportCountResponse>;
27 unreadApplicationCountRes: RequestState<GetUnreadRegistrationApplicationCountResponse>;
28 onSiteBanner?(url: string): any;
31 function handleCollapseClick(i: Navbar) {
33 i.collapseButtonRef.current?.attributes &&
34 i.collapseButtonRef.current?.attributes.getNamedItem("aria-expanded")
37 i.collapseButtonRef.current?.click();
41 function handleLogOut(i: Navbar) {
42 UserService.Instance.logout();
43 handleCollapseClick(i);
46 export class Navbar extends Component<NavbarProps, NavbarState> {
47 state: NavbarState = {
48 unreadInboxCountRes: { state: "empty" },
49 unreadReportCountRes: { state: "empty" },
50 unreadApplicationCountRes: { state: "empty" },
52 collapseButtonRef = createRef<HTMLButtonElement>();
53 mobileMenuRef = createRef<HTMLDivElement>();
55 constructor(props: any, context: any) {
56 super(props, context);
58 this.handleOutsideMenuClick = this.handleOutsideMenuClick.bind(this);
61 async componentDidMount() {
62 // Subscribe to jwt changes
64 // On the first load, check the unreads
65 this.requestNotificationPermission();
67 this.requestNotificationPermission();
69 document.addEventListener("mouseup", this.handleOutsideMenuClick);
73 componentWillUnmount() {
74 document.removeEventListener("mouseup", this.handleOutsideMenuClick);
77 // TODO class active corresponding to current pages
79 const siteView = this.props.siteRes?.site_view;
80 const person = UserService.Instance.myUserInfo?.local_user_view.person;
83 className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3 container-lg"
89 title={siteView?.site.description ?? siteView?.site.name}
90 className="d-flex align-items-center navbar-brand me-md-3"
91 onMouseUp={linkEvent(this, handleCollapseClick)}
93 {siteView?.site.icon && showAvatars() && (
94 <PictrsImage src={siteView.site.icon} icon />
99 <ul className="navbar-nav d-flex flex-row ms-auto d-md-none">
100 <li id="navMessages" className="nav-item nav-item-icon">
103 className="p-1 nav-link border-0 nav-messages"
104 title={I18NextService.i18n.t("unread_messages", {
105 count: Number(this.state.unreadApplicationCountRes.state),
106 formattedCount: numToSI(this.unreadInboxCount),
108 onMouseUp={linkEvent(this, handleCollapseClick)}
111 {this.unreadInboxCount > 0 && (
112 <span className="mx-1 badge text-bg-light">
113 {numToSI(this.unreadInboxCount)}
118 {this.moderatesSomething && (
119 <li className="nav-item nav-item-icon">
122 className="p-1 nav-link border-0"
123 title={I18NextService.i18n.t("unread_reports", {
124 count: Number(this.unreadReportCount),
125 formattedCount: numToSI(this.unreadReportCount),
127 onMouseUp={linkEvent(this, handleCollapseClick)}
129 <Icon icon="shield" />
130 {this.unreadReportCount > 0 && (
131 <span className="mx-1 badge text-bg-light">
132 {numToSI(this.unreadReportCount)}
139 <li className="nav-item nav-item-icon">
141 to="/registration_applications"
142 className="p-1 nav-link border-0"
143 title={I18NextService.i18n.t(
144 "unread_registration_applications",
146 count: Number(this.unreadApplicationCount),
147 formattedCount: numToSI(this.unreadApplicationCount),
150 onMouseUp={linkEvent(this, handleCollapseClick)}
152 <Icon icon="clipboard" />
153 {this.unreadApplicationCount > 0 && (
154 <span className="mx-1 badge text-bg-light">
155 {numToSI(this.unreadApplicationCount)}
164 className="navbar-toggler border-0 p-1"
167 data-tippy-content={I18NextService.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 id="navbarLinks" className="me-auto navbar-nav">
182 <li className="nav-item">
186 title={I18NextService.i18n.t("communities")}
187 onMouseUp={linkEvent(this, handleCollapseClick)}
189 {I18NextService.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={I18NextService.i18n.t("create_post")}
204 onMouseUp={linkEvent(this, handleCollapseClick)}
206 {I18NextService.i18n.t("create_post")}
209 {this.props.siteRes && canCreateCommunity(this.props.siteRes) && (
210 <li className="nav-item">
212 to="/create_community"
214 title={I18NextService.i18n.t("create_community")}
215 onMouseUp={linkEvent(this, handleCollapseClick)}
217 {I18NextService.i18n.t("create_community")}
221 <li className="nav-item">
223 className="nav-link d-inline-flex align-items-center d-md-inline-block"
224 title={I18NextService.i18n.t("support_lemmy")}
225 href={donateLemmyUrl}
227 <Icon icon="heart" classes="small" />
228 <span className="d-inline ms-1 d-md-none ms-md-0">
229 {I18NextService.i18n.t("support_lemmy")}
234 <ul id="navbarIcons" className="navbar-nav">
235 <li id="navSearch" className="nav-item">
238 className="nav-link d-inline-flex align-items-center d-md-inline-block"
239 title={I18NextService.i18n.t("search")}
240 onMouseUp={linkEvent(this, handleCollapseClick)}
242 <Icon icon="search" />
243 <span className="d-inline ms-1 d-md-none ms-md-0">
244 {I18NextService.i18n.t("search")}
249 <li id="navAdmin" className="nav-item">
252 className="nav-link d-inline-flex align-items-center d-md-inline-block"
253 title={I18NextService.i18n.t("admin_settings")}
254 onMouseUp={linkEvent(this, handleCollapseClick)}
256 <Icon icon="settings" />
257 <span className="d-inline ms-1 d-md-none ms-md-0">
258 {I18NextService.i18n.t("admin_settings")}
265 <li id="navMessages" className="nav-item">
267 className="nav-link d-inline-flex align-items-center d-md-inline-block"
269 title={I18NextService.i18n.t("unread_messages", {
270 count: Number(this.unreadInboxCount),
271 formattedCount: numToSI(this.unreadInboxCount),
273 onMouseUp={linkEvent(this, handleCollapseClick)}
276 <span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
277 {I18NextService.i18n.t("unread_messages", {
278 count: Number(this.unreadInboxCount),
279 formattedCount: numToSI(this.unreadInboxCount),
282 {this.unreadInboxCount > 0 && (
283 <span className="mx-1 badge text-bg-light">
284 {numToSI(this.unreadInboxCount)}
289 {this.moderatesSomething && (
290 <li id="navModeration" className="nav-item">
292 className="nav-link d-inline-flex align-items-center d-md-inline-block"
294 title={I18NextService.i18n.t("unread_reports", {
295 count: Number(this.unreadReportCount),
296 formattedCount: numToSI(this.unreadReportCount),
298 onMouseUp={linkEvent(this, handleCollapseClick)}
300 <Icon icon="shield" />
301 <span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
302 {I18NextService.i18n.t("unread_reports", {
303 count: Number(this.unreadReportCount),
304 formattedCount: numToSI(this.unreadReportCount),
307 {this.unreadReportCount > 0 && (
308 <span className="mx-1 badge text-bg-light">
309 {numToSI(this.unreadReportCount)}
316 <li id="navApplications" className="nav-item">
318 to="/registration_applications"
319 className="nav-link d-inline-flex align-items-center d-md-inline-block"
320 title={I18NextService.i18n.t(
321 "unread_registration_applications",
323 count: Number(this.unreadApplicationCount),
324 formattedCount: numToSI(this.unreadApplicationCount),
327 onMouseUp={linkEvent(this, handleCollapseClick)}
329 <Icon icon="clipboard" />
330 <span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
331 {I18NextService.i18n.t(
332 "unread_registration_applications",
334 count: Number(this.unreadApplicationCount),
335 formattedCount: numToSI(
336 this.unreadApplicationCount
341 {this.unreadApplicationCount > 0 && (
342 <span className="mx-1 badge text-bg-light">
343 {numToSI(this.unreadApplicationCount)}
350 <div id="dropdownUser" className="dropdown">
352 className="btn dropdown-toggle"
354 aria-expanded="false"
355 data-bs-toggle="dropdown"
357 {showAvatars() && person.avatar && (
358 <PictrsImage src={person.avatar} icon />
360 {person.display_name ?? person.name}
363 className="dropdown-menu"
364 style={{ "min-width": "fit-content" }}
368 to={`/u/${person.name}`}
369 className="dropdown-item px-2"
370 title={I18NextService.i18n.t("profile")}
371 onMouseUp={linkEvent(this, handleCollapseClick)}
373 <Icon icon="user" classes="me-1" />
374 {I18NextService.i18n.t("profile")}
380 className="dropdown-item px-2"
381 title={I18NextService.i18n.t("settings")}
382 onMouseUp={linkEvent(this, handleCollapseClick)}
384 <Icon icon="settings" classes="me-1" />
385 {I18NextService.i18n.t("settings")}
389 <hr className="dropdown-divider" />
393 className="dropdown-item btn btn-link px-2"
394 onClick={linkEvent(this, handleLogOut)}
396 <Icon icon="log-out" classes="me-1" />
397 {I18NextService.i18n.t("logout")}
406 <li className="nav-item">
410 title={I18NextService.i18n.t("login")}
411 onMouseUp={linkEvent(this, handleCollapseClick)}
413 {I18NextService.i18n.t("login")}
416 <li className="nav-item">
420 title={I18NextService.i18n.t("sign_up")}
421 onMouseUp={linkEvent(this, handleCollapseClick)}
423 {I18NextService.i18n.t("sign_up")}
434 handleOutsideMenuClick(event: MouseEvent) {
435 if (!this.mobileMenuRef.current?.contains(event.target as Node | null)) {
436 handleCollapseClick(this);
440 get moderatesSomething(): boolean {
441 const mods = UserService.Instance.myUserInfo?.moderates;
442 const moderatesS = (mods && mods.length > 0) || false;
443 return amAdmin() || moderatesS;
448 if (window.document.visibilityState !== "hidden") {
449 const auth = myAuth();
452 unreadInboxCountRes: await HttpService.client.getUnreadCount({
457 if (this.moderatesSomething) {
459 unreadReportCountRes: await HttpService.client.getReportCount({
467 unreadApplicationCountRes:
468 await HttpService.client.getUnreadRegistrationApplicationCount({
475 }, updateUnreadCountsInterval);
478 get unreadInboxCount(): number {
479 if (this.state.unreadInboxCountRes.state == "success") {
480 const data = this.state.unreadInboxCountRes.data;
481 return data.replies + data.mentions + data.private_messages;
487 get unreadReportCount(): number {
488 if (this.state.unreadReportCountRes.state == "success") {
489 const data = this.state.unreadReportCountRes.data;
492 data.comment_reports +
493 (data.private_message_reports ?? 0)
500 get unreadApplicationCount(): number {
501 if (this.state.unreadApplicationCountRes.state == "success") {
502 const data = this.state.unreadApplicationCountRes.data;
503 return data.registration_applications;
509 get currentLocation() {
510 return this.context.router.history.location.pathname;
513 requestNotificationPermission() {
514 if (UserService.Instance.myUserInfo) {
515 document.addEventListener("DOMContentLoaded", function () {
517 toast(I18NextService.i18n.t("notifications_error"), "danger");
521 if (Notification.permission !== "granted")
522 Notification.requestPermission();