]> Untitled Git - lemmy-ui.git/blob - src/shared/components/app/navbar.tsx
Merge branch 'main' into breakout-role-utils
[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 className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3 container-lg">
90         <NavLink
91           to="/"
92           title={siteView?.site.description ?? siteView?.site.name}
93           className="d-flex align-items-center navbar-brand mr-md-3"
94           onMouseUp={linkEvent(this, handleCollapseClick)}
95         >
96           {siteView?.site.icon && showAvatars() && (
97             <PictrsImage src={siteView.site.icon} icon />
98           )}
99           {siteView?.site.name}
100         </NavLink>
101         {person && (
102           <ul className="navbar-nav d-flex flex-row ml-auto d-md-none">
103             <li className="nav-item">
104               <NavLink
105                 to="/inbox"
106                 className="p-1 nav-link border-0"
107                 title={i18n.t("unread_messages", {
108                   count: Number(this.state.unreadApplicationCountRes.state),
109                   formattedCount: numToSI(this.unreadInboxCount),
110                 })}
111                 onMouseUp={linkEvent(this, handleCollapseClick)}
112               >
113                 <Icon icon="bell" />
114                 {this.unreadInboxCount > 0 && (
115                   <span className="mx-1 badge badge-light">
116                     {numToSI(this.unreadInboxCount)}
117                   </span>
118                 )}
119               </NavLink>
120             </li>
121             {this.moderatesSomething && (
122               <li className="nav-item">
123                 <NavLink
124                   to="/reports"
125                   className="p-1 nav-link border-0"
126                   title={i18n.t("unread_reports", {
127                     count: Number(this.unreadReportCount),
128                     formattedCount: numToSI(this.unreadReportCount),
129                   })}
130                   onMouseUp={linkEvent(this, handleCollapseClick)}
131                 >
132                   <Icon icon="shield" />
133                   {this.unreadReportCount > 0 && (
134                     <span className="mx-1 badge badge-light">
135                       {numToSI(this.unreadReportCount)}
136                     </span>
137                   )}
138                 </NavLink>
139               </li>
140             )}
141             {amAdmin() && (
142               <li className="nav-item">
143                 <NavLink
144                   to="/registration_applications"
145                   className="p-1 nav-link border-0"
146                   title={i18n.t("unread_registration_applications", {
147                     count: Number(this.unreadApplicationCount),
148                     formattedCount: numToSI(this.unreadApplicationCount),
149                   })}
150                   onMouseUp={linkEvent(this, handleCollapseClick)}
151                 >
152                   <Icon icon="clipboard" />
153                   {this.unreadApplicationCount > 0 && (
154                     <span className="mx-1 badge badge-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={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 className="mr-auto navbar-nav">
182             <li className="nav-item">
183               <NavLink
184                 to="/communities"
185                 className="nav-link"
186                 title={i18n.t("communities")}
187                 onMouseUp={linkEvent(this, handleCollapseClick)}
188               >
189                 {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={i18n.t("create_post")}
204                 onMouseUp={linkEvent(this, handleCollapseClick)}
205               >
206                 {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={i18n.t("create_community")}
215                   onMouseUp={linkEvent(this, handleCollapseClick)}
216                 >
217                   {i18n.t("create_community")}
218                 </NavLink>
219               </li>
220             )}
221             <li className="nav-item">
222               <a
223                 className="nav-link"
224                 title={i18n.t("support_lemmy")}
225                 href={donateLemmyUrl}
226               >
227                 <Icon icon="heart" classes="small" />
228               </a>
229             </li>
230           </ul>
231           <ul className="navbar-nav">
232             <li className="nav-item">
233               <NavLink
234                 to="/search"
235                 className="nav-link"
236                 title={i18n.t("search")}
237                 onMouseUp={linkEvent(this, handleCollapseClick)}
238               >
239                 <Icon icon="search" />
240               </NavLink>
241             </li>
242             {amAdmin() && (
243               <li className="nav-item">
244                 <NavLink
245                   to="/admin"
246                   className="nav-link"
247                   title={i18n.t("admin_settings")}
248                   onMouseUp={linkEvent(this, handleCollapseClick)}
249                 >
250                   <Icon icon="settings" />
251                 </NavLink>
252               </li>
253             )}
254             {person ? (
255               <>
256                 <li className="nav-item">
257                   <NavLink
258                     className="nav-link"
259                     to="/inbox"
260                     title={i18n.t("unread_messages", {
261                       count: Number(this.unreadInboxCount),
262                       formattedCount: numToSI(this.unreadInboxCount),
263                     })}
264                     onMouseUp={linkEvent(this, handleCollapseClick)}
265                   >
266                     <Icon icon="bell" />
267                     {this.unreadInboxCount > 0 && (
268                       <span className="mx-1 badge badge-light">
269                         {numToSI(this.unreadInboxCount)}
270                       </span>
271                     )}
272                   </NavLink>
273                 </li>
274                 {this.moderatesSomething && (
275                   <li className="nav-item">
276                     <NavLink
277                       className="nav-link"
278                       to="/reports"
279                       title={i18n.t("unread_reports", {
280                         count: Number(this.unreadReportCount),
281                         formattedCount: numToSI(this.unreadReportCount),
282                       })}
283                       onMouseUp={linkEvent(this, handleCollapseClick)}
284                     >
285                       <Icon icon="shield" />
286                       {this.unreadReportCount > 0 && (
287                         <span className="mx-1 badge badge-light">
288                           {numToSI(this.unreadReportCount)}
289                         </span>
290                       )}
291                     </NavLink>
292                   </li>
293                 )}
294                 {amAdmin() && (
295                   <li className="nav-item">
296                     <NavLink
297                       to="/registration_applications"
298                       className="nav-link"
299                       title={i18n.t("unread_registration_applications", {
300                         count: Number(this.unreadApplicationCount),
301                         formattedCount: numToSI(this.unreadApplicationCount),
302                       })}
303                       onMouseUp={linkEvent(this, handleCollapseClick)}
304                     >
305                       <Icon icon="clipboard" />
306                       {this.unreadApplicationCount > 0 && (
307                         <span className="mx-1 badge badge-light">
308                           {numToSI(this.unreadApplicationCount)}
309                         </span>
310                       )}
311                     </NavLink>
312                   </li>
313                 )}
314                 {person && (
315                   <div className="dropdown">
316                     <button
317                       className="btn dropdown-toggle"
318                       role="button"
319                       aria-expanded="false"
320                       data-bs-toggle="dropdown"
321                     >
322                       {showAvatars() && person.avatar && (
323                         <PictrsImage src={person.avatar} icon />
324                       )}
325                       {person.display_name ?? person.name}
326                     </button>
327                     <ul
328                       className="dropdown-menu"
329                       style={{ "min-width": "fit-content" }}
330                     >
331                       <li>
332                         <NavLink
333                           to={`/u/${person.name}`}
334                           className="dropdown-item px-2"
335                           title={i18n.t("profile")}
336                           onMouseUp={linkEvent(this, handleCollapseClick)}
337                         >
338                           <Icon icon="user" classes="mr-1" />
339                           {i18n.t("profile")}
340                         </NavLink>
341                       </li>
342                       <li>
343                         <NavLink
344                           to="/settings"
345                           className="dropdown-item px-2"
346                           title={i18n.t("settings")}
347                           onMouseUp={linkEvent(this, handleCollapseClick)}
348                         >
349                           <Icon icon="settings" classes="mr-1" />
350                           {i18n.t("settings")}
351                         </NavLink>
352                       </li>
353                       <li>
354                         <hr className="dropdown-divider" />
355                       </li>
356                       <li>
357                         <button
358                           className="dropdown-item btn btn-link px-2"
359                           onClick={linkEvent(this, handleLogOut)}
360                         >
361                           <Icon icon="log-out" classes="mr-1" />
362                           {i18n.t("logout")}
363                         </button>
364                       </li>
365                     </ul>
366                   </div>
367                 )}
368               </>
369             ) : (
370               <>
371                 <li className="nav-item">
372                   <NavLink
373                     to="/login"
374                     className="nav-link"
375                     title={i18n.t("login")}
376                     onMouseUp={linkEvent(this, handleCollapseClick)}
377                   >
378                     {i18n.t("login")}
379                   </NavLink>
380                 </li>
381                 <li className="nav-item">
382                   <NavLink
383                     to="/signup"
384                     className="nav-link"
385                     title={i18n.t("sign_up")}
386                     onMouseUp={linkEvent(this, handleCollapseClick)}
387                   >
388                     {i18n.t("sign_up")}
389                   </NavLink>
390                 </li>
391               </>
392             )}
393           </ul>
394         </div>
395       </nav>
396     );
397   }
398
399   handleOutsideMenuClick(event: MouseEvent) {
400     if (!this.mobileMenuRef.current?.contains(event.target as Node | null)) {
401       handleCollapseClick(this);
402     }
403   }
404
405   get moderatesSomething(): boolean {
406     const mods = UserService.Instance.myUserInfo?.moderates;
407     const moderatesS = (mods && mods.length > 0) || false;
408     return amAdmin() || moderatesS;
409   }
410
411   fetchUnreads() {
412     poll(async () => {
413       if (window.document.visibilityState !== "hidden") {
414         const auth = myAuth();
415         if (auth) {
416           this.setState({
417             unreadInboxCountRes: await HttpService.client.getUnreadCount({
418               auth,
419             }),
420           });
421
422           if (this.moderatesSomething) {
423             this.setState({
424               unreadReportCountRes: await HttpService.client.getReportCount({
425                 auth,
426               }),
427             });
428           }
429
430           if (amAdmin()) {
431             this.setState({
432               unreadApplicationCountRes:
433                 await HttpService.client.getUnreadRegistrationApplicationCount({
434                   auth,
435                 }),
436             });
437           }
438         }
439       }
440     }, updateUnreadCountsInterval);
441   }
442
443   get unreadInboxCount(): number {
444     if (this.state.unreadInboxCountRes.state == "success") {
445       const data = this.state.unreadInboxCountRes.data;
446       return data.replies + data.mentions + data.private_messages;
447     } else {
448       return 0;
449     }
450   }
451
452   get unreadReportCount(): number {
453     if (this.state.unreadReportCountRes.state == "success") {
454       const data = this.state.unreadReportCountRes.data;
455       return (
456         data.post_reports +
457         data.comment_reports +
458         (data.private_message_reports ?? 0)
459       );
460     } else {
461       return 0;
462     }
463   }
464
465   get unreadApplicationCount(): number {
466     if (this.state.unreadApplicationCountRes.state == "success") {
467       const data = this.state.unreadApplicationCountRes.data;
468       return data.registration_applications;
469     } else {
470       return 0;
471     }
472   }
473
474   get currentLocation() {
475     return this.context.router.history.location.pathname;
476   }
477
478   requestNotificationPermission() {
479     if (UserService.Instance.myUserInfo) {
480       document.addEventListener("DOMContentLoaded", function () {
481         if (!Notification) {
482           toast(i18n.t("notifications_error"), "danger");
483           return;
484         }
485
486         if (Notification.permission !== "granted")
487           Notification.requestPermission();
488       });
489     }
490   }
491 }