]> Untitled Git - lemmy-ui.git/blob - src/shared/components/app/navbar.tsx
448e92cbb05cd9f5dee8949a1637efb43c48f0d8
[lemmy-ui.git] / src / shared / components / app / navbar.tsx
1 import { Component, linkEvent } from "inferno";
2 import { NavLink } from "inferno-router";
3 import {
4   CommentResponse,
5   GetReportCount,
6   GetReportCountResponse,
7   GetSiteResponse,
8   GetUnreadCount,
9   GetUnreadCountResponse,
10   GetUnreadRegistrationApplicationCount,
11   GetUnreadRegistrationApplicationCountResponse,
12   PrivateMessageResponse,
13   UserOperation,
14   wsJsonToRes,
15   wsUserOp,
16 } from "lemmy-js-client";
17 import { Subscription } from "rxjs";
18 import { i18n } from "../../i18next";
19 import { UserService, WebSocketService } from "../../services";
20 import {
21   amAdmin,
22   canCreateCommunity,
23   donateLemmyUrl,
24   isBrowser,
25   myAuth,
26   notifyComment,
27   notifyPrivateMessage,
28   numToSI,
29   showAvatars,
30   toast,
31   wsClient,
32   wsSubscribe,
33 } from "../../utils";
34 import { Icon } from "../common/icon";
35 import { PictrsImage } from "../common/pictrs-image";
36
37 interface NavbarProps {
38   siteRes: GetSiteResponse;
39 }
40
41 interface NavbarState {
42   expanded: boolean;
43   unreadInboxCount: bigint;
44   unreadReportCount: bigint;
45   unreadApplicationCount: bigint;
46   showDropdown: boolean;
47   onSiteBanner?(url: string): any;
48 }
49
50 export class Navbar extends Component<NavbarProps, NavbarState> {
51   private wsSub: Subscription;
52   private userSub: Subscription;
53   private unreadInboxCountSub: Subscription;
54   private unreadReportCountSub: Subscription;
55   private unreadApplicationCountSub: Subscription;
56   state: NavbarState = {
57     unreadInboxCount: 0n,
58     unreadReportCount: 0n,
59     unreadApplicationCount: 0n,
60     expanded: false,
61     showDropdown: false,
62   };
63   subscription: any;
64
65   constructor(props: any, context: any) {
66     super(props, context);
67
68     this.parseMessage = this.parseMessage.bind(this);
69     this.subscription = wsSubscribe(this.parseMessage);
70   }
71
72   componentDidMount() {
73     // Subscribe to jwt changes
74     if (isBrowser()) {
75       // On the first load, check the unreads
76       let auth = myAuth(false);
77       if (auth && UserService.Instance.myUserInfo) {
78         this.requestNotificationPermission();
79         WebSocketService.Instance.send(
80           wsClient.userJoin({
81             auth,
82           })
83         );
84
85         this.fetchUnreads();
86       }
87
88       this.requestNotificationPermission();
89
90       // Subscribe to unread count changes
91       this.unreadInboxCountSub =
92         UserService.Instance.unreadInboxCountSub.subscribe(res => {
93           this.setState({ unreadInboxCount: res });
94         });
95       // Subscribe to unread report count changes
96       this.unreadReportCountSub =
97         UserService.Instance.unreadReportCountSub.subscribe(res => {
98           this.setState({ unreadReportCount: res });
99         });
100       // Subscribe to unread application count
101       this.unreadApplicationCountSub =
102         UserService.Instance.unreadApplicationCountSub.subscribe(res => {
103           this.setState({ unreadApplicationCount: res });
104         });
105     }
106   }
107
108   componentWillUnmount() {
109     this.wsSub.unsubscribe();
110     this.userSub.unsubscribe();
111     this.unreadInboxCountSub.unsubscribe();
112     this.unreadReportCountSub.unsubscribe();
113     this.unreadApplicationCountSub.unsubscribe();
114   }
115
116   render() {
117     return this.navbar();
118   }
119
120   // TODO class active corresponding to current page
121   navbar() {
122     let siteView = this.props.siteRes.site_view;
123     let person = UserService.Instance.myUserInfo?.local_user_view.person;
124     return (
125       <nav className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3">
126         <div className="container-lg">
127           <NavLink
128             to="/"
129             onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
130             title={siteView.site.description ?? siteView.site.name}
131             className="d-flex align-items-center navbar-brand mr-md-3"
132           >
133             {siteView.site.icon && showAvatars() && (
134               <PictrsImage src={siteView.site.icon} icon />
135             )}
136             {siteView.site.name}
137           </NavLink>
138           {UserService.Instance.myUserInfo && (
139             <>
140               <ul className="navbar-nav ml-auto">
141                 <li className="nav-item">
142                   <NavLink
143                     to="/inbox"
144                     className="p-1 navbar-toggler nav-link border-0"
145                     onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
146                     title={i18n.t("unread_messages", {
147                       count: Number(this.state.unreadInboxCount),
148                       formattedCount: numToSI(this.state.unreadInboxCount),
149                     })}
150                   >
151                     <Icon icon="bell" />
152                     {this.state.unreadInboxCount > 0 && (
153                       <span className="mx-1 badge badge-light">
154                         {numToSI(this.state.unreadInboxCount)}
155                       </span>
156                     )}
157                   </NavLink>
158                 </li>
159               </ul>
160               {this.moderatesSomething && (
161                 <ul className="navbar-nav ml-1">
162                   <li className="nav-item">
163                     <NavLink
164                       to="/reports"
165                       className="p-1 navbar-toggler nav-link border-0"
166                       onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
167                       title={i18n.t("unread_reports", {
168                         count: Number(this.state.unreadReportCount),
169                         formattedCount: numToSI(this.state.unreadReportCount),
170                       })}
171                     >
172                       <Icon icon="shield" />
173                       {this.state.unreadReportCount > 0 && (
174                         <span className="mx-1 badge badge-light">
175                           {numToSI(this.state.unreadReportCount)}
176                         </span>
177                       )}
178                     </NavLink>
179                   </li>
180                 </ul>
181               )}
182               {amAdmin() && (
183                 <ul className="navbar-nav ml-1">
184                   <li className="nav-item">
185                     <NavLink
186                       to="/registration_applications"
187                       className="p-1 navbar-toggler nav-link border-0"
188                       onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
189                       title={i18n.t("unread_registration_applications", {
190                         count: Number(this.state.unreadApplicationCount),
191                         formattedCount: numToSI(
192                           this.state.unreadApplicationCount
193                         ),
194                       })}
195                     >
196                       <Icon icon="clipboard" />
197                       {this.state.unreadApplicationCount > 0 && (
198                         <span className="mx-1 badge badge-light">
199                           {numToSI(this.state.unreadApplicationCount)}
200                         </span>
201                       )}
202                     </NavLink>
203                   </li>
204                 </ul>
205               )}
206             </>
207           )}
208           <button
209             className="navbar-toggler border-0 p-1"
210             type="button"
211             aria-label="menu"
212             onClick={linkEvent(this, this.handleToggleExpandNavbar)}
213             data-tippy-content={i18n.t("expand_here")}
214           >
215             <Icon icon="menu" />
216           </button>
217           <div
218             className={`${!this.state.expanded && "collapse"} navbar-collapse`}
219           >
220             <ul className="navbar-nav my-2 mr-auto">
221               <li className="nav-item">
222                 <NavLink
223                   to="/communities"
224                   className="nav-link"
225                   onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
226                   title={i18n.t("communities")}
227                 >
228                   {i18n.t("communities")}
229                 </NavLink>
230               </li>
231               <li className="nav-item">
232                 {/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
233                 <NavLink
234                   to={{
235                     pathname: "/create_post",
236                     search: "",
237                     hash: "",
238                     key: "",
239                     state: { prevPath: this.currentLocation },
240                   }}
241                   className="nav-link"
242                   onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
243                   title={i18n.t("create_post")}
244                 >
245                   {i18n.t("create_post")}
246                 </NavLink>
247               </li>
248               {canCreateCommunity(this.props.siteRes) && (
249                 <li className="nav-item">
250                   <NavLink
251                     to="/create_community"
252                     className="nav-link"
253                     onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
254                     title={i18n.t("create_community")}
255                   >
256                     {i18n.t("create_community")}
257                   </NavLink>
258                 </li>
259               )}
260               <li className="nav-item">
261                 <a
262                   className="nav-link"
263                   title={i18n.t("support_lemmy")}
264                   href={donateLemmyUrl}
265                 >
266                   <Icon icon="heart" classes="small" />
267                 </a>
268               </li>
269             </ul>
270             <ul className="navbar-nav my-2">
271               {amAdmin() && (
272                 <li className="nav-item">
273                   <NavLink
274                     to="/admin"
275                     className="nav-link"
276                     onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
277                     title={i18n.t("admin_settings")}
278                   >
279                     <Icon icon="settings" />
280                   </NavLink>
281                 </li>
282               )}
283             </ul>
284             {!this.context.router.history.location.pathname.match(
285               /^\/search/
286             ) && (
287               <ul className="navbar-nav">
288                 <li className="nav-item">
289                   <NavLink
290                     to="/search"
291                     className="nav-link"
292                     title={i18n.t("search")}
293                   >
294                     <Icon icon="search" />
295                   </NavLink>
296                 </li>
297               </ul>
298             )}
299             {UserService.Instance.myUserInfo ? (
300               <>
301                 <ul className="navbar-nav my-2">
302                   <li className="nav-item">
303                     <NavLink
304                       className="nav-link"
305                       to="/inbox"
306                       onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
307                       title={i18n.t("unread_messages", {
308                         count: Number(this.state.unreadInboxCount),
309                         formattedCount: numToSI(this.state.unreadInboxCount),
310                       })}
311                     >
312                       <Icon icon="bell" />
313                       {this.state.unreadInboxCount > 0 && (
314                         <span className="ml-1 badge badge-light">
315                           {numToSI(this.state.unreadInboxCount)}
316                         </span>
317                       )}
318                     </NavLink>
319                   </li>
320                 </ul>
321                 {this.moderatesSomething && (
322                   <ul className="navbar-nav my-2">
323                     <li className="nav-item">
324                       <NavLink
325                         className="nav-link"
326                         to="/reports"
327                         onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
328                         title={i18n.t("unread_reports", {
329                           count: Number(this.state.unreadReportCount),
330                           formattedCount: numToSI(this.state.unreadReportCount),
331                         })}
332                       >
333                         <Icon icon="shield" />
334                         {this.state.unreadReportCount > 0 && (
335                           <span className="ml-1 badge badge-light">
336                             {numToSI(this.state.unreadReportCount)}
337                           </span>
338                         )}
339                       </NavLink>
340                     </li>
341                   </ul>
342                 )}
343                 {amAdmin() && (
344                   <ul className="navbar-nav my-2">
345                     <li className="nav-item">
346                       <NavLink
347                         to="/registration_applications"
348                         className="nav-link"
349                         onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
350                         title={i18n.t("unread_registration_applications", {
351                           count: Number(this.state.unreadApplicationCount),
352                           formattedCount: numToSI(
353                             this.state.unreadApplicationCount
354                           ),
355                         })}
356                       >
357                         <Icon icon="clipboard" />
358                         {this.state.unreadApplicationCount > 0 && (
359                           <span className="mx-1 badge badge-light">
360                             {numToSI(this.state.unreadApplicationCount)}
361                           </span>
362                         )}
363                       </NavLink>
364                     </li>
365                   </ul>
366                 )}
367                 {person && (
368                   <ul className="navbar-nav">
369                     <li className="nav-item dropdown">
370                       <button
371                         className="nav-link btn btn-link dropdown-toggle"
372                         onClick={linkEvent(this, this.handleToggleDropdown)}
373                         id="navbarDropdown"
374                         role="button"
375                         aria-expanded="false"
376                       >
377                         <span>
378                           {showAvatars() && person.avatar && (
379                             <PictrsImage src={person.avatar} icon />
380                           )}
381                           {person.display_name ?? person.name}
382                         </span>
383                       </button>
384                       {this.state.showDropdown && (
385                         <div
386                           className="dropdown-content"
387                           onMouseLeave={linkEvent(
388                             this,
389                             this.handleToggleDropdown
390                           )}
391                         >
392                           <li className="nav-item">
393                             <NavLink
394                               to={`/u/${person.name}`}
395                               className="nav-link"
396                               title={i18n.t("profile")}
397                             >
398                               <Icon icon="user" classes="mr-1" />
399                               {i18n.t("profile")}
400                             </NavLink>
401                           </li>
402                           <li className="nav-item">
403                             <NavLink
404                               to="/settings"
405                               className="nav-link"
406                               title={i18n.t("settings")}
407                             >
408                               <Icon icon="settings" classes="mr-1" />
409                               {i18n.t("settings")}
410                             </NavLink>
411                           </li>
412                           <li>
413                             <hr className="dropdown-divider" />
414                           </li>
415                           <li className="nav-item">
416                             <button
417                               className="nav-link btn btn-link"
418                               onClick={linkEvent(this, this.handleLogoutClick)}
419                               title="test"
420                             >
421                               <Icon icon="log-out" classes="mr-1" />
422                               {i18n.t("logout")}
423                             </button>
424                           </li>
425                         </div>
426                       )}
427                     </li>
428                   </ul>
429                 )}
430               </>
431             ) : (
432               <ul className="navbar-nav my-2">
433                 <li className="nav-item">
434                   <NavLink
435                     to="/login"
436                     className="nav-link"
437                     onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
438                     title={i18n.t("login")}
439                   >
440                     {i18n.t("login")}
441                   </NavLink>
442                 </li>
443                 <li className="nav-item">
444                   <NavLink
445                     to="/signup"
446                     className="nav-link"
447                     onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
448                     title={i18n.t("sign_up")}
449                   >
450                     {i18n.t("sign_up")}
451                   </NavLink>
452                 </li>
453               </ul>
454             )}
455           </div>
456         </div>
457       </nav>
458     );
459   }
460
461   get moderatesSomething(): boolean {
462     let mods = UserService.Instance.myUserInfo?.moderates;
463     let moderatesS = (mods && mods.length > 0) || false;
464     return amAdmin() || moderatesS;
465   }
466
467   handleToggleExpandNavbar(i: Navbar) {
468     i.setState({ expanded: !i.state.expanded });
469   }
470
471   handleHideExpandNavbar(i: Navbar) {
472     i.setState({ expanded: false, showDropdown: false });
473   }
474
475   handleLogoutClick(i: Navbar) {
476     i.setState({ showDropdown: false, expanded: false });
477     UserService.Instance.logout();
478   }
479
480   handleToggleDropdown(i: Navbar) {
481     i.setState({ showDropdown: !i.state.showDropdown });
482   }
483
484   parseMessage(msg: any) {
485     let op = wsUserOp(msg);
486     console.log(msg);
487     if (msg.error) {
488       if (msg.error == "not_logged_in") {
489         UserService.Instance.logout();
490       }
491       return;
492     } else if (msg.reconnect) {
493       console.log(i18n.t("websocket_reconnected"));
494       let auth = myAuth(false);
495       if (UserService.Instance.myUserInfo && auth) {
496         WebSocketService.Instance.send(
497           wsClient.userJoin({
498             auth,
499           })
500         );
501         this.fetchUnreads();
502       }
503     } else if (op == UserOperation.GetUnreadCount) {
504       let data = wsJsonToRes<GetUnreadCountResponse>(msg);
505       this.setState({
506         unreadInboxCount: data.replies + data.mentions + data.private_messages,
507       });
508       this.sendUnreadCount();
509     } else if (op == UserOperation.GetReportCount) {
510       let data = wsJsonToRes<GetReportCountResponse>(msg);
511       this.setState({
512         unreadReportCount:
513           data.post_reports +
514           data.comment_reports +
515           (data.private_message_reports ?? 0n),
516       });
517       this.sendReportUnread();
518     } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
519       let data =
520         wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(msg);
521       this.setState({ unreadApplicationCount: data.registration_applications });
522       this.sendApplicationUnread();
523     } else if (op == UserOperation.CreateComment) {
524       let data = wsJsonToRes<CommentResponse>(msg);
525       let mui = UserService.Instance.myUserInfo;
526       if (
527         mui &&
528         data.recipient_ids.includes(mui.local_user_view.local_user.id)
529       ) {
530         this.setState({
531           unreadInboxCount: this.state.unreadInboxCount + 1n,
532         });
533         this.sendUnreadCount();
534         notifyComment(data.comment_view, this.context.router);
535       }
536     } else if (op == UserOperation.CreatePrivateMessage) {
537       let data = wsJsonToRes<PrivateMessageResponse>(msg);
538
539       if (
540         data.private_message_view.recipient.id ==
541         UserService.Instance.myUserInfo?.local_user_view.person.id
542       ) {
543         this.setState({
544           unreadInboxCount: this.state.unreadInboxCount + 1n,
545         });
546         this.sendUnreadCount();
547         notifyPrivateMessage(data.private_message_view, this.context.router);
548       }
549     }
550   }
551
552   fetchUnreads() {
553     console.log("Fetching inbox unreads...");
554
555     let auth = myAuth();
556     if (auth) {
557       let unreadForm: GetUnreadCount = {
558         auth,
559       };
560       WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
561
562       console.log("Fetching reports...");
563
564       let reportCountForm: GetReportCount = {
565         auth,
566       };
567       WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
568
569       if (amAdmin()) {
570         console.log("Fetching applications...");
571
572         let applicationCountForm: GetUnreadRegistrationApplicationCount = {
573           auth,
574         };
575         WebSocketService.Instance.send(
576           wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
577         );
578       }
579     }
580   }
581
582   get currentLocation() {
583     return this.context.router.history.location.pathname;
584   }
585
586   sendUnreadCount() {
587     UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
588   }
589
590   sendReportUnread() {
591     UserService.Instance.unreadReportCountSub.next(
592       this.state.unreadReportCount
593     );
594   }
595
596   sendApplicationUnread() {
597     UserService.Instance.unreadApplicationCountSub.next(
598       this.state.unreadApplicationCount
599     );
600   }
601
602   requestNotificationPermission() {
603     if (UserService.Instance.myUserInfo) {
604       document.addEventListener("DOMContentLoaded", function () {
605         if (!Notification) {
606           toast(i18n.t("notifications_error"), "danger");
607           return;
608         }
609
610         if (Notification.permission !== "granted")
611           Notification.requestPermission();
612       });
613     }
614   }
615 }