1 import { None, Some } 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";
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 toggleSearch: boolean;
48 showDropdown: boolean;
49 onSiteBanner?(url: string): any;
52 export class Navbar extends Component<NavbarProps, NavbarState> {
53 private wsSub: Subscription;
54 private userSub: Subscription;
55 private unreadInboxCountSub: Subscription;
56 private unreadReportCountSub: Subscription;
57 private unreadApplicationCountSub: Subscription;
58 private searchTextField: RefObject<HTMLInputElement>;
59 emptyState: NavbarState = {
62 unreadApplicationCount: 0,
70 constructor(props: any, context: any) {
71 super(props, context);
72 this.state = this.emptyState;
74 this.parseMessage = this.parseMessage.bind(this);
75 this.subscription = wsSubscribe(this.parseMessage);
79 // Subscribe to jwt changes
81 this.searchTextField = createRef();
83 // On the first load, check the unreads
84 if (UserService.Instance.myUserInfo.isSome()) {
85 this.requestNotificationPermission();
86 WebSocketService.Instance.send(
88 auth: auth().unwrap(),
92 if (this.props.siteRes.site_view.isSome()) {
97 this.requestNotificationPermission();
99 // Subscribe to unread count changes
100 this.unreadInboxCountSub =
101 UserService.Instance.unreadInboxCountSub.subscribe(res => {
102 this.setState({ unreadInboxCount: res });
104 // Subscribe to unread report count changes
105 this.unreadReportCountSub =
106 UserService.Instance.unreadReportCountSub.subscribe(res => {
107 this.setState({ unreadReportCount: res });
109 // Subscribe to unread application count
110 this.unreadApplicationCountSub =
111 UserService.Instance.unreadApplicationCountSub.subscribe(res => {
112 this.setState({ unreadApplicationCount: res });
117 componentWillUnmount() {
118 this.wsSub.unsubscribe();
119 this.userSub.unsubscribe();
120 this.unreadInboxCountSub.unsubscribe();
121 this.unreadReportCountSub.unsubscribe();
122 this.unreadApplicationCountSub.unsubscribe();
126 const searchParam = this.state.searchParam;
127 this.setState({ searchParam: "" });
128 this.setState({ toggleSearch: false });
129 this.setState({ showDropdown: false, expanded: false });
130 if (searchParam === "") {
131 this.context.router.history.push(`/search/`);
133 const searchParamEncoded = encodeURIComponent(searchParam);
134 this.context.router.history.push(
135 `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
141 return this.navbar();
144 // TODO class active corresponding to current page
147 <nav class="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3">
148 <div class="container">
149 {this.props.siteRes.site_view.match({
153 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
154 title={siteView.site.description.unwrapOr(siteView.site.name)}
155 className="d-flex align-items-center navbar-brand mr-md-3"
157 {siteView.site.icon.match({
159 showAvatars() && <PictrsImage src={icon} icon />,
167 {UserService.Instance.myUserInfo.isSome() && (
169 <ul class="navbar-nav ml-auto">
170 <li className="nav-item">
173 className="p-1 navbar-toggler nav-link border-0"
174 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
175 title={i18n.t("unread_messages", {
176 count: this.state.unreadInboxCount,
177 formattedCount: numToSI(this.state.unreadInboxCount),
181 {this.state.unreadInboxCount > 0 && (
182 <span class="mx-1 badge badge-light">
183 {numToSI(this.state.unreadInboxCount)}
189 {this.moderatesSomething && (
190 <ul class="navbar-nav ml-1">
191 <li className="nav-item">
194 className="p-1 navbar-toggler nav-link border-0"
195 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
196 title={i18n.t("unread_reports", {
197 count: this.state.unreadReportCount,
198 formattedCount: numToSI(this.state.unreadReportCount),
201 <Icon icon="shield" />
202 {this.state.unreadReportCount > 0 && (
203 <span class="mx-1 badge badge-light">
204 {numToSI(this.state.unreadReportCount)}
212 <ul class="navbar-nav ml-1">
213 <li className="nav-item">
215 to="/registration_applications"
216 className="p-1 navbar-toggler nav-link border-0"
217 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
218 title={i18n.t("unread_registration_applications", {
219 count: this.state.unreadApplicationCount,
220 formattedCount: numToSI(
221 this.state.unreadApplicationCount
225 <Icon icon="clipboard" />
226 {this.state.unreadApplicationCount > 0 && (
227 <span class="mx-1 badge badge-light">
228 {numToSI(this.state.unreadApplicationCount)}
238 class="navbar-toggler border-0 p-1"
241 onClick={linkEvent(this, this.handleToggleExpandNavbar)}
242 data-tippy-content={i18n.t("expand_here")}
247 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
249 <ul class="navbar-nav my-2 mr-auto">
250 <li class="nav-item">
254 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
255 title={i18n.t("communities")}
257 {i18n.t("communities")}
260 <li class="nav-item">
263 pathname: "/create_post",
264 prevPath: this.currentLocation,
267 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
268 title={i18n.t("create_post")}
270 {i18n.t("create_post")}
273 {this.canCreateCommunity && (
274 <li class="nav-item">
276 to="/create_community"
278 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
279 title={i18n.t("create_community")}
281 {i18n.t("create_community")}
285 <li class="nav-item">
288 title={i18n.t("support_lemmy")}
289 href={donateLemmyUrl}
291 <Icon icon="heart" classes="small" />
295 <ul class="navbar-nav my-2">
297 <li className="nav-item">
301 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
302 title={i18n.t("admin_settings")}
304 <Icon icon="settings" />
309 {!this.context.router.history.location.pathname.match(
313 class="form-inline mr-2"
314 onSubmit={linkEvent(this, this.handleSearchSubmit)}
318 class={`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}
325 placeholder={i18n.t("search")}
326 onBlur={linkEvent(this, this.handleSearchBlur)}
328 <label class="sr-only" htmlFor="search-input">
333 onClick={linkEvent(this, this.handleSearchBtn)}
334 class="px-1 btn btn-link"
335 style="color: var(--gray)"
336 aria-label={i18n.t("search")}
338 <Icon icon="search" />
342 {UserService.Instance.myUserInfo.isSome() ? (
344 <ul class="navbar-nav my-2">
345 <li className="nav-item">
349 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
350 title={i18n.t("unread_messages", {
351 count: this.state.unreadInboxCount,
352 formattedCount: numToSI(this.state.unreadInboxCount),
356 {this.state.unreadInboxCount > 0 && (
357 <span class="ml-1 badge badge-light">
358 {numToSI(this.state.unreadInboxCount)}
364 {this.moderatesSomething && (
365 <ul class="navbar-nav my-2">
366 <li className="nav-item">
370 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
371 title={i18n.t("unread_reports", {
372 count: this.state.unreadReportCount,
373 formattedCount: numToSI(this.state.unreadReportCount),
376 <Icon icon="shield" />
377 {this.state.unreadReportCount > 0 && (
378 <span class="ml-1 badge badge-light">
379 {numToSI(this.state.unreadReportCount)}
387 <ul class="navbar-nav my-2">
388 <li className="nav-item">
390 to="/registration_applications"
392 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
393 title={i18n.t("unread_registration_applications", {
394 count: this.state.unreadApplicationCount,
395 formattedCount: numToSI(
396 this.state.unreadApplicationCount
400 <Icon icon="clipboard" />
401 {this.state.unreadApplicationCount > 0 && (
402 <span class="mx-1 badge badge-light">
403 {numToSI(this.state.unreadApplicationCount)}
410 {UserService.Instance.myUserInfo
411 .map(m => m.local_user_view.person)
414 <ul class="navbar-nav">
415 <li class="nav-item dropdown">
417 class="nav-link btn btn-link dropdown-toggle"
418 onClick={linkEvent(this, this.handleToggleDropdown)}
421 aria-expanded="false"
425 person.avatar.match({
427 <PictrsImage src={avatar} icon />
431 {person.display_name.unwrapOr(person.name)}
434 {this.state.showDropdown && (
436 class="dropdown-content"
437 onMouseLeave={linkEvent(
439 this.handleToggleDropdown
442 <li className="nav-item">
444 to={`/u/${person.name}`}
446 title={i18n.t("profile")}
448 <Icon icon="user" classes="mr-1" />
452 <li className="nav-item">
456 title={i18n.t("settings")}
458 <Icon icon="settings" classes="mr-1" />
463 <hr class="dropdown-divider" />
465 <li className="nav-item">
467 className="nav-link btn btn-link"
470 this.handleLogoutClick
474 <Icon icon="log-out" classes="mr-1" />
487 <ul class="navbar-nav my-2">
488 <li className="nav-item">
492 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
493 title={i18n.t("login")}
498 <li className="nav-item">
502 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
503 title={i18n.t("sign_up")}
516 get moderatesSomething(): boolean {
518 UserService.Instance.myUserInfo.map(m => m.moderates).unwrapOr([])
523 get amAdmin(): boolean {
524 return amAdmin(Some(this.props.siteRes.admins));
527 get canCreateCommunity(): boolean {
528 let adminOnly = this.props.siteRes.site_view
529 .map(s => s.site.community_creation_admin_only)
531 return !adminOnly || this.amAdmin;
534 handleToggleExpandNavbar(i: Navbar) {
535 i.state.expanded = !i.state.expanded;
539 handleHideExpandNavbar(i: Navbar) {
540 i.setState({ expanded: false, showDropdown: false });
543 handleSearchParam(i: Navbar, event: any) {
544 i.state.searchParam = event.target.value;
548 handleSearchSubmit(i: Navbar, event: any) {
549 event.preventDefault();
553 handleSearchBtn(i: Navbar, event: any) {
554 event.preventDefault();
555 i.setState({ toggleSearch: true });
557 i.searchTextField.current.focus();
558 const offsetWidth = i.searchTextField.current.offsetWidth;
559 if (i.state.searchParam && offsetWidth > 100) {
564 handleSearchBlur(i: Navbar, event: any) {
565 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
566 i.state.toggleSearch = false;
571 handleLogoutClick(i: Navbar) {
572 i.setState({ showDropdown: false, expanded: false });
573 UserService.Instance.logout();
576 handleToggleDropdown(i: Navbar) {
577 i.state.showDropdown = !i.state.showDropdown;
581 parseMessage(msg: any) {
582 let op = wsUserOp(msg);
585 if (msg.error == "not_logged_in") {
586 UserService.Instance.logout();
589 } else if (msg.reconnect) {
590 console.log(i18n.t("websocket_reconnected"));
591 if (UserService.Instance.myUserInfo.isSome()) {
592 WebSocketService.Instance.send(
594 auth: auth().unwrap(),
599 } else if (op == UserOperation.GetUnreadCount) {
600 let data = wsJsonToRes<GetUnreadCountResponse>(
602 GetUnreadCountResponse
604 this.state.unreadInboxCount =
605 data.replies + data.mentions + data.private_messages;
606 this.setState(this.state);
607 this.sendUnreadCount();
608 } else if (op == UserOperation.GetReportCount) {
609 let data = wsJsonToRes<GetReportCountResponse>(
611 GetReportCountResponse
613 this.state.unreadReportCount = data.post_reports + data.comment_reports;
614 this.setState(this.state);
615 this.sendReportUnread();
616 } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
617 let data = wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(
619 GetUnreadRegistrationApplicationCountResponse
621 this.state.unreadApplicationCount = data.registration_applications;
622 this.setState(this.state);
623 this.sendApplicationUnread();
624 } else if (op == UserOperation.CreateComment) {
625 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
627 UserService.Instance.myUserInfo.match({
629 if (data.recipient_ids.includes(mui.local_user_view.local_user.id)) {
630 this.state.unreadInboxCount++;
631 this.setState(this.state);
632 this.sendUnreadCount();
633 notifyComment(data.comment_view, this.context.router);
638 } else if (op == UserOperation.CreatePrivateMessage) {
639 let data = wsJsonToRes<PrivateMessageResponse>(
641 PrivateMessageResponse
644 UserService.Instance.myUserInfo.match({
647 data.private_message_view.recipient.id ==
648 mui.local_user_view.person.id
650 this.state.unreadInboxCount++;
651 this.setState(this.state);
652 this.sendUnreadCount();
653 notifyPrivateMessage(
654 data.private_message_view,
665 console.log("Fetching inbox unreads...");
667 let unreadForm = new GetUnreadCount({
668 auth: auth().unwrap(),
670 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
672 console.log("Fetching reports...");
674 let reportCountForm = new GetReportCount({
676 auth: auth().unwrap(),
678 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
681 console.log("Fetching applications...");
683 let applicationCountForm = new GetUnreadRegistrationApplicationCount({
684 auth: auth().unwrap(),
686 WebSocketService.Instance.send(
687 wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
692 get currentLocation() {
693 return this.context.router.history.location.pathname;
697 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
701 UserService.Instance.unreadReportCountSub.next(
702 this.state.unreadReportCount
706 sendApplicationUnread() {
707 UserService.Instance.unreadApplicationCountSub.next(
708 this.state.unreadApplicationCount
712 requestNotificationPermission() {
713 if (UserService.Instance.myUserInfo.isSome()) {
714 document.addEventListener("DOMContentLoaded", function () {
716 toast(i18n.t("notifications_error"), "danger");
720 if (Notification.permission !== "granted")
721 Notification.requestPermission();