]> Untitled Git - lemmy-ui.git/blob - src/shared/components/app/navbar.tsx
d0943af2d56b072e1f78c5652d13abd25e7861e9
[lemmy-ui.git] / src / shared / components / app / navbar.tsx
1 import { Component, createRef, linkEvent } from "inferno";
2 import { NavLink } from "inferno-router";
3 import {
4   GetReportCountResponse,
5   GetSiteResponse,
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";
12 import {
13   donateLemmyUrl,
14   myAuth,
15   numToSI,
16   showAvatars,
17   toast,
18   updateUnreadCountsInterval,
19 } from "../../utils";
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";
26
27 interface NavbarProps {
28   siteRes?: GetSiteResponse;
29 }
30
31 interface NavbarState {
32   unreadInboxCountRes: RequestState<GetUnreadCountResponse>;
33   unreadReportCountRes: RequestState<GetReportCountResponse>;
34   unreadApplicationCountRes: RequestState<GetUnreadRegistrationApplicationCountResponse>;
35   onSiteBanner?(url: string): any;
36 }
37
38 function handleCollapseClick(i: Navbar) {
39   if (i.collapseButtonRef.current?.ariaExpanded === "true") {
40     i.collapseButtonRef.current?.click();
41   }
42 }
43
44 function handleLogOut(i: Navbar) {
45   UserService.Instance.logout();
46   handleCollapseClick(i);
47 }
48
49 export class Navbar extends Component<NavbarProps, NavbarState> {
50   state: NavbarState = {
51     unreadInboxCountRes: { state: "empty" },
52     unreadReportCountRes: { state: "empty" },
53     unreadApplicationCountRes: { state: "empty" },
54   };
55   collapseButtonRef = createRef<HTMLButtonElement>();
56   mobileMenuRef = createRef<HTMLDivElement>();
57
58   constructor(props: any, context: any) {
59     super(props, context);
60
61     this.handleOutsideMenuClick = this.handleOutsideMenuClick.bind(this);
62   }
63
64   async componentDidMount() {
65     // Subscribe to jwt changes
66     if (isBrowser()) {
67       // On the first load, check the unreads
68       this.requestNotificationPermission();
69       this.fetchUnreads();
70       this.requestNotificationPermission();
71
72       document.addEventListener("mouseup", this.handleOutsideMenuClick);
73     }
74   }
75
76   componentWillUnmount() {
77     document.removeEventListener("mouseup", this.handleOutsideMenuClick);
78   }
79
80   render() {
81     return this.navbar();
82   }
83
84   // TODO class active corresponding to current page
85   navbar() {
86     const siteView = this.props.siteRes?.site_view;
87     const person = UserService.Instance.myUserInfo?.local_user_view.person;
88     return (
89       <nav
90         className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3 container-lg"
91         id="navbar"
92       >
93         <NavLink
94           id="navTitle"
95           to="/"
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)}
99         >
100           {siteView?.site.icon && showAvatars() && (
101             <PictrsImage src={siteView.site.icon} icon />
102           )}
103           {siteView?.site.name}
104         </NavLink>
105         {person && (
106           <ul className="navbar-nav d-flex flex-row ml-auto d-md-none">
107             <li id="navMessages" className="nav-item nav-item-icon">
108               <NavLink
109                 to="/inbox"
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),
114                 })}
115                 onMouseUp={linkEvent(this, handleCollapseClick)}
116               >
117                 <Icon icon="bell" />
118                 {this.unreadInboxCount > 0 && (
119                   <span className="mx-1 badge badge-light">
120                     {numToSI(this.unreadInboxCount)}
121                   </span>
122                 )}
123               </NavLink>
124             </li>
125             {this.moderatesSomething && (
126               <li className="nav-item nav-item-icon">
127                 <NavLink
128                   to="/reports"
129                   className="p-1 nav-link border-0"
130                   title={i18n.t("unread_reports", {
131                     count: Number(this.unreadReportCount),
132                     formattedCount: numToSI(this.unreadReportCount),
133                   })}
134                   onMouseUp={linkEvent(this, handleCollapseClick)}
135                 >
136                   <Icon icon="shield" />
137                   {this.unreadReportCount > 0 && (
138                     <span className="mx-1 badge badge-light">
139                       {numToSI(this.unreadReportCount)}
140                     </span>
141                   )}
142                 </NavLink>
143               </li>
144             )}
145             {amAdmin() && (
146               <li className="nav-item nav-item-icon">
147                 <NavLink
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),
153                   })}
154                   onMouseUp={linkEvent(this, handleCollapseClick)}
155                 >
156                   <Icon icon="clipboard" />
157                   {this.unreadApplicationCount > 0 && (
158                     <span className="mx-1 badge badge-light">
159                       {numToSI(this.unreadApplicationCount)}
160                     </span>
161                   )}
162                 </NavLink>
163               </li>
164             )}
165           </ul>
166         )}
167         <button
168           className="navbar-toggler border-0 p-1"
169           type="button"
170           aria-label="menu"
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}
177         >
178           <Icon icon="menu" />
179         </button>
180         <div
181           className="collapse navbar-collapse my-2"
182           id="navbarDropdown"
183           ref={this.mobileMenuRef}
184         >
185           <ul id="navbarLinks" className="mr-auto navbar-nav">
186             <li className="nav-item">
187               <NavLink
188                 to="/communities"
189                 className="nav-link"
190                 title={i18n.t("communities")}
191                 onMouseUp={linkEvent(this, handleCollapseClick)}
192               >
193                 {i18n.t("communities")}
194               </NavLink>
195             </li>
196             <li className="nav-item">
197               {/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
198               <NavLink
199                 to={{
200                   pathname: "/create_post",
201                   search: "",
202                   hash: "",
203                   key: "",
204                   state: { prevPath: this.currentLocation },
205                 }}
206                 className="nav-link"
207                 title={i18n.t("create_post")}
208                 onMouseUp={linkEvent(this, handleCollapseClick)}
209               >
210                 {i18n.t("create_post")}
211               </NavLink>
212             </li>
213             {this.props.siteRes && canCreateCommunity(this.props.siteRes) && (
214               <li className="nav-item">
215                 <NavLink
216                   to="/create_community"
217                   className="nav-link"
218                   title={i18n.t("create_community")}
219                   onMouseUp={linkEvent(this, handleCollapseClick)}
220                 >
221                   {i18n.t("create_community")}
222                 </NavLink>
223               </li>
224             )}
225             <li className="nav-item">
226               <a
227                 className="nav-link d-inline-flex align-items-center d-md-inline-block"
228                 title={i18n.t("support_lemmy")}
229                 href={donateLemmyUrl}
230               >
231                 <Icon icon="heart" classes="small" />
232                 <span className="d-inline ml-1 d-md-none ml-md-0">
233                   {i18n.t("support_lemmy")}
234                 </span>
235               </a>
236             </li>
237           </ul>
238           <ul id="navbarIcons" className="navbar-nav">
239             <li id="navSearch" className="nav-item">
240               <NavLink
241                 to="/search"
242                 className="nav-link d-inline-flex align-items-center d-md-inline-block"
243                 title={i18n.t("search")}
244                 onMouseUp={linkEvent(this, handleCollapseClick)}
245               >
246                 <Icon icon="search" />
247                 <span className="d-inline ml-1 d-md-none ml-md-0">
248                   {i18n.t("search")}
249                 </span>
250               </NavLink>
251             </li>
252             {amAdmin() && (
253               <li id="navAdmin" className="nav-item">
254                 <NavLink
255                   to="/admin"
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)}
259                 >
260                   <Icon icon="settings" />
261                   <span className="d-inline ml-1 d-md-none ml-md-0">
262                     {i18n.t("admin_settings")}
263                   </span>
264                 </NavLink>
265               </li>
266             )}
267             {person ? (
268               <>
269                 <li id="navMessages" className="nav-item">
270                   <NavLink
271                     className="nav-link d-inline-flex align-items-center d-md-inline-block"
272                     to="/inbox"
273                     title={i18n.t("unread_messages", {
274                       count: Number(this.unreadInboxCount),
275                       formattedCount: numToSI(this.unreadInboxCount),
276                     })}
277                     onMouseUp={linkEvent(this, handleCollapseClick)}
278                   >
279                     <Icon icon="bell" />
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),
284                       })}
285                     </span>
286                     {this.unreadInboxCount > 0 && (
287                       <span className="mx-1 badge badge-light">
288                         {numToSI(this.unreadInboxCount)}
289                       </span>
290                     )}
291                   </NavLink>
292                 </li>
293                 {this.moderatesSomething && (
294                   <li id="navModeration" className="nav-item">
295                     <NavLink
296                       className="nav-link d-inline-flex align-items-center d-md-inline-block"
297                       to="/reports"
298                       title={i18n.t("unread_reports", {
299                         count: Number(this.unreadReportCount),
300                         formattedCount: numToSI(this.unreadReportCount),
301                       })}
302                       onMouseUp={linkEvent(this, handleCollapseClick)}
303                     >
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),
309                         })}
310                       </span>
311                       {this.unreadReportCount > 0 && (
312                         <span className="mx-1 badge badge-light">
313                           {numToSI(this.unreadReportCount)}
314                         </span>
315                       )}
316                     </NavLink>
317                   </li>
318                 )}
319                 {amAdmin() && (
320                   <li id="navApplications" className="nav-item">
321                     <NavLink
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),
327                       })}
328                       onMouseUp={linkEvent(this, handleCollapseClick)}
329                     >
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),
335                         })}
336                       </span>
337                       {this.unreadApplicationCount > 0 && (
338                         <span className="mx-1 badge badge-light">
339                           {numToSI(this.unreadApplicationCount)}
340                         </span>
341                       )}
342                     </NavLink>
343                   </li>
344                 )}
345                 {person && (
346                   <div id="dropdownUser" className="dropdown">
347                     <button
348                       className="btn dropdown-toggle"
349                       role="button"
350                       aria-expanded="false"
351                       data-bs-toggle="dropdown"
352                     >
353                       {showAvatars() && person.avatar && (
354                         <PictrsImage src={person.avatar} icon />
355                       )}
356                       {person.display_name ?? person.name}
357                     </button>
358                     <ul
359                       className="dropdown-menu"
360                       style={{ "min-width": "fit-content" }}
361                     >
362                       <li>
363                         <NavLink
364                           to={`/u/${person.name}`}
365                           className="dropdown-item px-2"
366                           title={i18n.t("profile")}
367                           onMouseUp={linkEvent(this, handleCollapseClick)}
368                         >
369                           <Icon icon="user" classes="mr-1" />
370                           {i18n.t("profile")}
371                         </NavLink>
372                       </li>
373                       <li>
374                         <NavLink
375                           to="/settings"
376                           className="dropdown-item px-2"
377                           title={i18n.t("settings")}
378                           onMouseUp={linkEvent(this, handleCollapseClick)}
379                         >
380                           <Icon icon="settings" classes="mr-1" />
381                           {i18n.t("settings")}
382                         </NavLink>
383                       </li>
384                       <li>
385                         <hr className="dropdown-divider" />
386                       </li>
387                       <li>
388                         <button
389                           className="dropdown-item btn btn-link px-2"
390                           onClick={linkEvent(this, handleLogOut)}
391                         >
392                           <Icon icon="log-out" classes="mr-1" />
393                           {i18n.t("logout")}
394                         </button>
395                       </li>
396                     </ul>
397                   </div>
398                 )}
399               </>
400             ) : (
401               <>
402                 <li className="nav-item">
403                   <NavLink
404                     to="/login"
405                     className="nav-link"
406                     title={i18n.t("login")}
407                     onMouseUp={linkEvent(this, handleCollapseClick)}
408                   >
409                     {i18n.t("login")}
410                   </NavLink>
411                 </li>
412                 <li className="nav-item">
413                   <NavLink
414                     to="/signup"
415                     className="nav-link"
416                     title={i18n.t("sign_up")}
417                     onMouseUp={linkEvent(this, handleCollapseClick)}
418                   >
419                     {i18n.t("sign_up")}
420                   </NavLink>
421                 </li>
422               </>
423             )}
424           </ul>
425         </div>
426       </nav>
427     );
428   }
429
430   handleOutsideMenuClick(event: MouseEvent) {
431     if (!this.mobileMenuRef.current?.contains(event.target as Node | null)) {
432       handleCollapseClick(this);
433     }
434   }
435
436   get moderatesSomething(): boolean {
437     const mods = UserService.Instance.myUserInfo?.moderates;
438     const moderatesS = (mods && mods.length > 0) || false;
439     return amAdmin() || moderatesS;
440   }
441
442   fetchUnreads() {
443     poll(async () => {
444       if (window.document.visibilityState !== "hidden") {
445         const auth = myAuth();
446         if (auth) {
447           this.setState({
448             unreadInboxCountRes: await HttpService.client.getUnreadCount({
449               auth,
450             }),
451           });
452
453           if (this.moderatesSomething) {
454             this.setState({
455               unreadReportCountRes: await HttpService.client.getReportCount({
456                 auth,
457               }),
458             });
459           }
460
461           if (amAdmin()) {
462             this.setState({
463               unreadApplicationCountRes:
464                 await HttpService.client.getUnreadRegistrationApplicationCount({
465                   auth,
466                 }),
467             });
468           }
469         }
470       }
471     }, updateUnreadCountsInterval);
472   }
473
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;
478     } else {
479       return 0;
480     }
481   }
482
483   get unreadReportCount(): number {
484     if (this.state.unreadReportCountRes.state == "success") {
485       const data = this.state.unreadReportCountRes.data;
486       return (
487         data.post_reports +
488         data.comment_reports +
489         (data.private_message_reports ?? 0)
490       );
491     } else {
492       return 0;
493     }
494   }
495
496   get unreadApplicationCount(): number {
497     if (this.state.unreadApplicationCountRes.state == "success") {
498       const data = this.state.unreadApplicationCountRes.data;
499       return data.registration_applications;
500     } else {
501       return 0;
502     }
503   }
504
505   get currentLocation() {
506     return this.context.router.history.location.pathname;
507   }
508
509   requestNotificationPermission() {
510     if (UserService.Instance.myUserInfo) {
511       document.addEventListener("DOMContentLoaded", function () {
512         if (!Notification) {
513           toast(i18n.t("notifications_error"), "danger");
514           return;
515         }
516
517         if (Notification.permission !== "granted")
518           Notification.requestPermission();
519       });
520     }
521   }
522 }