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