1 import { None } from "@sniptt/monads";
2 import { Component, createRef, linkEvent, RefObject } from "inferno";
3 import { NavLink } from "inferno-router";
7 GetReportCountResponse,
10 GetUnreadCountResponse,
11 GetUnreadRegistrationApplicationCount,
12 GetUnreadRegistrationApplicationCountResponse,
13 PrivateMessageResponse,
17 } from "lemmy-js-client";
18 import { Subscription } from "rxjs";
19 import { i18n } from "../../i18next";
20 import { UserService, WebSocketService } from "../../services";
35 import { Icon } from "../common/icon";
36 import { PictrsImage } from "../common/pictrs-image";
38 interface NavbarProps {
39 siteRes: GetSiteResponse;
42 interface NavbarState {
44 unreadInboxCount: number;
45 unreadReportCount: number;
46 unreadApplicationCount: number;
48 toggleSearch: boolean;
49 showDropdown: boolean;
50 onSiteBanner?(url: string): any;
53 export class Navbar extends Component<NavbarProps, NavbarState> {
54 private wsSub: Subscription;
55 private userSub: Subscription;
56 private unreadInboxCountSub: Subscription;
57 private unreadReportCountSub: Subscription;
58 private unreadApplicationCountSub: Subscription;
59 private searchTextField: RefObject<HTMLInputElement>;
60 emptyState: NavbarState = {
63 unreadApplicationCount: 0,
71 constructor(props: any, context: any) {
72 super(props, context);
73 this.state = this.emptyState;
75 this.parseMessage = this.parseMessage.bind(this);
76 this.subscription = wsSubscribe(this.parseMessage);
80 // Subscribe to jwt changes
82 this.searchTextField = createRef();
84 // On the first load, check the unreads
85 if (UserService.Instance.myUserInfo.isSome()) {
86 this.requestNotificationPermission();
87 WebSocketService.Instance.send(
89 auth: auth().unwrap(),
93 if (this.props.siteRes.site_view.isSome()) {
98 this.requestNotificationPermission();
100 // Subscribe to unread count changes
101 this.unreadInboxCountSub =
102 UserService.Instance.unreadInboxCountSub.subscribe(res => {
103 this.setState({ unreadInboxCount: res });
105 // Subscribe to unread report count changes
106 this.unreadReportCountSub =
107 UserService.Instance.unreadReportCountSub.subscribe(res => {
108 this.setState({ unreadReportCount: res });
110 // Subscribe to unread application count
111 this.unreadApplicationCountSub =
112 UserService.Instance.unreadApplicationCountSub.subscribe(res => {
113 this.setState({ unreadApplicationCount: res });
118 componentWillUnmount() {
119 this.wsSub.unsubscribe();
120 this.userSub.unsubscribe();
121 this.unreadInboxCountSub.unsubscribe();
122 this.unreadReportCountSub.unsubscribe();
123 this.unreadApplicationCountSub.unsubscribe();
127 const searchParam = this.state.searchParam;
128 this.setState({ searchParam: "" });
129 this.setState({ toggleSearch: false });
130 this.setState({ showDropdown: false, expanded: false });
131 if (searchParam === "") {
132 this.context.router.history.push(`/search/`);
134 const searchParamEncoded = encodeURIComponent(searchParam);
135 this.context.router.history.push(
136 `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
142 return this.navbar();
145 // TODO class active corresponding to current page
148 <nav className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3">
149 <div className="container-lg">
150 {this.props.siteRes.site_view.match({
154 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
155 title={siteView.site.description.unwrapOr(siteView.site.name)}
156 className="d-flex align-items-center navbar-brand mr-md-3"
158 {siteView.site.icon.match({
160 showAvatars() && <PictrsImage src={icon} icon />,
168 {UserService.Instance.myUserInfo.isSome() && (
170 <ul className="navbar-nav ml-auto">
171 <li className="nav-item">
174 className="p-1 navbar-toggler nav-link border-0"
175 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
176 title={i18n.t("unread_messages", {
177 count: this.state.unreadInboxCount,
178 formattedCount: numToSI(this.state.unreadInboxCount),
182 {this.state.unreadInboxCount > 0 && (
183 <span className="mx-1 badge badge-light">
184 {numToSI(this.state.unreadInboxCount)}
190 {this.moderatesSomething && (
191 <ul className="navbar-nav ml-1">
192 <li className="nav-item">
195 className="p-1 navbar-toggler nav-link border-0"
196 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
197 title={i18n.t("unread_reports", {
198 count: this.state.unreadReportCount,
199 formattedCount: numToSI(this.state.unreadReportCount),
202 <Icon icon="shield" />
203 {this.state.unreadReportCount > 0 && (
204 <span className="mx-1 badge badge-light">
205 {numToSI(this.state.unreadReportCount)}
213 <ul className="navbar-nav ml-1">
214 <li className="nav-item">
216 to="/registration_applications"
217 className="p-1 navbar-toggler nav-link border-0"
218 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
219 title={i18n.t("unread_registration_applications", {
220 count: this.state.unreadApplicationCount,
221 formattedCount: numToSI(
222 this.state.unreadApplicationCount
226 <Icon icon="clipboard" />
227 {this.state.unreadApplicationCount > 0 && (
228 <span className="mx-1 badge badge-light">
229 {numToSI(this.state.unreadApplicationCount)}
239 className="navbar-toggler border-0 p-1"
242 onClick={linkEvent(this, this.handleToggleExpandNavbar)}
243 data-tippy-content={i18n.t("expand_here")}
248 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
250 <ul className="navbar-nav my-2 mr-auto">
251 <li className="nav-item">
255 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
256 title={i18n.t("communities")}
258 {i18n.t("communities")}
261 <li className="nav-item">
262 {/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
265 pathname: "/create_post",
269 state: { prevPath: this.currentLocation },
272 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
273 title={i18n.t("create_post")}
275 {i18n.t("create_post")}
278 {canCreateCommunity(this.props.siteRes) && (
279 <li className="nav-item">
281 to="/create_community"
283 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
284 title={i18n.t("create_community")}
286 {i18n.t("create_community")}
290 <li className="nav-item">
293 title={i18n.t("support_lemmy")}
294 href={donateLemmyUrl}
296 <Icon icon="heart" classes="small" />
300 <ul className="navbar-nav my-2">
302 <li className="nav-item">
306 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
307 title={i18n.t("admin_settings")}
309 <Icon icon="settings" />
314 {!this.context.router.history.location.pathname.match(
317 <ul className="navbar-nav">
318 <li className="nav-item">
320 className="form-inline mr-1"
321 onSubmit={linkEvent(this, this.handleSearchSubmit)}
325 className={`form-control mr-0 search-input ${
326 this.state.toggleSearch ? "show-input" : "hide-input"
328 onInput={linkEvent(this, this.handleSearchParam)}
329 value={this.state.searchParam}
330 ref={this.searchTextField}
331 disabled={!this.state.toggleSearch}
333 placeholder={i18n.t("search")}
334 onBlur={linkEvent(this, this.handleSearchBlur)}
336 <label className="sr-only" htmlFor="search-input">
341 onClick={linkEvent(this, this.handleSearchBtn)}
342 className="px-1 btn btn-link nav-link"
343 aria-label={i18n.t("search")}
345 <Icon icon="search" />
351 {UserService.Instance.myUserInfo.isSome() ? (
353 <ul className="navbar-nav my-2">
354 <li className="nav-item">
358 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
359 title={i18n.t("unread_messages", {
360 count: this.state.unreadInboxCount,
361 formattedCount: numToSI(this.state.unreadInboxCount),
365 {this.state.unreadInboxCount > 0 && (
366 <span className="ml-1 badge badge-light">
367 {numToSI(this.state.unreadInboxCount)}
373 {this.moderatesSomething && (
374 <ul className="navbar-nav my-2">
375 <li className="nav-item">
379 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
380 title={i18n.t("unread_reports", {
381 count: this.state.unreadReportCount,
382 formattedCount: numToSI(this.state.unreadReportCount),
385 <Icon icon="shield" />
386 {this.state.unreadReportCount > 0 && (
387 <span className="ml-1 badge badge-light">
388 {numToSI(this.state.unreadReportCount)}
396 <ul className="navbar-nav my-2">
397 <li className="nav-item">
399 to="/registration_applications"
401 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
402 title={i18n.t("unread_registration_applications", {
403 count: this.state.unreadApplicationCount,
404 formattedCount: numToSI(
405 this.state.unreadApplicationCount
409 <Icon icon="clipboard" />
410 {this.state.unreadApplicationCount > 0 && (
411 <span className="mx-1 badge badge-light">
412 {numToSI(this.state.unreadApplicationCount)}
419 {UserService.Instance.myUserInfo
420 .map(m => m.local_user_view.person)
423 <ul className="navbar-nav">
424 <li className="nav-item dropdown">
426 className="nav-link btn btn-link dropdown-toggle"
427 onClick={linkEvent(this, this.handleToggleDropdown)}
430 aria-expanded="false"
434 person.avatar.match({
436 <PictrsImage src={avatar} icon />
440 {person.display_name.unwrapOr(person.name)}
443 {this.state.showDropdown && (
445 className="dropdown-content"
446 onMouseLeave={linkEvent(
448 this.handleToggleDropdown
451 <li className="nav-item">
453 to={`/u/${person.name}`}
455 title={i18n.t("profile")}
457 <Icon icon="user" classes="mr-1" />
461 <li className="nav-item">
465 title={i18n.t("settings")}
467 <Icon icon="settings" classes="mr-1" />
472 <hr className="dropdown-divider" />
474 <li className="nav-item">
476 className="nav-link btn btn-link"
479 this.handleLogoutClick
483 <Icon icon="log-out" classes="mr-1" />
496 <ul className="navbar-nav my-2">
497 <li className="nav-item">
501 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
502 title={i18n.t("login")}
507 <li className="nav-item">
511 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
512 title={i18n.t("sign_up")}
525 get moderatesSomething(): boolean {
527 UserService.Instance.myUserInfo.map(m => m.moderates).unwrapOr([])
532 handleToggleExpandNavbar(i: Navbar) {
533 i.setState({ expanded: !i.state.expanded });
536 handleHideExpandNavbar(i: Navbar) {
537 i.setState({ expanded: false, showDropdown: false });
540 handleSearchParam(i: Navbar, event: any) {
541 i.setState({ searchParam: event.target.value });
544 handleSearchSubmit(i: Navbar, event: any) {
545 event.preventDefault();
549 handleSearchBtn(i: Navbar, event: any) {
550 event.preventDefault();
551 i.setState({ toggleSearch: true });
553 i.searchTextField.current.focus();
554 const offsetWidth = i.searchTextField.current.offsetWidth;
555 if (i.state.searchParam && offsetWidth > 100) {
560 handleSearchBlur(i: Navbar, event: any) {
561 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
562 i.setState({ toggleSearch: false });
566 handleLogoutClick(i: Navbar) {
567 i.setState({ showDropdown: false, expanded: false });
568 UserService.Instance.logout();
571 handleToggleDropdown(i: Navbar) {
572 i.setState({ showDropdown: !i.state.showDropdown });
575 parseMessage(msg: any) {
576 let op = wsUserOp(msg);
579 if (msg.error == "not_logged_in") {
580 UserService.Instance.logout();
583 } else if (msg.reconnect) {
584 console.log(i18n.t("websocket_reconnected"));
585 if (UserService.Instance.myUserInfo.isSome()) {
586 WebSocketService.Instance.send(
588 auth: auth().unwrap(),
593 } else if (op == UserOperation.GetUnreadCount) {
594 let data = wsJsonToRes<GetUnreadCountResponse>(
596 GetUnreadCountResponse
599 unreadInboxCount: data.replies + data.mentions + data.private_messages,
601 this.sendUnreadCount();
602 } else if (op == UserOperation.GetReportCount) {
603 let data = wsJsonToRes<GetReportCountResponse>(
605 GetReportCountResponse
610 data.comment_reports +
611 data.private_message_reports.unwrapOr(0),
613 this.sendReportUnread();
614 } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
615 let data = wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(
617 GetUnreadRegistrationApplicationCountResponse
619 this.setState({ unreadApplicationCount: data.registration_applications });
620 this.sendApplicationUnread();
621 } else if (op == UserOperation.CreateComment) {
622 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
624 UserService.Instance.myUserInfo.match({
626 if (data.recipient_ids.includes(mui.local_user_view.local_user.id)) {
628 unreadInboxCount: this.state.unreadInboxCount + 1,
630 this.sendUnreadCount();
631 notifyComment(data.comment_view, this.context.router);
636 } else if (op == UserOperation.CreatePrivateMessage) {
637 let data = wsJsonToRes<PrivateMessageResponse>(
639 PrivateMessageResponse
642 UserService.Instance.myUserInfo.match({
645 data.private_message_view.recipient.id ==
646 mui.local_user_view.person.id
649 unreadInboxCount: this.state.unreadInboxCount + 1,
651 this.sendUnreadCount();
652 notifyPrivateMessage(
653 data.private_message_view,
664 console.log("Fetching inbox unreads...");
666 let unreadForm = new GetUnreadCount({
667 auth: auth().unwrap(),
669 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
671 console.log("Fetching reports...");
673 let reportCountForm = new GetReportCount({
675 auth: auth().unwrap(),
677 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
680 console.log("Fetching applications...");
682 let applicationCountForm = new GetUnreadRegistrationApplicationCount({
683 auth: auth().unwrap(),
685 WebSocketService.Instance.send(
686 wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
691 get currentLocation() {
692 return this.context.router.history.location.pathname;
696 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
700 UserService.Instance.unreadReportCountSub.next(
701 this.state.unreadReportCount
705 sendApplicationUnread() {
706 UserService.Instance.unreadApplicationCountSub.next(
707 this.state.unreadApplicationCount
711 requestNotificationPermission() {
712 if (UserService.Instance.myUserInfo.isSome()) {
713 document.addEventListener("DOMContentLoaded", function () {
715 toast(i18n.t("notifications_error"), "danger");
719 if (Notification.permission !== "granted")
720 Notification.requestPermission();