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