]> Untitled Git - lemmy-ui.git/blob - src/shared/components/app/navbar.tsx
Remove touch events
[lemmy-ui.git] / src / shared / components / app / navbar.tsx
1 import { Component, createRef, linkEvent, RefObject } from "inferno";
2 import { Link } 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             <Link
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             </Link>
175           )}
176           {this.state.isLoggedIn && (
177             <>
178               <ul class="navbar-nav ml-auto">
179                 <li className="nav-item">
180                   <Link
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                   </Link>
196                 </li>
197               </ul>
198               {UserService.Instance.myUserInfo?.moderates.length > 0 && (
199                 <ul class="navbar-nav ml-1">
200                   <li className="nav-item">
201                     <Link
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                     </Link>
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                 <Link
237                   to="/communities"
238                   className="nav-link"
239                   onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
240                   title={i18n.t("communities")}
241                 >
242                   {i18n.t("communities")}
243                 </Link>
244               </li>
245               <li class="nav-item">
246                 <Link
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                 </Link>
257               </li>
258               {this.canCreateCommunity && (
259                 <li class="nav-item">
260                   <Link
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                   </Link>
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                   <Link
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                   </Link>
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                     <Link
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                     </Link>
347                   </li>
348                 </ul>
349                 {UserService.Instance.myUserInfo?.moderates.length > 0 && (
350                   <ul class="navbar-nav my-2">
351                     <li className="nav-item">
352                       <Link
353                         className="nav-link"
354                         to="/reports"
355                         onTouchEnd={linkEvent(
356                           this,
357                           this.handleHideExpandNavbar
358                         )}
359                         onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
360                         title={i18n.t("unread_reports", {
361                           count: this.state.unreadReportCount,
362                           formattedCount: numToSI(this.state.unreadReportCount),
363                         })}
364                       >
365                         <Icon icon="shield" />
366                         {this.state.unreadReportCount > 0 && (
367                           <span class="ml-1 badge badge-light">
368                             {numToSI(this.state.unreadReportCount)}
369                           </span>
370                         )}
371                       </Link>
372                     </li>
373                   </ul>
374                 )}
375                 <ul class="navbar-nav">
376                   <li class="nav-item dropdown">
377                     <button
378                       class="nav-link btn btn-link dropdown-toggle"
379                       onClick={linkEvent(this, this.handleToggleDropdown)}
380                       id="navbarDropdown"
381                       role="button"
382                       aria-expanded="false"
383                     >
384                       <span>
385                         {person.avatar && showAvatars() && (
386                           <PictrsImage src={person.avatar} icon />
387                         )}
388                         {person.display_name
389                           ? person.display_name
390                           : person.name}
391                       </span>
392                     </button>
393                     {this.state.showDropdown && (
394                       <div
395                         class="dropdown-content"
396                         onMouseLeave={linkEvent(
397                           this,
398                           this.handleToggleDropdown
399                         )}
400                       >
401                         <li className="nav-item">
402                           <Link
403                             to={`/u/${UserService.Instance.myUserInfo.local_user_view.person.name}`}
404                             className="nav-link"
405                             title={i18n.t("profile")}
406                           >
407                             <Icon icon="user" classes="mr-1" />
408                             {i18n.t("profile")}
409                           </Link>
410                         </li>
411                         <li className="nav-item">
412                           <Link
413                             to="/settings"
414                             className="nav-link"
415                             title={i18n.t("settings")}
416                           >
417                             <Icon icon="settings" classes="mr-1" />
418                             {i18n.t("settings")}
419                           </Link>
420                         </li>
421                         <li>
422                           <hr class="dropdown-divider" />
423                         </li>
424                         <li className="nav-item">
425                           <button
426                             className="nav-link btn btn-link"
427                             onClick={linkEvent(this, this.handleLogoutClick)}
428                             title="test"
429                           >
430                             <Icon icon="log-out" classes="mr-1" />
431                             {i18n.t("logout")}
432                           </button>
433                         </li>
434                       </div>
435                     )}
436                   </li>
437                 </ul>
438               </>
439             ) : (
440               <ul class="navbar-nav my-2">
441                 <li className="nav-item">
442                   <Link
443                     to="/login"
444                     className="nav-link"
445                     onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
446                     title={i18n.t("login")}
447                   >
448                     {i18n.t("login")}
449                   </Link>
450                 </li>
451                 <li className="nav-item">
452                   <Link
453                     to="/signup"
454                     className="nav-link"
455                     onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
456                     title={i18n.t("sign_up")}
457                   >
458                     {i18n.t("sign_up")}
459                   </Link>
460                 </li>
461               </ul>
462             )}
463           </div>
464         </div>
465       </nav>
466     );
467   }
468
469   handleToggleExpandNavbar(i: Navbar) {
470     i.state.expanded = !i.state.expanded;
471     i.setState(i.state);
472   }
473
474   handleHideExpandNavbar(i: Navbar) {
475     i.setState({ expanded: false, showDropdown: false });
476   }
477
478   handleSearchParam(i: Navbar, event: any) {
479     i.state.searchParam = event.target.value;
480     i.setState(i.state);
481   }
482
483   handleSearchSubmit(i: Navbar, event: any) {
484     event.preventDefault();
485     i.updateUrl();
486   }
487
488   handleSearchBtn(i: Navbar, event: any) {
489     event.preventDefault();
490     i.setState({ toggleSearch: true });
491
492     i.searchTextField.current.focus();
493     const offsetWidth = i.searchTextField.current.offsetWidth;
494     if (i.state.searchParam && offsetWidth > 100) {
495       i.updateUrl();
496     }
497   }
498
499   handleSearchBlur(i: Navbar, event: any) {
500     if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
501       i.state.toggleSearch = false;
502       i.setState(i.state);
503     }
504   }
505
506   handleLogoutClick(i: Navbar) {
507     i.setState({ showDropdown: false, expanded: false });
508     UserService.Instance.logout();
509     window.location.href = "/";
510     location.reload();
511   }
512
513   handleToggleDropdown(i: Navbar) {
514     i.state.showDropdown = !i.state.showDropdown;
515     i.setState(i.state);
516   }
517
518   parseMessage(msg: any) {
519     let op = wsUserOp(msg);
520     console.log(msg);
521     if (msg.error) {
522       if (msg.error == "not_logged_in") {
523         UserService.Instance.logout();
524         location.reload();
525       }
526       return;
527     } else if (msg.reconnect) {
528       console.log(i18n.t("websocket_reconnected"));
529       WebSocketService.Instance.send(
530         wsClient.userJoin({
531           auth: authField(),
532         })
533       );
534       this.fetchUnreads();
535     } else if (op == UserOperation.GetUnreadCount) {
536       let data = wsJsonToRes<GetUnreadCountResponse>(msg).data;
537       this.state.unreadInboxCount =
538         data.replies + data.mentions + data.private_messages;
539       this.setState(this.state);
540       this.sendUnreadCount();
541     } else if (op == UserOperation.GetReportCount) {
542       let data = wsJsonToRes<GetReportCountResponse>(msg).data;
543       this.state.unreadReportCount = data.post_reports + data.comment_reports;
544       this.setState(this.state);
545       this.sendReportUnread();
546     } else if (op == UserOperation.GetSite) {
547       // This is only called on a successful login
548       let data = wsJsonToRes<GetSiteResponse>(msg).data;
549       console.log(data.my_user);
550       UserService.Instance.myUserInfo = data.my_user;
551       setTheme(
552         UserService.Instance.myUserInfo.local_user_view.local_user.theme
553       );
554       i18n.changeLanguage(getLanguage());
555       this.state.isLoggedIn = true;
556       this.setState(this.state);
557     } else if (op == UserOperation.CreateComment) {
558       let data = wsJsonToRes<CommentResponse>(msg).data;
559
560       if (this.state.isLoggedIn) {
561         if (
562           data.recipient_ids.includes(
563             UserService.Instance.myUserInfo.local_user_view.local_user.id
564           )
565         ) {
566           this.state.unreadInboxCount++;
567           this.setState(this.state);
568           this.sendUnreadCount();
569           notifyComment(data.comment_view, this.context.router);
570         }
571       }
572     } else if (op == UserOperation.CreatePrivateMessage) {
573       let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
574
575       if (this.state.isLoggedIn) {
576         if (
577           data.private_message_view.recipient.id ==
578           UserService.Instance.myUserInfo.local_user_view.person.id
579         ) {
580           this.state.unreadInboxCount++;
581           this.setState(this.state);
582           this.sendUnreadCount();
583           notifyPrivateMessage(data.private_message_view, this.context.router);
584         }
585       }
586     }
587   }
588
589   fetchUnreads() {
590     console.log("Fetching inbox unreads...");
591
592     let unreadForm: GetUnreadCount = {
593       auth: authField(),
594     };
595
596     WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
597
598     console.log("Fetching reports...");
599
600     let reportCountForm: GetReportCount = {
601       auth: authField(),
602     };
603
604     WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
605   }
606
607   get currentLocation() {
608     return this.context.router.history.location.pathname;
609   }
610
611   sendUnreadCount() {
612     UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
613   }
614
615   sendReportUnread() {
616     UserService.Instance.unreadReportCountSub.next(
617       this.state.unreadReportCount
618     );
619   }
620
621   get canAdmin(): boolean {
622     return (
623       UserService.Instance.myUserInfo &&
624       this.props.site_res.admins
625         .map(a => a.person.id)
626         .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
627     );
628   }
629
630   get canCreateCommunity(): boolean {
631     let adminOnly =
632       this.props.site_res.site_view?.site.community_creation_admin_only;
633     return !adminOnly || this.canAdmin;
634   }
635
636   /// Listens for some websocket errors
637   websocketEvents() {
638     let msg = i18n.t("websocket_disconnected");
639     WebSocketService.Instance.closeEventListener(() => {
640       console.error(msg);
641     });
642   }
643
644   requestNotificationPermission() {
645     if (UserService.Instance.myUserInfo) {
646       document.addEventListener("DOMContentLoaded", function () {
647         if (!Notification) {
648           toast(i18n.t("notifications_error"), "danger");
649           return;
650         }
651
652         if (Notification.permission !== "granted")
653           Notification.requestPermission();
654       });
655     }
656   }
657 }