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";
36 import { Icon } from "../common/icon";
37 import { PictrsImage } from "../common/pictrs-image";
39 interface NavbarProps {
40 siteRes: GetSiteResponse;
43 interface NavbarState {
45 unreadInboxCount: number;
46 unreadReportCount: number;
47 unreadApplicationCount: number;
49 toggleSearch: boolean;
50 showDropdown: boolean;
51 onSiteBanner?(url: string): any;
54 export class Navbar extends Component<NavbarProps, NavbarState> {
55 private wsSub: Subscription;
56 private userSub: Subscription;
57 private unreadInboxCountSub: Subscription;
58 private unreadReportCountSub: Subscription;
59 private unreadApplicationCountSub: Subscription;
60 private searchTextField: RefObject<HTMLInputElement>;
61 emptyState: NavbarState = {
64 unreadApplicationCount: 0,
72 constructor(props: any, context: any) {
73 super(props, context);
74 this.state = this.emptyState;
76 this.parseMessage = this.parseMessage.bind(this);
77 this.subscription = wsSubscribe(this.parseMessage);
81 // Subscribe to jwt changes
83 this.searchTextField = createRef();
85 // On the first load, check the unreads
86 if (UserService.Instance.myUserInfo.isSome()) {
87 this.requestNotificationPermission();
88 WebSocketService.Instance.send(
90 auth: auth().unwrap(),
96 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
99 this.requestNotificationPermission();
100 WebSocketService.Instance.send(
101 wsClient.getSite({ auth: res.map(r => r.jwt) })
106 // Subscribe to unread count changes
107 this.unreadInboxCountSub =
108 UserService.Instance.unreadInboxCountSub.subscribe(res => {
109 this.setState({ unreadInboxCount: res });
111 // Subscribe to unread report count changes
112 this.unreadReportCountSub =
113 UserService.Instance.unreadReportCountSub.subscribe(res => {
114 this.setState({ unreadReportCount: res });
116 // Subscribe to unread application count
117 this.unreadApplicationCountSub =
118 UserService.Instance.unreadApplicationCountSub.subscribe(res => {
119 this.setState({ unreadApplicationCount: res });
124 componentWillUnmount() {
125 this.wsSub.unsubscribe();
126 this.userSub.unsubscribe();
127 this.unreadInboxCountSub.unsubscribe();
128 this.unreadReportCountSub.unsubscribe();
129 this.unreadApplicationCountSub.unsubscribe();
133 const searchParam = this.state.searchParam;
134 this.setState({ searchParam: "" });
135 this.setState({ toggleSearch: false });
136 this.setState({ showDropdown: false, expanded: false });
137 if (searchParam === "") {
138 this.context.router.history.push(`/search/`);
140 const searchParamEncoded = encodeURIComponent(searchParam);
141 this.context.router.history.push(
142 `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
148 return this.navbar();
151 // TODO class active corresponding to current page
154 <nav class="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3">
155 <div class="container">
156 {this.props.siteRes.site_view.match({
160 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
161 title={siteView.site.description.unwrapOr(siteView.site.name)}
162 className="d-flex align-items-center navbar-brand mr-md-3"
164 {siteView.site.icon.match({
166 showAvatars() && <PictrsImage src={icon} icon />,
174 {UserService.Instance.myUserInfo.isSome() && (
176 <ul class="navbar-nav ml-auto">
177 <li className="nav-item">
180 className="p-1 navbar-toggler nav-link border-0"
181 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
182 title={i18n.t("unread_messages", {
183 count: this.state.unreadInboxCount,
184 formattedCount: numToSI(this.state.unreadInboxCount),
188 {this.state.unreadInboxCount > 0 && (
189 <span class="mx-1 badge badge-light">
190 {numToSI(this.state.unreadInboxCount)}
196 {this.moderatesSomething && (
197 <ul class="navbar-nav ml-1">
198 <li className="nav-item">
201 className="p-1 navbar-toggler nav-link border-0"
202 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
203 title={i18n.t("unread_reports", {
204 count: this.state.unreadReportCount,
205 formattedCount: numToSI(this.state.unreadReportCount),
208 <Icon icon="shield" />
209 {this.state.unreadReportCount > 0 && (
210 <span class="mx-1 badge badge-light">
211 {numToSI(this.state.unreadReportCount)}
219 <ul class="navbar-nav ml-1">
220 <li className="nav-item">
222 to="/registration_applications"
223 className="p-1 navbar-toggler nav-link border-0"
224 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
225 title={i18n.t("unread_registration_applications", {
226 count: this.state.unreadApplicationCount,
227 formattedCount: numToSI(
228 this.state.unreadApplicationCount
232 <Icon icon="clipboard" />
233 {this.state.unreadApplicationCount > 0 && (
234 <span class="mx-1 badge badge-light">
235 {numToSI(this.state.unreadApplicationCount)}
245 class="navbar-toggler border-0 p-1"
248 onClick={linkEvent(this, this.handleToggleExpandNavbar)}
249 data-tippy-content={i18n.t("expand_here")}
254 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
256 <ul class="navbar-nav my-2 mr-auto">
257 <li class="nav-item">
261 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
262 title={i18n.t("communities")}
264 {i18n.t("communities")}
267 <li class="nav-item">
270 pathname: "/create_post",
271 prevPath: this.currentLocation,
274 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
275 title={i18n.t("create_post")}
277 {i18n.t("create_post")}
280 {this.canCreateCommunity && (
281 <li class="nav-item">
283 to="/create_community"
285 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
286 title={i18n.t("create_community")}
288 {i18n.t("create_community")}
292 <li class="nav-item">
295 title={i18n.t("support_lemmy")}
296 href={donateLemmyUrl}
298 <Icon icon="heart" classes="small" />
302 <ul class="navbar-nav my-2">
304 <li className="nav-item">
308 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
309 title={i18n.t("admin_settings")}
311 <Icon icon="settings" />
316 {!this.context.router.history.location.pathname.match(
320 class="form-inline mr-2"
321 onSubmit={linkEvent(this, this.handleSearchSubmit)}
325 class={`form-control mr-0 search-input ${
326 this.state.toggleSearch ? "show-input" : "hide-input"
328 onInput={linkEvent(this, this.handleSearchParam)}
329 value={this.state.searchParam}
330 ref={this.searchTextField}
332 placeholder={i18n.t("search")}
333 onBlur={linkEvent(this, this.handleSearchBlur)}
335 <label class="sr-only" htmlFor="search-input">
340 onClick={linkEvent(this, this.handleSearchBtn)}
341 class="px-1 btn btn-link"
342 style="color: var(--gray)"
343 aria-label={i18n.t("search")}
345 <Icon icon="search" />
349 {UserService.Instance.myUserInfo.isSome() ? (
351 <ul class="navbar-nav my-2">
352 <li className="nav-item">
356 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
357 title={i18n.t("unread_messages", {
358 count: this.state.unreadInboxCount,
359 formattedCount: numToSI(this.state.unreadInboxCount),
363 {this.state.unreadInboxCount > 0 && (
364 <span class="ml-1 badge badge-light">
365 {numToSI(this.state.unreadInboxCount)}
371 {this.moderatesSomething && (
372 <ul class="navbar-nav my-2">
373 <li className="nav-item">
377 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
378 title={i18n.t("unread_reports", {
379 count: this.state.unreadReportCount,
380 formattedCount: numToSI(this.state.unreadReportCount),
383 <Icon icon="shield" />
384 {this.state.unreadReportCount > 0 && (
385 <span class="ml-1 badge badge-light">
386 {numToSI(this.state.unreadReportCount)}
394 <ul class="navbar-nav my-2">
395 <li className="nav-item">
397 to="/registration_applications"
399 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
400 title={i18n.t("unread_registration_applications", {
401 count: this.state.unreadApplicationCount,
402 formattedCount: numToSI(
403 this.state.unreadApplicationCount
407 <Icon icon="clipboard" />
408 {this.state.unreadApplicationCount > 0 && (
409 <span class="mx-1 badge badge-light">
410 {numToSI(this.state.unreadApplicationCount)}
417 {UserService.Instance.myUserInfo
418 .map(m => m.local_user_view.person)
421 <ul class="navbar-nav">
422 <li class="nav-item dropdown">
424 class="nav-link btn btn-link dropdown-toggle"
425 onClick={linkEvent(this, this.handleToggleDropdown)}
428 aria-expanded="false"
432 person.avatar.match({
434 <PictrsImage src={avatar} icon />
438 {person.display_name.unwrapOr(person.name)}
441 {this.state.showDropdown && (
443 class="dropdown-content"
444 onMouseLeave={linkEvent(
446 this.handleToggleDropdown
449 <li className="nav-item">
451 to={`/u/${person.name}`}
453 title={i18n.t("profile")}
455 <Icon icon="user" classes="mr-1" />
459 <li className="nav-item">
463 title={i18n.t("settings")}
465 <Icon icon="settings" classes="mr-1" />
470 <hr class="dropdown-divider" />
472 <li className="nav-item">
474 className="nav-link btn btn-link"
477 this.handleLogoutClick
481 <Icon icon="log-out" classes="mr-1" />
494 <ul class="navbar-nav my-2">
495 <li className="nav-item">
499 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
500 title={i18n.t("login")}
505 <li className="nav-item">
509 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
510 title={i18n.t("sign_up")}
523 get moderatesSomething(): boolean {
525 UserService.Instance.myUserInfo.map(m => m.moderates).unwrapOr([])
530 get amAdmin(): boolean {
531 return amAdmin(Some(this.props.siteRes.admins));
534 get canCreateCommunity(): boolean {
535 let adminOnly = this.props.siteRes.site_view
536 .map(s => s.site.community_creation_admin_only)
538 return !adminOnly || this.amAdmin;
541 handleToggleExpandNavbar(i: Navbar) {
542 i.state.expanded = !i.state.expanded;
546 handleHideExpandNavbar(i: Navbar) {
547 i.setState({ expanded: false, showDropdown: false });
550 handleSearchParam(i: Navbar, event: any) {
551 i.state.searchParam = event.target.value;
555 handleSearchSubmit(i: Navbar, event: any) {
556 event.preventDefault();
560 handleSearchBtn(i: Navbar, event: any) {
561 event.preventDefault();
562 i.setState({ toggleSearch: true });
564 i.searchTextField.current.focus();
565 const offsetWidth = i.searchTextField.current.offsetWidth;
566 if (i.state.searchParam && offsetWidth > 100) {
571 handleSearchBlur(i: Navbar, event: any) {
572 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
573 i.state.toggleSearch = false;
578 handleLogoutClick(i: Navbar) {
579 i.setState({ showDropdown: false, expanded: false });
580 UserService.Instance.logout();
583 handleToggleDropdown(i: Navbar) {
584 i.state.showDropdown = !i.state.showDropdown;
588 parseMessage(msg: any) {
589 let op = wsUserOp(msg);
592 if (msg.error == "not_logged_in") {
593 UserService.Instance.logout();
596 } else if (msg.reconnect) {
597 console.log(i18n.t("websocket_reconnected"));
598 if (UserService.Instance.myUserInfo.isSome()) {
599 WebSocketService.Instance.send(
601 auth: auth().unwrap(),
606 } else if (op == UserOperation.GetUnreadCount) {
607 let data = wsJsonToRes<GetUnreadCountResponse>(
609 GetUnreadCountResponse
611 this.state.unreadInboxCount =
612 data.replies + data.mentions + data.private_messages;
613 this.setState(this.state);
614 this.sendUnreadCount();
615 } else if (op == UserOperation.GetReportCount) {
616 let data = wsJsonToRes<GetReportCountResponse>(
618 GetReportCountResponse
620 this.state.unreadReportCount = data.post_reports + data.comment_reports;
621 this.setState(this.state);
622 this.sendReportUnread();
623 } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
624 let data = wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(
626 GetUnreadRegistrationApplicationCountResponse
628 this.state.unreadApplicationCount = data.registration_applications;
629 this.setState(this.state);
630 this.sendApplicationUnread();
631 } else if (op == UserOperation.GetSite) {
632 // This is only called on a successful login
633 let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
634 UserService.Instance.myUserInfo = data.my_user;
635 UserService.Instance.myUserInfo.match({
637 setTheme(mui.local_user_view.local_user.theme);
638 i18n.changeLanguage(getLanguages()[0]);
639 this.setState(this.state);
643 } else if (op == UserOperation.CreateComment) {
644 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
646 UserService.Instance.myUserInfo.match({
648 if (data.recipient_ids.includes(mui.local_user_view.local_user.id)) {
649 this.state.unreadInboxCount++;
650 this.setState(this.state);
651 this.sendUnreadCount();
652 notifyComment(data.comment_view, this.context.router);
657 } else if (op == UserOperation.CreatePrivateMessage) {
658 let data = wsJsonToRes<PrivateMessageResponse>(
660 PrivateMessageResponse
663 UserService.Instance.myUserInfo.match({
666 data.private_message_view.recipient.id ==
667 mui.local_user_view.person.id
669 this.state.unreadInboxCount++;
670 this.setState(this.state);
671 this.sendUnreadCount();
672 notifyPrivateMessage(
673 data.private_message_view,
684 console.log("Fetching inbox unreads...");
686 let unreadForm = new GetUnreadCount({
687 auth: auth().unwrap(),
689 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
691 console.log("Fetching reports...");
693 let reportCountForm = new GetReportCount({
695 auth: auth().unwrap(),
697 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
700 console.log("Fetching applications...");
702 let applicationCountForm = new GetUnreadRegistrationApplicationCount({
703 auth: auth().unwrap(),
705 WebSocketService.Instance.send(
706 wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
711 get currentLocation() {
712 return this.context.router.history.location.pathname;
716 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
720 UserService.Instance.unreadReportCountSub.next(
721 this.state.unreadReportCount
725 sendApplicationUnread() {
726 UserService.Instance.unreadApplicationCountSub.next(
727 this.state.unreadApplicationCount
731 requestNotificationPermission() {
732 if (UserService.Instance.myUserInfo.isSome()) {
733 document.addEventListener("DOMContentLoaded", function () {
735 toast(i18n.t("notifications_error"), "danger");
739 if (Notification.permission !== "granted")
740 Notification.requestPermission();