1 import { Component, createRef, linkEvent, RefObject } from "inferno";
2 import { NavLink } from "inferno-router";
6 GetReportCountResponse,
9 GetUnreadCountResponse,
10 GetUnreadRegistrationApplicationCount,
11 GetUnreadRegistrationApplicationCountResponse,
12 PrivateMessageResponse,
14 } from "lemmy-js-client";
15 import { Subscription } from "rxjs";
16 import { i18n } from "../../i18next";
17 import { UserService, WebSocketService } from "../../services";
34 import { Icon } from "../common/icon";
35 import { PictrsImage } from "../common/pictrs-image";
37 interface NavbarProps {
38 site_res: GetSiteResponse;
41 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 = {
61 isLoggedIn: !!this.props.site_res.my_user,
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();
84 console.log(`isLoggedIn = ${this.state.isLoggedIn}`);
86 // On the first load, check the unreads
87 if (this.state.isLoggedIn == false) {
88 // setTheme(data.my_user.theme, true);
89 // i18n.changeLanguage(getLanguage());
90 // i18n.changeLanguage('de');
92 this.requestNotificationPermission();
93 WebSocketService.Instance.send(
101 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
103 if (res !== undefined) {
104 this.requestNotificationPermission();
105 WebSocketService.Instance.send(
106 wsClient.getSite({ auth: authField() })
109 this.setState({ isLoggedIn: false });
113 // Subscribe to unread count changes
114 this.unreadInboxCountSub =
115 UserService.Instance.unreadInboxCountSub.subscribe(res => {
116 this.setState({ unreadInboxCount: res });
118 // Subscribe to unread report count changes
119 this.unreadReportCountSub =
120 UserService.Instance.unreadReportCountSub.subscribe(res => {
121 this.setState({ unreadReportCount: res });
123 // Subscribe to unread application count
124 this.unreadApplicationCountSub =
125 UserService.Instance.unreadApplicationCountSub.subscribe(res => {
126 this.setState({ unreadApplicationCount: res });
131 componentWillUnmount() {
132 this.wsSub.unsubscribe();
133 this.userSub.unsubscribe();
134 this.unreadInboxCountSub.unsubscribe();
135 this.unreadReportCountSub.unsubscribe();
136 this.unreadApplicationCountSub.unsubscribe();
140 const searchParam = this.state.searchParam;
141 this.setState({ searchParam: "" });
142 this.setState({ toggleSearch: false });
143 this.setState({ showDropdown: false, expanded: false });
144 if (searchParam === "") {
145 this.context.router.history.push(`/search/`);
147 const searchParamEncoded = encodeURIComponent(searchParam);
148 this.context.router.history.push(
149 `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
155 return this.navbar();
158 // TODO class active corresponding to current page
161 UserService.Instance.myUserInfo || this.props.site_res.my_user;
162 let person = myUserInfo?.local_user_view.person;
164 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
165 <div class="container">
166 {this.props.site_res.site_view && (
169 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
171 this.props.site_res.site_view.site.description ||
172 this.props.site_res.site_view.site.name
174 className="d-flex align-items-center navbar-brand mr-md-3"
176 {this.props.site_res.site_view.site.icon && showAvatars() && (
178 src={this.props.site_res.site_view.site.icon}
182 {this.props.site_res.site_view.site.name}
185 {this.state.isLoggedIn && (
187 <ul class="navbar-nav ml-auto">
188 <li className="nav-item">
191 className="p-1 navbar-toggler nav-link border-0"
192 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
193 title={i18n.t("unread_messages", {
194 count: this.state.unreadInboxCount,
195 formattedCount: numToSI(this.state.unreadInboxCount),
199 {this.state.unreadInboxCount > 0 && (
200 <span class="mx-1 badge badge-light">
201 {numToSI(this.state.unreadInboxCount)}
207 {UserService.Instance.myUserInfo?.moderates.length > 0 && (
208 <ul class="navbar-nav ml-1">
209 <li className="nav-item">
212 className="p-1 navbar-toggler nav-link border-0"
213 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
214 title={i18n.t("unread_reports", {
215 count: this.state.unreadReportCount,
216 formattedCount: numToSI(this.state.unreadReportCount),
219 <Icon icon="shield" />
220 {this.state.unreadReportCount > 0 && (
221 <span class="mx-1 badge badge-light">
222 {numToSI(this.state.unreadReportCount)}
229 {UserService.Instance.myUserInfo?.local_user_view.person
231 <ul class="navbar-nav ml-1">
232 <li className="nav-item">
234 to="/registration_applications"
235 className="p-1 navbar-toggler nav-link border-0"
236 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
237 title={i18n.t("unread_registration_applications", {
238 count: this.state.unreadApplicationCount,
239 formattedCount: numToSI(
240 this.state.unreadApplicationCount
244 <Icon icon="clipboard" />
245 {this.state.unreadApplicationCount > 0 && (
246 <span class="mx-1 badge badge-light">
247 {numToSI(this.state.unreadApplicationCount)}
257 class="navbar-toggler border-0 p-1"
260 onClick={linkEvent(this, this.handleToggleExpandNavbar)}
261 data-tippy-content={i18n.t("expand_here")}
266 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
268 <ul class="navbar-nav my-2 mr-auto">
269 <li class="nav-item">
273 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
274 title={i18n.t("communities")}
276 {i18n.t("communities")}
279 <li class="nav-item">
282 pathname: "/create_post",
283 prevPath: this.currentLocation,
286 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
287 title={i18n.t("create_post")}
289 {i18n.t("create_post")}
292 {this.canCreateCommunity && (
293 <li class="nav-item">
295 to="/create_community"
297 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
298 title={i18n.t("create_community")}
300 {i18n.t("create_community")}
304 <li class="nav-item">
307 title={i18n.t("support_lemmy")}
308 href={donateLemmyUrl}
310 <Icon icon="heart" classes="small" />
314 <ul class="navbar-nav my-2">
316 <li className="nav-item">
320 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
321 title={i18n.t("admin_settings")}
323 <Icon icon="settings" />
328 {!this.context.router.history.location.pathname.match(
332 class="form-inline mr-2"
333 onSubmit={linkEvent(this, this.handleSearchSubmit)}
337 class={`form-control mr-0 search-input ${
338 this.state.toggleSearch ? "show-input" : "hide-input"
340 onInput={linkEvent(this, this.handleSearchParam)}
341 value={this.state.searchParam}
342 ref={this.searchTextField}
344 placeholder={i18n.t("search")}
345 onBlur={linkEvent(this, this.handleSearchBlur)}
347 <label class="sr-only" htmlFor="search-input">
352 onClick={linkEvent(this, this.handleSearchBtn)}
353 class="px-1 btn btn-link"
354 style="color: var(--gray)"
355 aria-label={i18n.t("search")}
357 <Icon icon="search" />
361 {this.state.isLoggedIn ? (
363 <ul class="navbar-nav my-2">
364 <li className="nav-item">
368 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
369 title={i18n.t("unread_messages", {
370 count: this.state.unreadInboxCount,
371 formattedCount: numToSI(this.state.unreadInboxCount),
375 {this.state.unreadInboxCount > 0 && (
376 <span class="ml-1 badge badge-light">
377 {numToSI(this.state.unreadInboxCount)}
383 {UserService.Instance.myUserInfo?.moderates.length > 0 && (
384 <ul class="navbar-nav my-2">
385 <li className="nav-item">
389 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
390 title={i18n.t("unread_reports", {
391 count: this.state.unreadReportCount,
392 formattedCount: numToSI(this.state.unreadReportCount),
395 <Icon icon="shield" />
396 {this.state.unreadReportCount > 0 && (
397 <span class="ml-1 badge badge-light">
398 {numToSI(this.state.unreadReportCount)}
405 {UserService.Instance.myUserInfo?.local_user_view.person
407 <ul class="navbar-nav my-2">
408 <li className="nav-item">
410 to="/registration_applications"
412 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
413 title={i18n.t("unread_registration_applications", {
414 count: this.state.unreadApplicationCount,
415 formattedCount: numToSI(
416 this.state.unreadApplicationCount
420 <Icon icon="clipboard" />
421 {this.state.unreadApplicationCount > 0 && (
422 <span class="mx-1 badge badge-light">
423 {numToSI(this.state.unreadApplicationCount)}
430 <ul class="navbar-nav">
431 <li class="nav-item dropdown">
433 class="nav-link btn btn-link dropdown-toggle"
434 onClick={linkEvent(this, this.handleToggleDropdown)}
437 aria-expanded="false"
440 {person.avatar && showAvatars() && (
441 <PictrsImage src={person.avatar} icon />
444 ? person.display_name
448 {this.state.showDropdown && (
450 class="dropdown-content"
451 onMouseLeave={linkEvent(
453 this.handleToggleDropdown
456 <li className="nav-item">
458 to={`/u/${UserService.Instance.myUserInfo.local_user_view.person.name}`}
460 title={i18n.t("profile")}
462 <Icon icon="user" classes="mr-1" />
466 <li className="nav-item">
470 title={i18n.t("settings")}
472 <Icon icon="settings" classes="mr-1" />
477 <hr class="dropdown-divider" />
479 <li className="nav-item">
481 className="nav-link btn btn-link"
482 onClick={linkEvent(this, this.handleLogoutClick)}
485 <Icon icon="log-out" classes="mr-1" />
495 <ul class="navbar-nav my-2">
496 <li className="nav-item">
500 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
501 title={i18n.t("login")}
506 <li className="nav-item">
510 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
511 title={i18n.t("sign_up")}
524 handleToggleExpandNavbar(i: Navbar) {
525 i.state.expanded = !i.state.expanded;
529 handleHideExpandNavbar(i: Navbar) {
530 i.setState({ expanded: false, showDropdown: false });
533 handleSearchParam(i: Navbar, event: any) {
534 i.state.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.state.toggleSearch = false;
561 handleLogoutClick(i: Navbar) {
562 i.setState({ showDropdown: false, expanded: false });
563 UserService.Instance.logout();
564 window.location.href = "/";
568 handleToggleDropdown(i: Navbar) {
569 i.state.showDropdown = !i.state.showDropdown;
573 parseMessage(msg: any) {
574 let op = wsUserOp(msg);
577 if (msg.error == "not_logged_in") {
578 UserService.Instance.logout();
582 } else if (msg.reconnect) {
583 console.log(i18n.t("websocket_reconnected"));
584 WebSocketService.Instance.send(
590 } else if (op == UserOperation.GetUnreadCount) {
591 let data = wsJsonToRes<GetUnreadCountResponse>(msg).data;
592 this.state.unreadInboxCount =
593 data.replies + data.mentions + data.private_messages;
594 this.setState(this.state);
595 this.sendUnreadCount();
596 } else if (op == UserOperation.GetReportCount) {
597 let data = wsJsonToRes<GetReportCountResponse>(msg).data;
598 this.state.unreadReportCount = data.post_reports + data.comment_reports;
599 this.setState(this.state);
600 this.sendReportUnread();
601 } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
603 wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(msg).data;
604 this.state.unreadApplicationCount = data.registration_applications;
605 this.setState(this.state);
606 this.sendApplicationUnread();
607 } else if (op == UserOperation.GetSite) {
608 // This is only called on a successful login
609 let data = wsJsonToRes<GetSiteResponse>(msg).data;
610 console.log(data.my_user);
611 UserService.Instance.myUserInfo = data.my_user;
613 UserService.Instance.myUserInfo.local_user_view.local_user.theme
615 i18n.changeLanguage(getLanguages()[0]);
616 this.state.isLoggedIn = true;
617 this.setState(this.state);
618 } else if (op == UserOperation.CreateComment) {
619 let data = wsJsonToRes<CommentResponse>(msg).data;
621 if (this.state.isLoggedIn) {
623 data.recipient_ids.includes(
624 UserService.Instance.myUserInfo.local_user_view.local_user.id
627 this.state.unreadInboxCount++;
628 this.setState(this.state);
629 this.sendUnreadCount();
630 notifyComment(data.comment_view, this.context.router);
633 } else if (op == UserOperation.CreatePrivateMessage) {
634 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
636 if (this.state.isLoggedIn) {
638 data.private_message_view.recipient.id ==
639 UserService.Instance.myUserInfo.local_user_view.person.id
641 this.state.unreadInboxCount++;
642 this.setState(this.state);
643 this.sendUnreadCount();
644 notifyPrivateMessage(data.private_message_view, this.context.router);
651 console.log("Fetching inbox unreads...");
653 let unreadForm: GetUnreadCount = {
656 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
658 console.log("Fetching reports...");
660 let reportCountForm: GetReportCount = {
663 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
665 if (UserService.Instance.myUserInfo?.local_user_view.person.admin) {
666 console.log("Fetching applications...");
668 let applicationCountForm: GetUnreadRegistrationApplicationCount = {
671 WebSocketService.Instance.send(
672 wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
677 get currentLocation() {
678 return this.context.router.history.location.pathname;
682 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
686 UserService.Instance.unreadReportCountSub.next(
687 this.state.unreadReportCount
691 sendApplicationUnread() {
692 UserService.Instance.unreadApplicationCountSub.next(
693 this.state.unreadApplicationCount
697 get canAdmin(): boolean {
699 UserService.Instance.myUserInfo &&
700 this.props.site_res.admins
701 .map(a => a.person.id)
702 .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
706 get canCreateCommunity(): boolean {
708 this.props.site_res.site_view?.site.community_creation_admin_only;
709 return !adminOnly || this.canAdmin;
712 requestNotificationPermission() {
713 if (UserService.Instance.myUserInfo) {
714 document.addEventListener("DOMContentLoaded", function () {
716 toast(i18n.t("notifications_error"), "danger");
720 if (Notification.permission !== "granted")
721 Notification.requestPermission();