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(
318 className="form-inline mr-2"
319 onSubmit={linkEvent(this, this.handleSearchSubmit)}
323 className={`form-control mr-0 search-input ${
324 this.state.toggleSearch ? "show-input" : "hide-input"
326 onInput={linkEvent(this, this.handleSearchParam)}
327 value={this.state.searchParam}
328 ref={this.searchTextField}
330 placeholder={i18n.t("search")}
331 onBlur={linkEvent(this, this.handleSearchBlur)}
333 <label className="sr-only" htmlFor="search-input">
338 onClick={linkEvent(this, this.handleSearchBtn)}
339 className="px-1 btn btn-link"
340 style="color: var(--gray)"
341 aria-label={i18n.t("search")}
343 <Icon icon="search" />
347 {UserService.Instance.myUserInfo.isSome() ? (
349 <ul className="navbar-nav my-2">
350 <li className="nav-item">
354 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
355 title={i18n.t("unread_messages", {
356 count: this.state.unreadInboxCount,
357 formattedCount: numToSI(this.state.unreadInboxCount),
361 {this.state.unreadInboxCount > 0 && (
362 <span className="ml-1 badge badge-light">
363 {numToSI(this.state.unreadInboxCount)}
369 {this.moderatesSomething && (
370 <ul className="navbar-nav my-2">
371 <li className="nav-item">
375 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
376 title={i18n.t("unread_reports", {
377 count: this.state.unreadReportCount,
378 formattedCount: numToSI(this.state.unreadReportCount),
381 <Icon icon="shield" />
382 {this.state.unreadReportCount > 0 && (
383 <span className="ml-1 badge badge-light">
384 {numToSI(this.state.unreadReportCount)}
392 <ul className="navbar-nav my-2">
393 <li className="nav-item">
395 to="/registration_applications"
397 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
398 title={i18n.t("unread_registration_applications", {
399 count: this.state.unreadApplicationCount,
400 formattedCount: numToSI(
401 this.state.unreadApplicationCount
405 <Icon icon="clipboard" />
406 {this.state.unreadApplicationCount > 0 && (
407 <span className="mx-1 badge badge-light">
408 {numToSI(this.state.unreadApplicationCount)}
415 {UserService.Instance.myUserInfo
416 .map(m => m.local_user_view.person)
419 <ul className="navbar-nav">
420 <li className="nav-item dropdown">
422 className="nav-link btn btn-link dropdown-toggle"
423 onClick={linkEvent(this, this.handleToggleDropdown)}
426 aria-expanded="false"
430 person.avatar.match({
432 <PictrsImage src={avatar} icon />
436 {person.display_name.unwrapOr(person.name)}
439 {this.state.showDropdown && (
441 className="dropdown-content"
442 onMouseLeave={linkEvent(
444 this.handleToggleDropdown
447 <li className="nav-item">
449 to={`/u/${person.name}`}
451 title={i18n.t("profile")}
453 <Icon icon="user" classes="mr-1" />
457 <li className="nav-item">
461 title={i18n.t("settings")}
463 <Icon icon="settings" classes="mr-1" />
468 <hr className="dropdown-divider" />
470 <li className="nav-item">
472 className="nav-link btn btn-link"
475 this.handleLogoutClick
479 <Icon icon="log-out" classes="mr-1" />
492 <ul className="navbar-nav my-2">
493 <li className="nav-item">
497 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
498 title={i18n.t("login")}
503 <li className="nav-item">
507 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
508 title={i18n.t("sign_up")}
521 get moderatesSomething(): boolean {
523 UserService.Instance.myUserInfo.map(m => m.moderates).unwrapOr([])
528 handleToggleExpandNavbar(i: Navbar) {
529 i.setState({ expanded: !i.state.expanded });
532 handleHideExpandNavbar(i: Navbar) {
533 i.setState({ expanded: false, showDropdown: false });
536 handleSearchParam(i: Navbar, event: any) {
537 i.setState({ searchParam: event.target.value });
540 handleSearchSubmit(i: Navbar, event: any) {
541 event.preventDefault();
545 handleSearchBtn(i: Navbar, event: any) {
546 event.preventDefault();
547 i.setState({ toggleSearch: true });
549 i.searchTextField.current.focus();
550 const offsetWidth = i.searchTextField.current.offsetWidth;
551 if (i.state.searchParam && offsetWidth > 100) {
556 handleSearchBlur(i: Navbar, event: any) {
557 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
558 i.setState({ toggleSearch: false });
562 handleLogoutClick(i: Navbar) {
563 i.setState({ showDropdown: false, expanded: false });
564 UserService.Instance.logout();
567 handleToggleDropdown(i: Navbar) {
568 i.setState({ showDropdown: !i.state.showDropdown });
571 parseMessage(msg: any) {
572 let op = wsUserOp(msg);
575 if (msg.error == "not_logged_in") {
576 UserService.Instance.logout();
579 } else if (msg.reconnect) {
580 console.log(i18n.t("websocket_reconnected"));
581 if (UserService.Instance.myUserInfo.isSome()) {
582 WebSocketService.Instance.send(
584 auth: auth().unwrap(),
589 } else if (op == UserOperation.GetUnreadCount) {
590 let data = wsJsonToRes<GetUnreadCountResponse>(
592 GetUnreadCountResponse
595 unreadInboxCount: data.replies + data.mentions + data.private_messages,
597 this.sendUnreadCount();
598 } else if (op == UserOperation.GetReportCount) {
599 let data = wsJsonToRes<GetReportCountResponse>(
601 GetReportCountResponse
606 data.comment_reports +
607 data.private_message_reports.unwrapOr(0),
609 this.sendReportUnread();
610 } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
611 let data = wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(
613 GetUnreadRegistrationApplicationCountResponse
615 this.setState({ unreadApplicationCount: data.registration_applications });
616 this.sendApplicationUnread();
617 } else if (op == UserOperation.CreateComment) {
618 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
620 UserService.Instance.myUserInfo.match({
622 if (data.recipient_ids.includes(mui.local_user_view.local_user.id)) {
624 unreadInboxCount: this.state.unreadInboxCount + 1,
626 this.sendUnreadCount();
627 notifyComment(data.comment_view, this.context.router);
632 } else if (op == UserOperation.CreatePrivateMessage) {
633 let data = wsJsonToRes<PrivateMessageResponse>(
635 PrivateMessageResponse
638 UserService.Instance.myUserInfo.match({
641 data.private_message_view.recipient.id ==
642 mui.local_user_view.person.id
645 unreadInboxCount: this.state.unreadInboxCount + 1,
647 this.sendUnreadCount();
648 notifyPrivateMessage(
649 data.private_message_view,
660 console.log("Fetching inbox unreads...");
662 let unreadForm = new GetUnreadCount({
663 auth: auth().unwrap(),
665 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
667 console.log("Fetching reports...");
669 let reportCountForm = new GetReportCount({
671 auth: auth().unwrap(),
673 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
676 console.log("Fetching applications...");
678 let applicationCountForm = new GetUnreadRegistrationApplicationCount({
679 auth: auth().unwrap(),
681 WebSocketService.Instance.send(
682 wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
687 get currentLocation() {
688 return this.context.router.history.location.pathname;
692 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
696 UserService.Instance.unreadReportCountSub.next(
697 this.state.unreadReportCount
701 sendApplicationUnread() {
702 UserService.Instance.unreadApplicationCountSub.next(
703 this.state.unreadApplicationCount
707 requestNotificationPermission() {
708 if (UserService.Instance.myUserInfo.isSome()) {
709 document.addEventListener("DOMContentLoaded", function () {
711 toast(i18n.t("notifications_error"), "danger");
715 if (Notification.permission !== "granted")
716 Notification.requestPermission();