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