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 {
521 UserService.Instance.myUserInfo.map(m => m.moderates).unwrapOr([])
526 handleToggleExpandNavbar(i: Navbar) {
527 i.setState({ expanded: !i.state.expanded });
530 handleHideExpandNavbar(i: Navbar) {
531 i.setState({ expanded: false, showDropdown: false });
534 handleSearchParam(i: Navbar, event: any) {
535 i.setState({ searchParam: event.target.value });
538 handleSearchSubmit(i: Navbar, event: any) {
539 event.preventDefault();
543 handleSearchBtn(i: Navbar, event: any) {
544 event.preventDefault();
545 i.setState({ toggleSearch: true });
547 i.searchTextField.current.focus();
548 const offsetWidth = i.searchTextField.current.offsetWidth;
549 if (i.state.searchParam && offsetWidth > 100) {
554 handleSearchBlur(i: Navbar, event: any) {
555 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
556 i.setState({ toggleSearch: false });
560 handleLogoutClick(i: Navbar) {
561 i.setState({ showDropdown: false, expanded: false });
562 UserService.Instance.logout();
565 handleToggleDropdown(i: Navbar) {
566 i.setState({ showDropdown: !i.state.showDropdown });
569 parseMessage(msg: any) {
570 let op = wsUserOp(msg);
573 if (msg.error == "not_logged_in") {
574 UserService.Instance.logout();
577 } else if (msg.reconnect) {
578 console.log(i18n.t("websocket_reconnected"));
579 if (UserService.Instance.myUserInfo.isSome()) {
580 WebSocketService.Instance.send(
582 auth: auth().unwrap(),
587 } else if (op == UserOperation.GetUnreadCount) {
588 let data = wsJsonToRes<GetUnreadCountResponse>(
590 GetUnreadCountResponse
593 unreadInboxCount: data.replies + data.mentions + data.private_messages,
595 this.sendUnreadCount();
596 } else if (op == UserOperation.GetReportCount) {
597 let data = wsJsonToRes<GetReportCountResponse>(
599 GetReportCountResponse
604 data.comment_reports +
605 data.private_message_reports.unwrapOr(0),
607 this.sendReportUnread();
608 } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
609 let data = wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(
611 GetUnreadRegistrationApplicationCountResponse
613 this.setState({ unreadApplicationCount: data.registration_applications });
614 this.sendApplicationUnread();
615 } else if (op == UserOperation.CreateComment) {
616 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
618 UserService.Instance.myUserInfo.match({
620 if (data.recipient_ids.includes(mui.local_user_view.local_user.id)) {
622 unreadInboxCount: this.state.unreadInboxCount + 1,
624 this.sendUnreadCount();
625 notifyComment(data.comment_view, this.context.router);
630 } else if (op == UserOperation.CreatePrivateMessage) {
631 let data = wsJsonToRes<PrivateMessageResponse>(
633 PrivateMessageResponse
636 UserService.Instance.myUserInfo.match({
639 data.private_message_view.recipient.id ==
640 mui.local_user_view.person.id
643 unreadInboxCount: this.state.unreadInboxCount + 1,
645 this.sendUnreadCount();
646 notifyPrivateMessage(
647 data.private_message_view,
658 console.log("Fetching inbox unreads...");
660 let unreadForm = new GetUnreadCount({
661 auth: auth().unwrap(),
663 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
665 console.log("Fetching reports...");
667 let reportCountForm = new GetReportCount({
669 auth: auth().unwrap(),
671 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
674 console.log("Fetching applications...");
676 let applicationCountForm = new GetUnreadRegistrationApplicationCount({
677 auth: auth().unwrap(),
679 WebSocketService.Instance.send(
680 wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
685 get currentLocation() {
686 return this.context.router.history.location.pathname;
690 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
694 UserService.Instance.unreadReportCountSub.next(
695 this.state.unreadReportCount
699 sendApplicationUnread() {
700 UserService.Instance.unreadApplicationCountSub.next(
701 this.state.unreadApplicationCount
705 requestNotificationPermission() {
706 if (UserService.Instance.myUserInfo.isSome()) {
707 document.addEventListener("DOMContentLoaded", function () {
709 toast(i18n.t("notifications_error"), "danger");
713 if (Notification.permission !== "granted")
714 Notification.requestPermission();