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