]> Untitled Git - lemmy-ui.git/blob - src/shared/components/app/navbar.tsx
Merge branch 'main' into feat/markdown-format-bar-above
[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   amAdmin,
14   canCreateCommunity,
15   donateLemmyUrl,
16   isBrowser,
17   myAuth,
18   numToSI,
19   poll,
20   showAvatars,
21   toast,
22   updateUnreadCountsInterval,
23 } from "../../utils";
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"
228                 title={i18n.t("support_lemmy")}
229                 href={donateLemmyUrl}
230               >
231                 <Icon icon="heart" classes="small" />
232               </a>
233             </li>
234           </ul>
235           <ul id="navbarIcons" className="navbar-nav">
236             <li id="navSearch" className="nav-item">
237               <NavLink
238                 to="/search"
239                 className="nav-link"
240                 title={i18n.t("search")}
241                 onMouseUp={linkEvent(this, handleCollapseClick)}
242               >
243                 <Icon icon="search" />
244               </NavLink>
245             </li>
246             {amAdmin() && (
247               <li id="navAdmin" className="nav-item">
248                 <NavLink
249                   to="/admin"
250                   className="nav-link"
251                   title={i18n.t("admin_settings")}
252                   onMouseUp={linkEvent(this, handleCollapseClick)}
253                 >
254                   <Icon icon="settings" />
255                 </NavLink>
256               </li>
257             )}
258             {person ? (
259               <>
260                 <li id="navMessages" className="nav-item">
261                   <NavLink
262                     className="nav-link"
263                     to="/inbox"
264                     title={i18n.t("unread_messages", {
265                       count: Number(this.unreadInboxCount),
266                       formattedCount: numToSI(this.unreadInboxCount),
267                     })}
268                     onMouseUp={linkEvent(this, handleCollapseClick)}
269                   >
270                     <Icon icon="bell" />
271                     {this.unreadInboxCount > 0 && (
272                       <span className="mx-1 badge badge-light">
273                         {numToSI(this.unreadInboxCount)}
274                       </span>
275                     )}
276                   </NavLink>
277                 </li>
278                 {this.moderatesSomething && (
279                   <li id="navModeration" className="nav-item">
280                     <NavLink
281                       className="nav-link"
282                       to="/reports"
283                       title={i18n.t("unread_reports", {
284                         count: Number(this.unreadReportCount),
285                         formattedCount: numToSI(this.unreadReportCount),
286                       })}
287                       onMouseUp={linkEvent(this, handleCollapseClick)}
288                     >
289                       <Icon icon="shield" />
290                       {this.unreadReportCount > 0 && (
291                         <span className="mx-1 badge badge-light">
292                           {numToSI(this.unreadReportCount)}
293                         </span>
294                       )}
295                     </NavLink>
296                   </li>
297                 )}
298                 {amAdmin() && (
299                   <li id="navApplications" className="nav-item">
300                     <NavLink
301                       to="/registration_applications"
302                       className="nav-link"
303                       title={i18n.t("unread_registration_applications", {
304                         count: Number(this.unreadApplicationCount),
305                         formattedCount: numToSI(this.unreadApplicationCount),
306                       })}
307                       onMouseUp={linkEvent(this, handleCollapseClick)}
308                     >
309                       <Icon icon="clipboard" />
310                       {this.unreadApplicationCount > 0 && (
311                         <span className="mx-1 badge badge-light">
312                           {numToSI(this.unreadApplicationCount)}
313                         </span>
314                       )}
315                     </NavLink>
316                   </li>
317                 )}
318                 {person && (
319                   <div id="dropdownUser" className="dropdown">
320                     <button
321                       className="btn dropdown-toggle"
322                       role="button"
323                       aria-expanded="false"
324                       data-bs-toggle="dropdown"
325                     >
326                       {showAvatars() && person.avatar && (
327                         <PictrsImage src={person.avatar} icon />
328                       )}
329                       {person.display_name ?? person.name}
330                     </button>
331                     <ul
332                       className="dropdown-menu"
333                       style={{ "min-width": "fit-content" }}
334                     >
335                       <li>
336                         <NavLink
337                           to={`/u/${person.name}`}
338                           className="dropdown-item px-2"
339                           title={i18n.t("profile")}
340                           onMouseUp={linkEvent(this, handleCollapseClick)}
341                         >
342                           <Icon icon="user" classes="mr-1" />
343                           {i18n.t("profile")}
344                         </NavLink>
345                       </li>
346                       <li>
347                         <NavLink
348                           to="/settings"
349                           className="dropdown-item px-2"
350                           title={i18n.t("settings")}
351                           onMouseUp={linkEvent(this, handleCollapseClick)}
352                         >
353                           <Icon icon="settings" classes="mr-1" />
354                           {i18n.t("settings")}
355                         </NavLink>
356                       </li>
357                       <li>
358                         <hr className="dropdown-divider" />
359                       </li>
360                       <li>
361                         <button
362                           className="dropdown-item btn btn-link px-2"
363                           onClick={linkEvent(this, handleLogOut)}
364                         >
365                           <Icon icon="log-out" classes="mr-1" />
366                           {i18n.t("logout")}
367                         </button>
368                       </li>
369                     </ul>
370                   </div>
371                 )}
372               </>
373             ) : (
374               <>
375                 <li className="nav-item">
376                   <NavLink
377                     to="/login"
378                     className="nav-link"
379                     title={i18n.t("login")}
380                     onMouseUp={linkEvent(this, handleCollapseClick)}
381                   >
382                     {i18n.t("login")}
383                   </NavLink>
384                 </li>
385                 <li className="nav-item">
386                   <NavLink
387                     to="/signup"
388                     className="nav-link"
389                     title={i18n.t("sign_up")}
390                     onMouseUp={linkEvent(this, handleCollapseClick)}
391                   >
392                     {i18n.t("sign_up")}
393                   </NavLink>
394                 </li>
395               </>
396             )}
397           </ul>
398         </div>
399       </nav>
400     );
401   }
402
403   handleOutsideMenuClick(event: MouseEvent) {
404     if (!this.mobileMenuRef.current?.contains(event.target as Node | null)) {
405       handleCollapseClick(this);
406     }
407   }
408
409   get moderatesSomething(): boolean {
410     const mods = UserService.Instance.myUserInfo?.moderates;
411     const moderatesS = (mods && mods.length > 0) || false;
412     return amAdmin() || moderatesS;
413   }
414
415   fetchUnreads() {
416     poll(async () => {
417       if (window.document.visibilityState !== "hidden") {
418         const auth = myAuth();
419         if (auth) {
420           this.setState({
421             unreadInboxCountRes: await HttpService.client.getUnreadCount({
422               auth,
423             }),
424           });
425
426           if (this.moderatesSomething) {
427             this.setState({
428               unreadReportCountRes: await HttpService.client.getReportCount({
429                 auth,
430               }),
431             });
432           }
433
434           if (amAdmin()) {
435             this.setState({
436               unreadApplicationCountRes:
437                 await HttpService.client.getUnreadRegistrationApplicationCount({
438                   auth,
439                 }),
440             });
441           }
442         }
443       }
444     }, updateUnreadCountsInterval);
445   }
446
447   get unreadInboxCount(): number {
448     if (this.state.unreadInboxCountRes.state == "success") {
449       const data = this.state.unreadInboxCountRes.data;
450       return data.replies + data.mentions + data.private_messages;
451     } else {
452       return 0;
453     }
454   }
455
456   get unreadReportCount(): number {
457     if (this.state.unreadReportCountRes.state == "success") {
458       const data = this.state.unreadReportCountRes.data;
459       return (
460         data.post_reports +
461         data.comment_reports +
462         (data.private_message_reports ?? 0)
463       );
464     } else {
465       return 0;
466     }
467   }
468
469   get unreadApplicationCount(): number {
470     if (this.state.unreadApplicationCountRes.state == "success") {
471       const data = this.state.unreadApplicationCountRes.data;
472       return data.registration_applications;
473     } else {
474       return 0;
475     }
476   }
477
478   get currentLocation() {
479     return this.context.router.history.location.pathname;
480   }
481
482   requestNotificationPermission() {
483     if (UserService.Instance.myUserInfo) {
484       document.addEventListener("DOMContentLoaded", function () {
485         if (!Notification) {
486           toast(i18n.t("notifications_error"), "danger");
487           return;
488         }
489
490         if (Notification.permission !== "granted")
491           Notification.requestPermission();
492       });
493     }
494   }
495 }