1 import { Component, linkEvent } from "inferno";
2 import { NavLink } from "inferno-router";
6 GetReportCountResponse,
9 GetUnreadCountResponse,
10 GetUnreadRegistrationApplicationCount,
11 GetUnreadRegistrationApplicationCountResponse,
12 PrivateMessageResponse,
16 } from "lemmy-js-client";
17 import { Subscription } from "rxjs";
18 import { i18n } from "../../i18next";
19 import { UserService, WebSocketService } from "../../services";
34 import { Icon } from "../common/icon";
35 import { PictrsImage } from "../common/pictrs-image";
37 interface NavbarProps {
38 siteRes: GetSiteResponse;
41 interface NavbarState {
43 unreadInboxCount: number;
44 unreadReportCount: number;
45 unreadApplicationCount: number;
47 showDropdown: boolean;
48 onSiteBanner?(url: string): any;
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 = {
60 unreadApplicationCount: 0,
67 constructor(props: any, context: any) {
68 super(props, context);
70 this.parseMessage = this.parseMessage.bind(this);
71 this.subscription = wsSubscribe(this.parseMessage);
75 // Subscribe to jwt changes
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(
90 this.requestNotificationPermission();
92 // Subscribe to unread count changes
93 this.unreadInboxCountSub =
94 UserService.Instance.unreadInboxCountSub.subscribe(res => {
95 this.setState({ unreadInboxCount: res });
97 // Subscribe to unread report count changes
98 this.unreadReportCountSub =
99 UserService.Instance.unreadReportCountSub.subscribe(res => {
100 this.setState({ unreadReportCount: res });
102 // Subscribe to unread application count
103 this.unreadApplicationCountSub =
104 UserService.Instance.unreadApplicationCountSub.subscribe(res => {
105 this.setState({ unreadApplicationCount: res });
110 componentWillUnmount() {
111 this.wsSub.unsubscribe();
112 this.userSub.unsubscribe();
113 this.unreadInboxCountSub.unsubscribe();
114 this.unreadReportCountSub.unsubscribe();
115 this.unreadApplicationCountSub.unsubscribe();
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/`);
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`
133 return this.navbar();
136 // TODO class active corresponding to current page
138 let siteView = this.props.siteRes.site_view;
139 let person = UserService.Instance.myUserInfo?.local_user_view.person;
141 <nav className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3">
142 <div className="container-lg">
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"
149 {siteView.site.icon && showAvatars() && (
150 <PictrsImage src={siteView.site.icon} icon />
154 {UserService.Instance.myUserInfo && (
156 <ul className="navbar-nav ml-auto">
157 <li className="nav-item">
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),
168 {this.state.unreadInboxCount > 0 && (
169 <span className="mx-1 badge badge-light">
170 {numToSI(this.state.unreadInboxCount)}
176 {this.moderatesSomething && (
177 <ul className="navbar-nav ml-1">
178 <li className="nav-item">
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),
188 <Icon icon="shield" />
189 {this.state.unreadReportCount > 0 && (
190 <span className="mx-1 badge badge-light">
191 {numToSI(this.state.unreadReportCount)}
199 <ul className="navbar-nav ml-1">
200 <li className="nav-item">
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
212 <Icon icon="clipboard" />
213 {this.state.unreadApplicationCount > 0 && (
214 <span className="mx-1 badge badge-light">
215 {numToSI(this.state.unreadApplicationCount)}
225 className="navbar-toggler border-0 p-1"
228 onClick={linkEvent(this, this.handleToggleExpandNavbar)}
229 data-tippy-content={i18n.t("expand_here")}
234 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
236 <ul className="navbar-nav my-2 mr-auto">
237 <li className="nav-item">
241 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
242 title={i18n.t("communities")}
244 {i18n.t("communities")}
247 <li className="nav-item">
248 {/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
251 pathname: "/create_post",
255 state: { prevPath: this.currentLocation },
258 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
259 title={i18n.t("create_post")}
261 {i18n.t("create_post")}
264 {canCreateCommunity(this.props.siteRes) && (
265 <li className="nav-item">
267 to="/create_community"
269 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
270 title={i18n.t("create_community")}
272 {i18n.t("create_community")}
276 <li className="nav-item">
279 title={i18n.t("support_lemmy")}
280 href={donateLemmyUrl}
282 <Icon icon="heart" classes="small" />
286 <ul className="navbar-nav my-2">
288 <li className="nav-item">
292 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
293 title={i18n.t("admin_settings")}
295 <Icon icon="settings" />
300 {!this.context.router.history.location.pathname.match(
303 <ul className="navbar-nav">
304 <li className="nav-item">
308 title={i18n.t("search")}
310 <Icon icon="search" />
315 {UserService.Instance.myUserInfo ? (
317 <ul className="navbar-nav my-2">
318 <li className="nav-item">
322 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
323 title={i18n.t("unread_messages", {
324 count: this.state.unreadInboxCount,
325 formattedCount: numToSI(this.state.unreadInboxCount),
329 {this.state.unreadInboxCount > 0 && (
330 <span className="ml-1 badge badge-light">
331 {numToSI(this.state.unreadInboxCount)}
337 {this.moderatesSomething && (
338 <ul className="navbar-nav my-2">
339 <li className="nav-item">
343 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
344 title={i18n.t("unread_reports", {
345 count: this.state.unreadReportCount,
346 formattedCount: numToSI(this.state.unreadReportCount),
349 <Icon icon="shield" />
350 {this.state.unreadReportCount > 0 && (
351 <span className="ml-1 badge badge-light">
352 {numToSI(this.state.unreadReportCount)}
360 <ul className="navbar-nav my-2">
361 <li className="nav-item">
363 to="/registration_applications"
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
373 <Icon icon="clipboard" />
374 {this.state.unreadApplicationCount > 0 && (
375 <span className="mx-1 badge badge-light">
376 {numToSI(this.state.unreadApplicationCount)}
384 <ul className="navbar-nav">
385 <li className="nav-item dropdown">
387 className="nav-link btn btn-link dropdown-toggle"
388 onClick={linkEvent(this, this.handleToggleDropdown)}
391 aria-expanded="false"
394 {showAvatars() && person.avatar && (
395 <PictrsImage src={person.avatar} icon />
397 {person.display_name ?? person.name}
400 {this.state.showDropdown && (
402 className="dropdown-content"
403 onMouseLeave={linkEvent(
405 this.handleToggleDropdown
408 <li className="nav-item">
410 to={`/u/${person.name}`}
412 title={i18n.t("profile")}
414 <Icon icon="user" classes="mr-1" />
418 <li className="nav-item">
422 title={i18n.t("settings")}
424 <Icon icon="settings" classes="mr-1" />
429 <hr className="dropdown-divider" />
431 <li className="nav-item">
433 className="nav-link btn btn-link"
434 onClick={linkEvent(this, this.handleLogoutClick)}
437 <Icon icon="log-out" classes="mr-1" />
448 <ul className="navbar-nav my-2">
449 <li className="nav-item">
453 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
454 title={i18n.t("login")}
459 <li className="nav-item">
463 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
464 title={i18n.t("sign_up")}
477 get moderatesSomething(): boolean {
478 let mods = UserService.Instance.myUserInfo?.moderates;
479 let moderatesS = (mods && mods.length > 0) || false;
480 return amAdmin() || moderatesS;
483 handleToggleExpandNavbar(i: Navbar) {
484 i.setState({ expanded: !i.state.expanded });
487 handleHideExpandNavbar(i: Navbar) {
488 i.setState({ expanded: false, showDropdown: false });
491 handleSearchParam(i: Navbar, event: any) {
492 i.setState({ searchParam: event.target.value });
495 handleLogoutClick(i: Navbar) {
496 i.setState({ showDropdown: false, expanded: false });
497 UserService.Instance.logout();
500 handleToggleDropdown(i: Navbar) {
501 i.setState({ showDropdown: !i.state.showDropdown });
504 parseMessage(msg: any) {
505 let op = wsUserOp(msg);
508 if (msg.error == "not_logged_in") {
509 UserService.Instance.logout();
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(
523 } else if (op == UserOperation.GetUnreadCount) {
524 let data = wsJsonToRes<GetUnreadCountResponse>(msg);
526 unreadInboxCount: data.replies + data.mentions + data.private_messages,
528 this.sendUnreadCount();
529 } else if (op == UserOperation.GetReportCount) {
530 let data = wsJsonToRes<GetReportCountResponse>(msg);
534 data.comment_reports +
535 (data.private_message_reports ?? 0),
537 this.sendReportUnread();
538 } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
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;
548 data.recipient_ids.includes(mui.local_user_view.local_user.id)
551 unreadInboxCount: this.state.unreadInboxCount + 1,
553 this.sendUnreadCount();
554 notifyComment(data.comment_view, this.context.router);
556 } else if (op == UserOperation.CreatePrivateMessage) {
557 let data = wsJsonToRes<PrivateMessageResponse>(msg);
560 data.private_message_view.recipient.id ==
561 UserService.Instance.myUserInfo?.local_user_view.person.id
564 unreadInboxCount: this.state.unreadInboxCount + 1,
566 this.sendUnreadCount();
567 notifyPrivateMessage(data.private_message_view, this.context.router);
573 console.log("Fetching inbox unreads...");
577 let unreadForm: GetUnreadCount = {
580 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
582 console.log("Fetching reports...");
584 let reportCountForm: GetReportCount = {
587 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
590 console.log("Fetching applications...");
592 let applicationCountForm: GetUnreadRegistrationApplicationCount = {
595 WebSocketService.Instance.send(
596 wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
602 get currentLocation() {
603 return this.context.router.history.location.pathname;
607 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
611 UserService.Instance.unreadReportCountSub.next(
612 this.state.unreadReportCount
616 sendApplicationUnread() {
617 UserService.Instance.unreadApplicationCountSub.next(
618 this.state.unreadApplicationCount
622 requestNotificationPermission() {
623 if (UserService.Instance.myUserInfo) {
624 document.addEventListener("DOMContentLoaded", function () {
626 toast(i18n.t("notifications_error"), "danger");
630 if (Notification.permission !== "granted")
631 Notification.requestPermission();