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