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