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 className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3">
148 <div className="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 className="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 className="mx-1 badge badge-light">
183 {numToSI(this.state.unreadInboxCount)}
189 {this.moderatesSomething && (
190 <ul className="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 className="mx-1 badge badge-light">
204 {numToSI(this.state.unreadReportCount)}
212 <ul className="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 className="mx-1 badge badge-light">
228 {numToSI(this.state.unreadApplicationCount)}
238 className="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 className="navbar-nav my-2 mr-auto">
250 <li className="nav-item">
254 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
255 title={i18n.t("communities")}
257 {i18n.t("communities")}
260 <li className="nav-item">
261 {/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
264 pathname: "/create_post",
268 state: { prevPath: this.currentLocation },
271 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
272 title={i18n.t("create_post")}
274 {i18n.t("create_post")}
277 {this.canCreateCommunity && (
278 <li className="nav-item">
280 to="/create_community"
282 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
283 title={i18n.t("create_community")}
285 {i18n.t("create_community")}
289 <li className="nav-item">
292 title={i18n.t("support_lemmy")}
293 href={donateLemmyUrl}
295 <Icon icon="heart" classes="small" />
299 <ul className="navbar-nav my-2">
301 <li className="nav-item">
305 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
306 title={i18n.t("admin_settings")}
308 <Icon icon="settings" />
313 {!this.context.router.history.location.pathname.match(
317 className="form-inline mr-2"
318 onSubmit={linkEvent(this, this.handleSearchSubmit)}
322 className={`form-control mr-0 search-input ${
323 this.state.toggleSearch ? "show-input" : "hide-input"
325 onInput={linkEvent(this, this.handleSearchParam)}
326 value={this.state.searchParam}
327 ref={this.searchTextField}
329 placeholder={i18n.t("search")}
330 onBlur={linkEvent(this, this.handleSearchBlur)}
332 <label className="sr-only" htmlFor="search-input">
337 onClick={linkEvent(this, this.handleSearchBtn)}
338 className="px-1 btn btn-link"
339 style="color: var(--gray)"
340 aria-label={i18n.t("search")}
342 <Icon icon="search" />
346 {UserService.Instance.myUserInfo.isSome() ? (
348 <ul className="navbar-nav my-2">
349 <li className="nav-item">
353 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
354 title={i18n.t("unread_messages", {
355 count: this.state.unreadInboxCount,
356 formattedCount: numToSI(this.state.unreadInboxCount),
360 {this.state.unreadInboxCount > 0 && (
361 <span className="ml-1 badge badge-light">
362 {numToSI(this.state.unreadInboxCount)}
368 {this.moderatesSomething && (
369 <ul className="navbar-nav my-2">
370 <li className="nav-item">
374 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
375 title={i18n.t("unread_reports", {
376 count: this.state.unreadReportCount,
377 formattedCount: numToSI(this.state.unreadReportCount),
380 <Icon icon="shield" />
381 {this.state.unreadReportCount > 0 && (
382 <span className="ml-1 badge badge-light">
383 {numToSI(this.state.unreadReportCount)}
391 <ul className="navbar-nav my-2">
392 <li className="nav-item">
394 to="/registration_applications"
396 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
397 title={i18n.t("unread_registration_applications", {
398 count: this.state.unreadApplicationCount,
399 formattedCount: numToSI(
400 this.state.unreadApplicationCount
404 <Icon icon="clipboard" />
405 {this.state.unreadApplicationCount > 0 && (
406 <span className="mx-1 badge badge-light">
407 {numToSI(this.state.unreadApplicationCount)}
414 {UserService.Instance.myUserInfo
415 .map(m => m.local_user_view.person)
418 <ul className="navbar-nav">
419 <li className="nav-item dropdown">
421 className="nav-link btn btn-link dropdown-toggle"
422 onClick={linkEvent(this, this.handleToggleDropdown)}
425 aria-expanded="false"
429 person.avatar.match({
431 <PictrsImage src={avatar} icon />
435 {person.display_name.unwrapOr(person.name)}
438 {this.state.showDropdown && (
440 className="dropdown-content"
441 onMouseLeave={linkEvent(
443 this.handleToggleDropdown
446 <li className="nav-item">
448 to={`/u/${person.name}`}
450 title={i18n.t("profile")}
452 <Icon icon="user" classes="mr-1" />
456 <li className="nav-item">
460 title={i18n.t("settings")}
462 <Icon icon="settings" classes="mr-1" />
467 <hr className="dropdown-divider" />
469 <li className="nav-item">
471 className="nav-link btn btn-link"
474 this.handleLogoutClick
478 <Icon icon="log-out" classes="mr-1" />
491 <ul className="navbar-nav my-2">
492 <li className="nav-item">
496 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
497 title={i18n.t("login")}
502 <li className="nav-item">
506 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
507 title={i18n.t("sign_up")}
520 get moderatesSomething(): boolean {
522 UserService.Instance.myUserInfo.map(m => m.moderates).unwrapOr([])
527 get amAdmin(): boolean {
528 return amAdmin(Some(this.props.siteRes.admins));
531 get canCreateCommunity(): boolean {
532 let adminOnly = this.props.siteRes.site_view
533 .map(s => s.site.community_creation_admin_only)
535 return !adminOnly || this.amAdmin;
538 handleToggleExpandNavbar(i: Navbar) {
539 i.setState({ expanded: !i.state.expanded });
542 handleHideExpandNavbar(i: Navbar) {
543 i.setState({ expanded: false, showDropdown: false });
546 handleSearchParam(i: Navbar, event: any) {
547 i.setState({ searchParam: event.target.value });
550 handleSearchSubmit(i: Navbar, event: any) {
551 event.preventDefault();
555 handleSearchBtn(i: Navbar, event: any) {
556 event.preventDefault();
557 i.setState({ toggleSearch: true });
559 i.searchTextField.current.focus();
560 const offsetWidth = i.searchTextField.current.offsetWidth;
561 if (i.state.searchParam && offsetWidth > 100) {
566 handleSearchBlur(i: Navbar, event: any) {
567 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
568 i.setState({ toggleSearch: false });
572 handleLogoutClick(i: Navbar) {
573 i.setState({ showDropdown: false, expanded: false });
574 UserService.Instance.logout();
577 handleToggleDropdown(i: Navbar) {
578 i.setState({ 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
605 unreadInboxCount: data.replies + data.mentions + data.private_messages,
607 this.sendUnreadCount();
608 } else if (op == UserOperation.GetReportCount) {
609 let data = wsJsonToRes<GetReportCountResponse>(
611 GetReportCountResponse
614 unreadReportCount: data.post_reports + data.comment_reports,
616 this.sendReportUnread();
617 } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
618 let data = wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(
620 GetUnreadRegistrationApplicationCountResponse
622 this.setState({ unreadApplicationCount: data.registration_applications });
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)) {
631 unreadInboxCount: this.state.unreadInboxCount + 1,
633 this.sendUnreadCount();
634 notifyComment(data.comment_view, this.context.router);
639 } else if (op == UserOperation.CreatePrivateMessage) {
640 let data = wsJsonToRes<PrivateMessageResponse>(
642 PrivateMessageResponse
645 UserService.Instance.myUserInfo.match({
648 data.private_message_view.recipient.id ==
649 mui.local_user_view.person.id
652 unreadInboxCount: this.state.unreadInboxCount + 1,
654 this.sendUnreadCount();
655 notifyPrivateMessage(
656 data.private_message_view,
667 console.log("Fetching inbox unreads...");
669 let unreadForm = new GetUnreadCount({
670 auth: auth().unwrap(),
672 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
674 console.log("Fetching reports...");
676 let reportCountForm = new GetReportCount({
678 auth: auth().unwrap(),
680 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
683 console.log("Fetching applications...");
685 let applicationCountForm = new GetUnreadRegistrationApplicationCount({
686 auth: auth().unwrap(),
688 WebSocketService.Instance.send(
689 wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
694 get currentLocation() {
695 return this.context.router.history.location.pathname;
699 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
703 UserService.Instance.unreadReportCountSub.next(
704 this.state.unreadReportCount
708 sendApplicationUnread() {
709 UserService.Instance.unreadApplicationCountSub.next(
710 this.state.unreadApplicationCount
714 requestNotificationPermission() {
715 if (UserService.Instance.myUserInfo.isSome()) {
716 document.addEventListener("DOMContentLoaded", function () {
718 toast(i18n.t("notifications_error"), "danger");
722 if (Notification.permission !== "granted")
723 Notification.requestPermission();