]> Untitled Git - lemmy-ui.git/blob - src/shared/components/app/navbar.tsx
Private instances (#523)
[lemmy-ui.git] / src / shared / components / app / navbar.tsx
1 import { Component, createRef, linkEvent, RefObject } 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 } from "lemmy-js-client";
15 import { Subscription } from "rxjs";
16 import { i18n } from "../../i18next";
17 import { UserService, WebSocketService } from "../../services";
18 import {
19   authField,
20   donateLemmyUrl,
21   getLanguage,
22   isBrowser,
23   notifyComment,
24   notifyPrivateMessage,
25   numToSI,
26   setTheme,
27   showAvatars,
28   toast,
29   wsClient,
30   wsJsonToRes,
31   wsSubscribe,
32   wsUserOp,
33 } from "../../utils";
34 import { Icon } from "../common/icon";
35 import { PictrsImage } from "../common/pictrs-image";
36
37 interface NavbarProps {
38   site_res: GetSiteResponse;
39 }
40
41 interface NavbarState {
42   isLoggedIn: boolean;
43   expanded: boolean;
44   unreadInboxCount: number;
45   unreadReportCount: number;
46   unreadApplicationCount: number;
47   searchParam: string;
48   toggleSearch: boolean;
49   showDropdown: boolean;
50   onSiteBanner?(url: string): any;
51 }
52
53 export class Navbar extends Component<NavbarProps, NavbarState> {
54   private wsSub: Subscription;
55   private userSub: Subscription;
56   private unreadInboxCountSub: Subscription;
57   private unreadReportCountSub: Subscription;
58   private unreadApplicationCountSub: Subscription;
59   private searchTextField: RefObject<HTMLInputElement>;
60   emptyState: NavbarState = {
61     isLoggedIn: !!this.props.site_res.my_user,
62     unreadInboxCount: 0,
63     unreadReportCount: 0,
64     unreadApplicationCount: 0,
65     expanded: false,
66     searchParam: "",
67     toggleSearch: false,
68     showDropdown: false,
69   };
70   subscription: any;
71
72   constructor(props: any, context: any) {
73     super(props, context);
74     this.state = this.emptyState;
75
76     this.parseMessage = this.parseMessage.bind(this);
77     this.subscription = wsSubscribe(this.parseMessage);
78   }
79
80   componentDidMount() {
81     // Subscribe to jwt changes
82     if (isBrowser()) {
83       this.searchTextField = createRef();
84       console.log(`isLoggedIn = ${this.state.isLoggedIn}`);
85
86       // On the first load, check the unreads
87       if (this.state.isLoggedIn == false) {
88         // setTheme(data.my_user.theme, true);
89         // i18n.changeLanguage(getLanguage());
90         // i18n.changeLanguage('de');
91       } else {
92         this.requestNotificationPermission();
93         WebSocketService.Instance.send(
94           wsClient.userJoin({
95             auth: authField(),
96           })
97         );
98         this.fetchUnreads();
99       }
100
101       this.userSub = UserService.Instance.jwtSub.subscribe(res => {
102         // A login
103         if (res !== undefined) {
104           this.requestNotificationPermission();
105           WebSocketService.Instance.send(
106             wsClient.getSite({ auth: authField() })
107           );
108         } else {
109           this.setState({ isLoggedIn: false });
110         }
111       });
112
113       // Subscribe to unread count changes
114       this.unreadInboxCountSub =
115         UserService.Instance.unreadInboxCountSub.subscribe(res => {
116           this.setState({ unreadInboxCount: res });
117         });
118       // Subscribe to unread report count changes
119       this.unreadReportCountSub =
120         UserService.Instance.unreadReportCountSub.subscribe(res => {
121           this.setState({ unreadReportCount: res });
122         });
123       // Subscribe to unread application count
124       this.unreadApplicationCountSub =
125         UserService.Instance.unreadApplicationCountSub.subscribe(res => {
126           this.setState({ unreadApplicationCount: res });
127         });
128     }
129   }
130
131   componentWillUnmount() {
132     this.wsSub.unsubscribe();
133     this.userSub.unsubscribe();
134     this.unreadInboxCountSub.unsubscribe();
135     this.unreadReportCountSub.unsubscribe();
136     this.unreadApplicationCountSub.unsubscribe();
137   }
138
139   updateUrl() {
140     const searchParam = this.state.searchParam;
141     this.setState({ searchParam: "" });
142     this.setState({ toggleSearch: false });
143     this.setState({ showDropdown: false, expanded: false });
144     if (searchParam === "") {
145       this.context.router.history.push(`/search/`);
146     } else {
147       const searchParamEncoded = encodeURIComponent(searchParam);
148       this.context.router.history.push(
149         `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
150       );
151     }
152   }
153
154   render() {
155     return this.navbar();
156   }
157
158   // TODO class active corresponding to current page
159   navbar() {
160     let myUserInfo =
161       UserService.Instance.myUserInfo || this.props.site_res.my_user;
162     let person = myUserInfo?.local_user_view.person;
163     return (
164       <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
165         <div class="container">
166           {this.props.site_res.site_view && (
167             <NavLink
168               to="/"
169               onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
170               title={
171                 this.props.site_res.site_view.site.description ||
172                 this.props.site_res.site_view.site.name
173               }
174               className="d-flex align-items-center navbar-brand mr-md-3"
175             >
176               {this.props.site_res.site_view.site.icon && showAvatars() && (
177                 <PictrsImage
178                   src={this.props.site_res.site_view.site.icon}
179                   icon
180                 />
181               )}
182               {this.props.site_res.site_view.site.name}
183             </NavLink>
184           )}
185           {this.state.isLoggedIn && (
186             <>
187               <ul class="navbar-nav ml-auto">
188                 <li className="nav-item">
189                   <NavLink
190                     to="/inbox"
191                     className="p-1 navbar-toggler nav-link border-0"
192                     onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
193                     title={i18n.t("unread_messages", {
194                       count: this.state.unreadInboxCount,
195                       formattedCount: numToSI(this.state.unreadInboxCount),
196                     })}
197                   >
198                     <Icon icon="bell" />
199                     {this.state.unreadInboxCount > 0 && (
200                       <span class="mx-1 badge badge-light">
201                         {numToSI(this.state.unreadInboxCount)}
202                       </span>
203                     )}
204                   </NavLink>
205                 </li>
206               </ul>
207               {UserService.Instance.myUserInfo?.moderates.length > 0 && (
208                 <ul class="navbar-nav ml-1">
209                   <li className="nav-item">
210                     <NavLink
211                       to="/reports"
212                       className="p-1 navbar-toggler nav-link border-0"
213                       onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
214                       title={i18n.t("unread_reports", {
215                         count: this.state.unreadReportCount,
216                         formattedCount: numToSI(this.state.unreadReportCount),
217                       })}
218                     >
219                       <Icon icon="shield" />
220                       {this.state.unreadReportCount > 0 && (
221                         <span class="mx-1 badge badge-light">
222                           {numToSI(this.state.unreadReportCount)}
223                         </span>
224                       )}
225                     </NavLink>
226                   </li>
227                 </ul>
228               )}
229               {UserService.Instance.myUserInfo?.local_user_view.person
230                 .admin && (
231                 <ul class="navbar-nav ml-1">
232                   <li className="nav-item">
233                     <NavLink
234                       to="/registration_applications"
235                       className="p-1 navbar-toggler nav-link border-0"
236                       onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
237                       title={i18n.t("unread_registration_applications", {
238                         count: this.state.unreadApplicationCount,
239                         formattedCount: numToSI(
240                           this.state.unreadApplicationCount
241                         ),
242                       })}
243                     >
244                       <Icon icon="clipboard" />
245                       {this.state.unreadApplicationCount > 0 && (
246                         <span class="mx-1 badge badge-light">
247                           {numToSI(this.state.unreadApplicationCount)}
248                         </span>
249                       )}
250                     </NavLink>
251                   </li>
252                 </ul>
253               )}
254             </>
255           )}
256           <button
257             class="navbar-toggler border-0 p-1"
258             type="button"
259             aria-label="menu"
260             onClick={linkEvent(this, this.handleToggleExpandNavbar)}
261             data-tippy-content={i18n.t("expand_here")}
262           >
263             <Icon icon="menu" />
264           </button>
265           <div
266             className={`${!this.state.expanded && "collapse"} navbar-collapse`}
267           >
268             <ul class="navbar-nav my-2 mr-auto">
269               <li class="nav-item">
270                 <NavLink
271                   to="/communities"
272                   className="nav-link"
273                   onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
274                   title={i18n.t("communities")}
275                 >
276                   {i18n.t("communities")}
277                 </NavLink>
278               </li>
279               <li class="nav-item">
280                 <NavLink
281                   to={{
282                     pathname: "/create_post",
283                     prevPath: this.currentLocation,
284                   }}
285                   className="nav-link"
286                   onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
287                   title={i18n.t("create_post")}
288                 >
289                   {i18n.t("create_post")}
290                 </NavLink>
291               </li>
292               {this.canCreateCommunity && (
293                 <li class="nav-item">
294                   <NavLink
295                     to="/create_community"
296                     className="nav-link"
297                     onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
298                     title={i18n.t("create_community")}
299                   >
300                     {i18n.t("create_community")}
301                   </NavLink>
302                 </li>
303               )}
304               <li class="nav-item">
305                 <a
306                   className="nav-link"
307                   title={i18n.t("support_lemmy")}
308                   href={donateLemmyUrl}
309                 >
310                   <Icon icon="heart" classes="small" />
311                 </a>
312               </li>
313             </ul>
314             <ul class="navbar-nav my-2">
315               {this.canAdmin && (
316                 <li className="nav-item">
317                   <NavLink
318                     to="/admin"
319                     className="nav-link"
320                     onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
321                     title={i18n.t("admin_settings")}
322                   >
323                     <Icon icon="settings" />
324                   </NavLink>
325                 </li>
326               )}
327             </ul>
328             {!this.context.router.history.location.pathname.match(
329               /^\/search/
330             ) && (
331               <form
332                 class="form-inline mr-2"
333                 onSubmit={linkEvent(this, this.handleSearchSubmit)}
334               >
335                 <input
336                   id="search-input"
337                   class={`form-control mr-0 search-input ${
338                     this.state.toggleSearch ? "show-input" : "hide-input"
339                   }`}
340                   onInput={linkEvent(this, this.handleSearchParam)}
341                   value={this.state.searchParam}
342                   ref={this.searchTextField}
343                   type="text"
344                   placeholder={i18n.t("search")}
345                   onBlur={linkEvent(this, this.handleSearchBlur)}
346                 ></input>
347                 <label class="sr-only" htmlFor="search-input">
348                   {i18n.t("search")}
349                 </label>
350                 <button
351                   name="search-btn"
352                   onClick={linkEvent(this, this.handleSearchBtn)}
353                   class="px-1 btn btn-link"
354                   style="color: var(--gray)"
355                   aria-label={i18n.t("search")}
356                 >
357                   <Icon icon="search" />
358                 </button>
359               </form>
360             )}
361             {this.state.isLoggedIn ? (
362               <>
363                 <ul class="navbar-nav my-2">
364                   <li className="nav-item">
365                     <NavLink
366                       className="nav-link"
367                       to="/inbox"
368                       onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
369                       title={i18n.t("unread_messages", {
370                         count: this.state.unreadInboxCount,
371                         formattedCount: numToSI(this.state.unreadInboxCount),
372                       })}
373                     >
374                       <Icon icon="bell" />
375                       {this.state.unreadInboxCount > 0 && (
376                         <span class="ml-1 badge badge-light">
377                           {numToSI(this.state.unreadInboxCount)}
378                         </span>
379                       )}
380                     </NavLink>
381                   </li>
382                 </ul>
383                 {UserService.Instance.myUserInfo?.moderates.length > 0 && (
384                   <ul class="navbar-nav my-2">
385                     <li className="nav-item">
386                       <NavLink
387                         className="nav-link"
388                         to="/reports"
389                         onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
390                         title={i18n.t("unread_reports", {
391                           count: this.state.unreadReportCount,
392                           formattedCount: numToSI(this.state.unreadReportCount),
393                         })}
394                       >
395                         <Icon icon="shield" />
396                         {this.state.unreadReportCount > 0 && (
397                           <span class="ml-1 badge badge-light">
398                             {numToSI(this.state.unreadReportCount)}
399                           </span>
400                         )}
401                       </NavLink>
402                     </li>
403                   </ul>
404                 )}
405                 {UserService.Instance.myUserInfo?.local_user_view.person
406                   .admin && (
407                   <ul class="navbar-nav my-2">
408                     <li className="nav-item">
409                       <NavLink
410                         to="/registration_applications"
411                         className="nav-link"
412                         onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
413                         title={i18n.t("unread_registration_applications", {
414                           count: this.state.unreadApplicationCount,
415                           formattedCount: numToSI(
416                             this.state.unreadApplicationCount
417                           ),
418                         })}
419                       >
420                         <Icon icon="clipboard" />
421                         {this.state.unreadApplicationCount > 0 && (
422                           <span class="mx-1 badge badge-light">
423                             {numToSI(this.state.unreadApplicationCount)}
424                           </span>
425                         )}
426                       </NavLink>
427                     </li>
428                   </ul>
429                 )}
430                 <ul class="navbar-nav">
431                   <li class="nav-item dropdown">
432                     <button
433                       class="nav-link btn btn-link dropdown-toggle"
434                       onClick={linkEvent(this, this.handleToggleDropdown)}
435                       id="navbarDropdown"
436                       role="button"
437                       aria-expanded="false"
438                     >
439                       <span>
440                         {person.avatar && showAvatars() && (
441                           <PictrsImage src={person.avatar} icon />
442                         )}
443                         {person.display_name
444                           ? person.display_name
445                           : person.name}
446                       </span>
447                     </button>
448                     {this.state.showDropdown && (
449                       <div
450                         class="dropdown-content"
451                         onMouseLeave={linkEvent(
452                           this,
453                           this.handleToggleDropdown
454                         )}
455                       >
456                         <li className="nav-item">
457                           <NavLink
458                             to={`/u/${UserService.Instance.myUserInfo.local_user_view.person.name}`}
459                             className="nav-link"
460                             title={i18n.t("profile")}
461                           >
462                             <Icon icon="user" classes="mr-1" />
463                             {i18n.t("profile")}
464                           </NavLink>
465                         </li>
466                         <li className="nav-item">
467                           <NavLink
468                             to="/settings"
469                             className="nav-link"
470                             title={i18n.t("settings")}
471                           >
472                             <Icon icon="settings" classes="mr-1" />
473                             {i18n.t("settings")}
474                           </NavLink>
475                         </li>
476                         <li>
477                           <hr class="dropdown-divider" />
478                         </li>
479                         <li className="nav-item">
480                           <button
481                             className="nav-link btn btn-link"
482                             onClick={linkEvent(this, this.handleLogoutClick)}
483                             title="test"
484                           >
485                             <Icon icon="log-out" classes="mr-1" />
486                             {i18n.t("logout")}
487                           </button>
488                         </li>
489                       </div>
490                     )}
491                   </li>
492                 </ul>
493               </>
494             ) : (
495               <ul class="navbar-nav my-2">
496                 <li className="nav-item">
497                   <NavLink
498                     to="/login"
499                     className="nav-link"
500                     onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
501                     title={i18n.t("login")}
502                   >
503                     {i18n.t("login")}
504                   </NavLink>
505                 </li>
506                 <li className="nav-item">
507                   <NavLink
508                     to="/signup"
509                     className="nav-link"
510                     onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
511                     title={i18n.t("sign_up")}
512                   >
513                     {i18n.t("sign_up")}
514                   </NavLink>
515                 </li>
516               </ul>
517             )}
518           </div>
519         </div>
520       </nav>
521     );
522   }
523
524   handleToggleExpandNavbar(i: Navbar) {
525     i.state.expanded = !i.state.expanded;
526     i.setState(i.state);
527   }
528
529   handleHideExpandNavbar(i: Navbar) {
530     i.setState({ expanded: false, showDropdown: false });
531   }
532
533   handleSearchParam(i: Navbar, event: any) {
534     i.state.searchParam = event.target.value;
535     i.setState(i.state);
536   }
537
538   handleSearchSubmit(i: Navbar, event: any) {
539     event.preventDefault();
540     i.updateUrl();
541   }
542
543   handleSearchBtn(i: Navbar, event: any) {
544     event.preventDefault();
545     i.setState({ toggleSearch: true });
546
547     i.searchTextField.current.focus();
548     const offsetWidth = i.searchTextField.current.offsetWidth;
549     if (i.state.searchParam && offsetWidth > 100) {
550       i.updateUrl();
551     }
552   }
553
554   handleSearchBlur(i: Navbar, event: any) {
555     if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
556       i.state.toggleSearch = false;
557       i.setState(i.state);
558     }
559   }
560
561   handleLogoutClick(i: Navbar) {
562     i.setState({ showDropdown: false, expanded: false });
563     UserService.Instance.logout();
564     window.location.href = "/";
565     location.reload();
566   }
567
568   handleToggleDropdown(i: Navbar) {
569     i.state.showDropdown = !i.state.showDropdown;
570     i.setState(i.state);
571   }
572
573   parseMessage(msg: any) {
574     let op = wsUserOp(msg);
575     console.log(msg);
576     if (msg.error) {
577       if (msg.error == "not_logged_in") {
578         UserService.Instance.logout();
579         location.reload();
580       }
581       return;
582     } else if (msg.reconnect) {
583       console.log(i18n.t("websocket_reconnected"));
584       WebSocketService.Instance.send(
585         wsClient.userJoin({
586           auth: authField(),
587         })
588       );
589       this.fetchUnreads();
590     } else if (op == UserOperation.GetUnreadCount) {
591       let data = wsJsonToRes<GetUnreadCountResponse>(msg).data;
592       this.state.unreadInboxCount =
593         data.replies + data.mentions + data.private_messages;
594       this.setState(this.state);
595       this.sendUnreadCount();
596     } else if (op == UserOperation.GetReportCount) {
597       let data = wsJsonToRes<GetReportCountResponse>(msg).data;
598       this.state.unreadReportCount = data.post_reports + data.comment_reports;
599       this.setState(this.state);
600       this.sendReportUnread();
601     } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
602       let data =
603         wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(msg).data;
604       this.state.unreadApplicationCount = data.registration_applications;
605       this.setState(this.state);
606       this.sendApplicationUnread();
607     } else if (op == UserOperation.GetSite) {
608       // This is only called on a successful login
609       let data = wsJsonToRes<GetSiteResponse>(msg).data;
610       console.log(data.my_user);
611       UserService.Instance.myUserInfo = data.my_user;
612       setTheme(
613         UserService.Instance.myUserInfo.local_user_view.local_user.theme
614       );
615       i18n.changeLanguage(getLanguage());
616       this.state.isLoggedIn = true;
617       this.setState(this.state);
618     } else if (op == UserOperation.CreateComment) {
619       let data = wsJsonToRes<CommentResponse>(msg).data;
620
621       if (this.state.isLoggedIn) {
622         if (
623           data.recipient_ids.includes(
624             UserService.Instance.myUserInfo.local_user_view.local_user.id
625           )
626         ) {
627           this.state.unreadInboxCount++;
628           this.setState(this.state);
629           this.sendUnreadCount();
630           notifyComment(data.comment_view, this.context.router);
631         }
632       }
633     } else if (op == UserOperation.CreatePrivateMessage) {
634       let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
635
636       if (this.state.isLoggedIn) {
637         if (
638           data.private_message_view.recipient.id ==
639           UserService.Instance.myUserInfo.local_user_view.person.id
640         ) {
641           this.state.unreadInboxCount++;
642           this.setState(this.state);
643           this.sendUnreadCount();
644           notifyPrivateMessage(data.private_message_view, this.context.router);
645         }
646       }
647     }
648   }
649
650   fetchUnreads() {
651     console.log("Fetching inbox unreads...");
652
653     let unreadForm: GetUnreadCount = {
654       auth: authField(),
655     };
656     WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
657
658     console.log("Fetching reports...");
659
660     let reportCountForm: GetReportCount = {
661       auth: authField(),
662     };
663     WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
664
665     if (UserService.Instance.myUserInfo?.local_user_view.person.admin) {
666       console.log("Fetching applications...");
667
668       let applicationCountForm: GetUnreadRegistrationApplicationCount = {
669         auth: authField(),
670       };
671       WebSocketService.Instance.send(
672         wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
673       );
674     }
675   }
676
677   get currentLocation() {
678     return this.context.router.history.location.pathname;
679   }
680
681   sendUnreadCount() {
682     UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
683   }
684
685   sendReportUnread() {
686     UserService.Instance.unreadReportCountSub.next(
687       this.state.unreadReportCount
688     );
689   }
690
691   sendApplicationUnread() {
692     UserService.Instance.unreadApplicationCountSub.next(
693       this.state.unreadApplicationCount
694     );
695   }
696
697   get canAdmin(): boolean {
698     return (
699       UserService.Instance.myUserInfo &&
700       this.props.site_res.admins
701         .map(a => a.person.id)
702         .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
703     );
704   }
705
706   get canCreateCommunity(): boolean {
707     let adminOnly =
708       this.props.site_res.site_view?.site.community_creation_admin_only;
709     return !adminOnly || this.canAdmin;
710   }
711
712   requestNotificationPermission() {
713     if (UserService.Instance.myUserInfo) {
714       document.addEventListener("DOMContentLoaded", function () {
715         if (!Notification) {
716           toast(i18n.t("notifications_error"), "danger");
717           return;
718         }
719
720         if (Notification.permission !== "granted")
721           Notification.requestPermission();
722       });
723     }
724   }
725 }