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;
82 <div className="shadow-sm">
84 className="navbar navbar-expand-md navbar-light 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={I18NextService.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={I18NextService.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={I18NextService.i18n.t(
145 "unread_registration_applications",
147 count: Number(this.unreadApplicationCount),
148 formattedCount: numToSI(this.unreadApplicationCount),
151 onMouseUp={linkEvent(this, handleCollapseClick)}
153 <Icon icon="clipboard" />
154 {this.unreadApplicationCount > 0 && (
155 <span className="mx-1 badge text-bg-light">
156 {numToSI(this.unreadApplicationCount)}
165 className="navbar-toggler border-0 p-1"
168 data-tippy-content={I18NextService.i18n.t("expand_here")}
169 data-bs-toggle="collapse"
170 data-bs-target="#navbarDropdown"
171 aria-controls="navbarDropdown"
172 aria-expanded="false"
173 ref={this.collapseButtonRef}
178 className="collapse navbar-collapse my-2"
180 ref={this.mobileMenuRef}
182 <ul id="navbarLinks" className="me-auto navbar-nav">
183 <li className="nav-item">
187 title={I18NextService.i18n.t("communities")}
188 onMouseUp={linkEvent(this, handleCollapseClick)}
190 {I18NextService.i18n.t("communities")}
193 <li className="nav-item">
194 {/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
197 pathname: "/create_post",
201 state: { prevPath: this.currentLocation },
204 title={I18NextService.i18n.t("create_post")}
205 onMouseUp={linkEvent(this, handleCollapseClick)}
207 {I18NextService.i18n.t("create_post")}
210 {this.props.siteRes && canCreateCommunity(this.props.siteRes) && (
211 <li className="nav-item">
213 to="/create_community"
215 title={I18NextService.i18n.t("create_community")}
216 onMouseUp={linkEvent(this, handleCollapseClick)}
218 {I18NextService.i18n.t("create_community")}
222 <li className="nav-item">
224 className="nav-link d-inline-flex align-items-center d-md-inline-block"
225 title={I18NextService.i18n.t("support_lemmy")}
226 href={donateLemmyUrl}
228 <Icon icon="heart" classes="small" />
229 <span className="d-inline ms-1 d-md-none ms-md-0">
230 {I18NextService.i18n.t("support_lemmy")}
235 <ul id="navbarIcons" className="navbar-nav">
236 <li id="navSearch" className="nav-item">
239 className="nav-link d-inline-flex align-items-center d-md-inline-block"
240 title={I18NextService.i18n.t("search")}
241 onMouseUp={linkEvent(this, handleCollapseClick)}
243 <Icon icon="search" />
244 <span className="d-inline ms-1 d-md-none ms-md-0">
245 {I18NextService.i18n.t("search")}
250 <li id="navAdmin" className="nav-item">
253 className="nav-link d-inline-flex align-items-center d-md-inline-block"
254 title={I18NextService.i18n.t("admin_settings")}
255 onMouseUp={linkEvent(this, handleCollapseClick)}
257 <Icon icon="settings" />
258 <span className="d-inline ms-1 d-md-none ms-md-0">
259 {I18NextService.i18n.t("admin_settings")}
266 <li id="navMessages" className="nav-item">
268 className="nav-link d-inline-flex align-items-center d-md-inline-block"
270 title={I18NextService.i18n.t("unread_messages", {
271 count: Number(this.unreadInboxCount),
272 formattedCount: numToSI(this.unreadInboxCount),
274 onMouseUp={linkEvent(this, handleCollapseClick)}
277 <span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
278 {I18NextService.i18n.t("unread_messages", {
279 count: Number(this.unreadInboxCount),
280 formattedCount: numToSI(this.unreadInboxCount),
283 {this.unreadInboxCount > 0 && (
284 <span className="mx-1 badge text-bg-light">
285 {numToSI(this.unreadInboxCount)}
290 {this.moderatesSomething && (
291 <li id="navModeration" className="nav-item">
293 className="nav-link d-inline-flex align-items-center d-md-inline-block"
295 title={I18NextService.i18n.t("unread_reports", {
296 count: Number(this.unreadReportCount),
297 formattedCount: numToSI(this.unreadReportCount),
299 onMouseUp={linkEvent(this, handleCollapseClick)}
301 <Icon icon="shield" />
302 <span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
303 {I18NextService.i18n.t("unread_reports", {
304 count: Number(this.unreadReportCount),
305 formattedCount: numToSI(this.unreadReportCount),
308 {this.unreadReportCount > 0 && (
309 <span className="mx-1 badge text-bg-light">
310 {numToSI(this.unreadReportCount)}
317 <li id="navApplications" className="nav-item">
319 to="/registration_applications"
320 className="nav-link d-inline-flex align-items-center d-md-inline-block"
321 title={I18NextService.i18n.t(
322 "unread_registration_applications",
324 count: Number(this.unreadApplicationCount),
325 formattedCount: numToSI(
326 this.unreadApplicationCount
330 onMouseUp={linkEvent(this, handleCollapseClick)}
332 <Icon icon="clipboard" />
333 <span className="badge text-bg-light d-inline ms-1 d-md-none ms-md-0">
334 {I18NextService.i18n.t(
335 "unread_registration_applications",
337 count: Number(this.unreadApplicationCount),
338 formattedCount: numToSI(
339 this.unreadApplicationCount
344 {this.unreadApplicationCount > 0 && (
345 <span className="mx-1 badge text-bg-light">
346 {numToSI(this.unreadApplicationCount)}
353 <li id="dropdownUser" className="dropdown">
356 className="btn dropdown-toggle"
357 aria-expanded="false"
358 data-bs-toggle="dropdown"
360 {showAvatars() && person.avatar && (
361 <PictrsImage src={person.avatar} icon />
363 {person.display_name ?? person.name}
366 className="dropdown-menu"
367 style={{ "min-width": "fit-content" }}
371 to={`/u/${person.name}`}
372 className="dropdown-item px-2"
373 title={I18NextService.i18n.t("profile")}
374 onMouseUp={linkEvent(this, handleCollapseClick)}
376 <Icon icon="user" classes="me-1" />
377 {I18NextService.i18n.t("profile")}
383 className="dropdown-item px-2"
384 title={I18NextService.i18n.t("settings")}
385 onMouseUp={linkEvent(this, handleCollapseClick)}
387 <Icon icon="settings" classes="me-1" />
388 {I18NextService.i18n.t("settings")}
392 <hr className="dropdown-divider" />
396 className="dropdown-item btn btn-link px-2"
397 onClick={linkEvent(this, handleLogOut)}
399 <Icon icon="log-out" classes="me-1" />
400 {I18NextService.i18n.t("logout")}
409 <li className="nav-item">
413 title={I18NextService.i18n.t("login")}
414 onMouseUp={linkEvent(this, handleCollapseClick)}
416 {I18NextService.i18n.t("login")}
419 <li className="nav-item">
423 title={I18NextService.i18n.t("sign_up")}
424 onMouseUp={linkEvent(this, handleCollapseClick)}
426 {I18NextService.i18n.t("sign_up")}
438 handleOutsideMenuClick(event: MouseEvent) {
439 if (!this.mobileMenuRef.current?.contains(event.target as Node | null)) {
440 handleCollapseClick(this);
444 get moderatesSomething(): boolean {
445 const mods = UserService.Instance.myUserInfo?.moderates;
446 const moderatesS = (mods && mods.length > 0) || false;
447 return amAdmin() || moderatesS;
452 if (window.document.visibilityState !== "hidden") {
453 const auth = myAuth();
456 unreadInboxCountRes: await HttpService.client.getUnreadCount({
461 if (this.moderatesSomething) {
463 unreadReportCountRes: await HttpService.client.getReportCount({
471 unreadApplicationCountRes:
472 await HttpService.client.getUnreadRegistrationApplicationCount({
479 }, updateUnreadCountsInterval);
482 get unreadInboxCount(): number {
483 if (this.state.unreadInboxCountRes.state == "success") {
484 const data = this.state.unreadInboxCountRes.data;
485 return data.replies + data.mentions + data.private_messages;
491 get unreadReportCount(): number {
492 if (this.state.unreadReportCountRes.state == "success") {
493 const data = this.state.unreadReportCountRes.data;
496 data.comment_reports +
497 (data.private_message_reports ?? 0)
504 get unreadApplicationCount(): number {
505 if (this.state.unreadApplicationCountRes.state == "success") {
506 const data = this.state.unreadApplicationCountRes.data;
507 return data.registration_applications;
513 get currentLocation() {
514 return this.context.router.history.location.pathname;
517 requestNotificationPermission() {
518 if (UserService.Instance.myUserInfo) {
519 document.addEventListener("DOMContentLoaded", function () {
521 toast(I18NextService.i18n.t("notifications_error"), "danger");
525 if (Notification.permission !== "granted")
526 Notification.requestPermission();