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(),
96 this.requestNotificationPermission();
98 // Subscribe to unread count changes
99 this.unreadInboxCountSub =
100 UserService.Instance.unreadInboxCountSub.subscribe(res => {
101 this.setState({ unreadInboxCount: res });
103 // Subscribe to unread report count changes
104 this.unreadReportCountSub =
105 UserService.Instance.unreadReportCountSub.subscribe(res => {
106 this.setState({ unreadReportCount: res });
108 // Subscribe to unread application count
109 this.unreadApplicationCountSub =
110 UserService.Instance.unreadApplicationCountSub.subscribe(res => {
111 this.setState({ unreadApplicationCount: res });
116 componentWillUnmount() {
117 this.wsSub.unsubscribe();
118 this.userSub.unsubscribe();
119 this.unreadInboxCountSub.unsubscribe();
120 this.unreadReportCountSub.unsubscribe();
121 this.unreadApplicationCountSub.unsubscribe();
125 const searchParam = this.state.searchParam;
126 this.setState({ searchParam: "" });
127 this.setState({ toggleSearch: false });
128 this.setState({ showDropdown: false, expanded: false });
129 if (searchParam === "") {
130 this.context.router.history.push(`/search/`);
132 const searchParamEncoded = encodeURIComponent(searchParam);
133 this.context.router.history.push(
134 `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
140 return this.navbar();
143 // TODO class active corresponding to current page
145 let siteView = this.props.siteRes.site_view;
147 <nav className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3">
148 <div className="container-lg">
151 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
152 title={siteView.site.description.unwrapOr(siteView.site.name)}
153 className="d-flex align-items-center navbar-brand mr-md-3"
155 {siteView.site.icon.match({
156 some: icon => showAvatars() && <PictrsImage src={icon} icon />,
161 {UserService.Instance.myUserInfo.isSome() && (
163 <ul className="navbar-nav ml-auto">
164 <li className="nav-item">
167 className="p-1 navbar-toggler nav-link border-0"
168 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
169 title={i18n.t("unread_messages", {
170 count: this.state.unreadInboxCount,
171 formattedCount: numToSI(this.state.unreadInboxCount),
175 {this.state.unreadInboxCount > 0 && (
176 <span className="mx-1 badge badge-light">
177 {numToSI(this.state.unreadInboxCount)}
183 {this.moderatesSomething && (
184 <ul className="navbar-nav ml-1">
185 <li className="nav-item">
188 className="p-1 navbar-toggler nav-link border-0"
189 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
190 title={i18n.t("unread_reports", {
191 count: this.state.unreadReportCount,
192 formattedCount: numToSI(this.state.unreadReportCount),
195 <Icon icon="shield" />
196 {this.state.unreadReportCount > 0 && (
197 <span className="mx-1 badge badge-light">
198 {numToSI(this.state.unreadReportCount)}
206 <ul className="navbar-nav ml-1">
207 <li className="nav-item">
209 to="/registration_applications"
210 className="p-1 navbar-toggler nav-link border-0"
211 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
212 title={i18n.t("unread_registration_applications", {
213 count: this.state.unreadApplicationCount,
214 formattedCount: numToSI(
215 this.state.unreadApplicationCount
219 <Icon icon="clipboard" />
220 {this.state.unreadApplicationCount > 0 && (
221 <span className="mx-1 badge badge-light">
222 {numToSI(this.state.unreadApplicationCount)}
232 className="navbar-toggler border-0 p-1"
235 onClick={linkEvent(this, this.handleToggleExpandNavbar)}
236 data-tippy-content={i18n.t("expand_here")}
241 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
243 <ul className="navbar-nav my-2 mr-auto">
244 <li className="nav-item">
248 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
249 title={i18n.t("communities")}
251 {i18n.t("communities")}
254 <li className="nav-item">
255 {/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
258 pathname: "/create_post",
262 state: { prevPath: this.currentLocation },
265 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
266 title={i18n.t("create_post")}
268 {i18n.t("create_post")}
271 {canCreateCommunity(this.props.siteRes) && (
272 <li className="nav-item">
274 to="/create_community"
276 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
277 title={i18n.t("create_community")}
279 {i18n.t("create_community")}
283 <li className="nav-item">
286 title={i18n.t("support_lemmy")}
287 href={donateLemmyUrl}
289 <Icon icon="heart" classes="small" />
293 <ul className="navbar-nav my-2">
295 <li className="nav-item">
299 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
300 title={i18n.t("admin_settings")}
302 <Icon icon="settings" />
307 {!this.context.router.history.location.pathname.match(
310 <ul className="navbar-nav">
311 <li className="nav-item">
313 className="form-inline mr-1"
314 onSubmit={linkEvent(this, this.handleSearchSubmit)}
318 className={`form-control mr-0 search-input ${
319 this.state.toggleSearch ? "show-input" : "hide-input"
321 onInput={linkEvent(this, this.handleSearchParam)}
322 value={this.state.searchParam}
323 ref={this.searchTextField}
324 disabled={!this.state.toggleSearch}
326 placeholder={i18n.t("search")}
327 onBlur={linkEvent(this, this.handleSearchBlur)}
329 <label className="sr-only" htmlFor="search-input">
334 onClick={linkEvent(this, this.handleSearchBtn)}
335 className="px-1 btn btn-link nav-link"
336 aria-label={i18n.t("search")}
338 <Icon icon="search" />
344 {UserService.Instance.myUserInfo.isSome() ? (
346 <ul className="navbar-nav my-2">
347 <li className="nav-item">
351 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
352 title={i18n.t("unread_messages", {
353 count: this.state.unreadInboxCount,
354 formattedCount: numToSI(this.state.unreadInboxCount),
358 {this.state.unreadInboxCount > 0 && (
359 <span className="ml-1 badge badge-light">
360 {numToSI(this.state.unreadInboxCount)}
366 {this.moderatesSomething && (
367 <ul className="navbar-nav my-2">
368 <li className="nav-item">
372 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
373 title={i18n.t("unread_reports", {
374 count: this.state.unreadReportCount,
375 formattedCount: numToSI(this.state.unreadReportCount),
378 <Icon icon="shield" />
379 {this.state.unreadReportCount > 0 && (
380 <span className="ml-1 badge badge-light">
381 {numToSI(this.state.unreadReportCount)}
389 <ul className="navbar-nav my-2">
390 <li className="nav-item">
392 to="/registration_applications"
394 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
395 title={i18n.t("unread_registration_applications", {
396 count: this.state.unreadApplicationCount,
397 formattedCount: numToSI(
398 this.state.unreadApplicationCount
402 <Icon icon="clipboard" />
403 {this.state.unreadApplicationCount > 0 && (
404 <span className="mx-1 badge badge-light">
405 {numToSI(this.state.unreadApplicationCount)}
412 {UserService.Instance.myUserInfo
413 .map(m => m.local_user_view.person)
416 <ul className="navbar-nav">
417 <li className="nav-item dropdown">
419 className="nav-link btn btn-link dropdown-toggle"
420 onClick={linkEvent(this, this.handleToggleDropdown)}
423 aria-expanded="false"
427 person.avatar.match({
429 <PictrsImage src={avatar} icon />
433 {person.display_name.unwrapOr(person.name)}
436 {this.state.showDropdown && (
438 className="dropdown-content"
439 onMouseLeave={linkEvent(
441 this.handleToggleDropdown
444 <li className="nav-item">
446 to={`/u/${person.name}`}
448 title={i18n.t("profile")}
450 <Icon icon="user" classes="mr-1" />
454 <li className="nav-item">
458 title={i18n.t("settings")}
460 <Icon icon="settings" classes="mr-1" />
465 <hr className="dropdown-divider" />
467 <li className="nav-item">
469 className="nav-link btn btn-link"
472 this.handleLogoutClick
476 <Icon icon="log-out" classes="mr-1" />
489 <ul className="navbar-nav my-2">
490 <li className="nav-item">
494 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
495 title={i18n.t("login")}
500 <li className="nav-item">
504 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
505 title={i18n.t("sign_up")}
518 get moderatesSomething(): boolean {
520 UserService.Instance.myUserInfo.map(m => m.moderates).unwrapOr([])
525 handleToggleExpandNavbar(i: Navbar) {
526 i.setState({ expanded: !i.state.expanded });
529 handleHideExpandNavbar(i: Navbar) {
530 i.setState({ expanded: false, showDropdown: false });
533 handleSearchParam(i: Navbar, event: any) {
534 i.setState({ searchParam: event.target.value });
537 handleSearchSubmit(i: Navbar, event: any) {
538 event.preventDefault();
542 handleSearchBtn(i: Navbar, event: any) {
543 event.preventDefault();
544 i.setState({ toggleSearch: true });
546 i.searchTextField.current.focus();
547 const offsetWidth = i.searchTextField.current.offsetWidth;
548 if (i.state.searchParam && offsetWidth > 100) {
553 handleSearchBlur(i: Navbar, event: any) {
554 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
555 i.setState({ toggleSearch: false });
559 handleLogoutClick(i: Navbar) {
560 i.setState({ showDropdown: false, expanded: false });
561 UserService.Instance.logout();
564 handleToggleDropdown(i: Navbar) {
565 i.setState({ showDropdown: !i.state.showDropdown });
568 parseMessage(msg: any) {
569 let op = wsUserOp(msg);
572 if (msg.error == "not_logged_in") {
573 UserService.Instance.logout();
576 } else if (msg.reconnect) {
577 console.log(i18n.t("websocket_reconnected"));
578 if (UserService.Instance.myUserInfo.isSome()) {
579 WebSocketService.Instance.send(
581 auth: auth().unwrap(),
586 } else if (op == UserOperation.GetUnreadCount) {
587 let data = wsJsonToRes<GetUnreadCountResponse>(
589 GetUnreadCountResponse
592 unreadInboxCount: data.replies + data.mentions + data.private_messages,
594 this.sendUnreadCount();
595 } else if (op == UserOperation.GetReportCount) {
596 let data = wsJsonToRes<GetReportCountResponse>(
598 GetReportCountResponse
603 data.comment_reports +
604 data.private_message_reports.unwrapOr(0),
606 this.sendReportUnread();
607 } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
608 let data = wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(
610 GetUnreadRegistrationApplicationCountResponse
612 this.setState({ unreadApplicationCount: data.registration_applications });
613 this.sendApplicationUnread();
614 } else if (op == UserOperation.CreateComment) {
615 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
617 UserService.Instance.myUserInfo.match({
619 if (data.recipient_ids.includes(mui.local_user_view.local_user.id)) {
621 unreadInboxCount: this.state.unreadInboxCount + 1,
623 this.sendUnreadCount();
624 notifyComment(data.comment_view, this.context.router);
629 } else if (op == UserOperation.CreatePrivateMessage) {
630 let data = wsJsonToRes<PrivateMessageResponse>(
632 PrivateMessageResponse
635 UserService.Instance.myUserInfo.match({
638 data.private_message_view.recipient.id ==
639 mui.local_user_view.person.id
642 unreadInboxCount: this.state.unreadInboxCount + 1,
644 this.sendUnreadCount();
645 notifyPrivateMessage(
646 data.private_message_view,
657 console.log("Fetching inbox unreads...");
659 let unreadForm = new GetUnreadCount({
660 auth: auth().unwrap(),
662 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
664 console.log("Fetching reports...");
666 let reportCountForm = new GetReportCount({
668 auth: auth().unwrap(),
670 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
673 console.log("Fetching applications...");
675 let applicationCountForm = new GetUnreadRegistrationApplicationCount({
676 auth: auth().unwrap(),
678 WebSocketService.Instance.send(
679 wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
684 get currentLocation() {
685 return this.context.router.history.location.pathname;
689 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
693 UserService.Instance.unreadReportCountSub.next(
694 this.state.unreadReportCount
698 sendApplicationUnread() {
699 UserService.Instance.unreadApplicationCountSub.next(
700 this.state.unreadApplicationCount
704 requestNotificationPermission() {
705 if (UserService.Instance.myUserInfo.isSome()) {
706 document.addEventListener("DOMContentLoaded", function () {
708 toast(i18n.t("notifications_error"), "danger");
712 if (Notification.permission !== "granted")
713 Notification.requestPermission();