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