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