1 import { Component, createRef, linkEvent, RefObject } from "inferno";
2 import { NavLink } from "inferno-router";
6 GetReportCountResponse,
9 GetUnreadCountResponse,
10 GetUnreadRegistrationApplicationCount,
11 GetUnreadRegistrationApplicationCountResponse,
12 PrivateMessageResponse,
16 } from "lemmy-js-client";
17 import { Subscription } from "rxjs";
18 import { i18n } from "../../i18next";
19 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 state: NavbarState = {
62 unreadApplicationCount: 0,
70 constructor(props: any, context: any) {
71 super(props, context);
73 this.parseMessage = this.parseMessage.bind(this);
74 this.subscription = wsSubscribe(this.parseMessage);
78 // Subscribe to jwt changes
80 this.searchTextField = createRef();
82 // On the first load, check the unreads
83 let auth = myAuth(false);
84 if (auth && UserService.Instance.myUserInfo) {
85 this.requestNotificationPermission();
86 WebSocketService.Instance.send(
95 this.requestNotificationPermission();
97 // Subscribe to unread count changes
98 this.unreadInboxCountSub =
99 UserService.Instance.unreadInboxCountSub.subscribe(res => {
100 this.setState({ unreadInboxCount: res });
102 // Subscribe to unread report count changes
103 this.unreadReportCountSub =
104 UserService.Instance.unreadReportCountSub.subscribe(res => {
105 this.setState({ unreadReportCount: res });
107 // Subscribe to unread application count
108 this.unreadApplicationCountSub =
109 UserService.Instance.unreadApplicationCountSub.subscribe(res => {
110 this.setState({ unreadApplicationCount: res });
115 componentWillUnmount() {
116 this.wsSub.unsubscribe();
117 this.userSub.unsubscribe();
118 this.unreadInboxCountSub.unsubscribe();
119 this.unreadReportCountSub.unsubscribe();
120 this.unreadApplicationCountSub.unsubscribe();
124 const searchParam = this.state.searchParam;
125 this.setState({ searchParam: "" });
126 this.setState({ toggleSearch: false });
127 this.setState({ showDropdown: false, expanded: false });
128 if (searchParam === "") {
129 this.context.router.history.push(`/search/`);
131 const searchParamEncoded = encodeURIComponent(searchParam);
132 this.context.router.history.push(
133 `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
139 return this.navbar();
142 // TODO class active corresponding to current page
144 let siteView = this.props.siteRes.site_view;
145 let person = UserService.Instance.myUserInfo?.local_user_view.person;
147 <nav className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3">
148 <div className="container-lg">
151 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
152 title={siteView.site.description ?? siteView.site.name}
153 className="d-flex align-items-center navbar-brand mr-md-3"
155 {siteView.site.icon && showAvatars() && (
156 <PictrsImage src={siteView.site.icon} icon />
160 {UserService.Instance.myUserInfo && (
162 <ul className="navbar-nav ml-auto">
163 <li className="nav-item">
166 className="p-1 navbar-toggler nav-link border-0"
167 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
168 title={i18n.t("unread_messages", {
169 count: this.state.unreadInboxCount,
170 formattedCount: numToSI(this.state.unreadInboxCount),
174 {this.state.unreadInboxCount > 0 && (
175 <span className="mx-1 badge badge-light">
176 {numToSI(this.state.unreadInboxCount)}
182 {this.moderatesSomething && (
183 <ul className="navbar-nav ml-1">
184 <li className="nav-item">
187 className="p-1 navbar-toggler nav-link border-0"
188 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
189 title={i18n.t("unread_reports", {
190 count: this.state.unreadReportCount,
191 formattedCount: numToSI(this.state.unreadReportCount),
194 <Icon icon="shield" />
195 {this.state.unreadReportCount > 0 && (
196 <span className="mx-1 badge badge-light">
197 {numToSI(this.state.unreadReportCount)}
205 <ul className="navbar-nav ml-1">
206 <li className="nav-item">
208 to="/registration_applications"
209 className="p-1 navbar-toggler nav-link border-0"
210 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
211 title={i18n.t("unread_registration_applications", {
212 count: this.state.unreadApplicationCount,
213 formattedCount: numToSI(
214 this.state.unreadApplicationCount
218 <Icon icon="clipboard" />
219 {this.state.unreadApplicationCount > 0 && (
220 <span className="mx-1 badge badge-light">
221 {numToSI(this.state.unreadApplicationCount)}
231 className="navbar-toggler border-0 p-1"
234 onClick={linkEvent(this, this.handleToggleExpandNavbar)}
235 data-tippy-content={i18n.t("expand_here")}
240 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
242 <ul className="navbar-nav my-2 mr-auto">
243 <li className="nav-item">
247 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
248 title={i18n.t("communities")}
250 {i18n.t("communities")}
253 <li className="nav-item">
254 {/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
257 pathname: "/create_post",
261 state: { prevPath: this.currentLocation },
264 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
265 title={i18n.t("create_post")}
267 {i18n.t("create_post")}
270 {canCreateCommunity(this.props.siteRes) && (
271 <li className="nav-item">
273 to="/create_community"
275 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
276 title={i18n.t("create_community")}
278 {i18n.t("create_community")}
282 <li className="nav-item">
285 title={i18n.t("support_lemmy")}
286 href={donateLemmyUrl}
288 <Icon icon="heart" classes="small" />
292 <ul className="navbar-nav my-2">
294 <li className="nav-item">
298 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
299 title={i18n.t("admin_settings")}
301 <Icon icon="settings" />
306 {!this.context.router.history.location.pathname.match(
309 <ul className="navbar-nav">
310 <li className="nav-item">
312 className="form-inline mr-1"
313 onSubmit={linkEvent(this, this.handleSearchSubmit)}
317 className={`form-control mr-0 search-input ${
318 this.state.toggleSearch ? "show-input" : "hide-input"
320 onInput={linkEvent(this, this.handleSearchParam)}
321 value={this.state.searchParam}
322 ref={this.searchTextField}
323 disabled={!this.state.toggleSearch}
325 placeholder={i18n.t("search")}
326 onBlur={linkEvent(this, this.handleSearchBlur)}
328 <label className="sr-only" htmlFor="search-input">
333 onClick={linkEvent(this, this.handleSearchBtn)}
334 className="px-1 btn btn-link nav-link"
335 aria-label={i18n.t("search")}
337 <Icon icon="search" />
343 {UserService.Instance.myUserInfo ? (
345 <ul className="navbar-nav my-2">
346 <li className="nav-item">
350 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
351 title={i18n.t("unread_messages", {
352 count: this.state.unreadInboxCount,
353 formattedCount: numToSI(this.state.unreadInboxCount),
357 {this.state.unreadInboxCount > 0 && (
358 <span className="ml-1 badge badge-light">
359 {numToSI(this.state.unreadInboxCount)}
365 {this.moderatesSomething && (
366 <ul className="navbar-nav my-2">
367 <li className="nav-item">
371 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
372 title={i18n.t("unread_reports", {
373 count: this.state.unreadReportCount,
374 formattedCount: numToSI(this.state.unreadReportCount),
377 <Icon icon="shield" />
378 {this.state.unreadReportCount > 0 && (
379 <span className="ml-1 badge badge-light">
380 {numToSI(this.state.unreadReportCount)}
388 <ul className="navbar-nav my-2">
389 <li className="nav-item">
391 to="/registration_applications"
393 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
394 title={i18n.t("unread_registration_applications", {
395 count: this.state.unreadApplicationCount,
396 formattedCount: numToSI(
397 this.state.unreadApplicationCount
401 <Icon icon="clipboard" />
402 {this.state.unreadApplicationCount > 0 && (
403 <span className="mx-1 badge badge-light">
404 {numToSI(this.state.unreadApplicationCount)}
412 <ul className="navbar-nav">
413 <li className="nav-item dropdown">
415 className="nav-link btn btn-link dropdown-toggle"
416 onClick={linkEvent(this, this.handleToggleDropdown)}
419 aria-expanded="false"
422 {showAvatars() && person.avatar && (
423 <PictrsImage src={person.avatar} icon />
425 {person.display_name ?? person.name}
428 {this.state.showDropdown && (
430 className="dropdown-content"
431 onMouseLeave={linkEvent(
433 this.handleToggleDropdown
436 <li className="nav-item">
438 to={`/u/${person.name}`}
440 title={i18n.t("profile")}
442 <Icon icon="user" classes="mr-1" />
446 <li className="nav-item">
450 title={i18n.t("settings")}
452 <Icon icon="settings" classes="mr-1" />
457 <hr className="dropdown-divider" />
459 <li className="nav-item">
461 className="nav-link btn btn-link"
462 onClick={linkEvent(this, this.handleLogoutClick)}
465 <Icon icon="log-out" classes="mr-1" />
476 <ul className="navbar-nav my-2">
477 <li className="nav-item">
481 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
482 title={i18n.t("login")}
487 <li className="nav-item">
491 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
492 title={i18n.t("sign_up")}
505 get moderatesSomething(): boolean {
506 let mods = UserService.Instance.myUserInfo?.moderates;
507 let moderatesS = (mods && mods.length > 0) || false;
508 return amAdmin() || moderatesS;
511 handleToggleExpandNavbar(i: Navbar) {
512 i.setState({ expanded: !i.state.expanded });
515 handleHideExpandNavbar(i: Navbar) {
516 i.setState({ expanded: false, showDropdown: false });
519 handleSearchParam(i: Navbar, event: any) {
520 i.setState({ searchParam: event.target.value });
523 handleSearchSubmit(i: Navbar, event: any) {
524 event.preventDefault();
528 handleSearchBtn(i: Navbar, event: any) {
529 event.preventDefault();
530 i.setState({ toggleSearch: true });
532 i.searchTextField.current?.focus();
533 const offsetWidth = i.searchTextField.current?.offsetWidth;
534 if (i.state.searchParam && offsetWidth && offsetWidth > 100) {
539 handleSearchBlur(i: Navbar, event: any) {
540 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
541 i.setState({ toggleSearch: false });
545 handleLogoutClick(i: Navbar) {
546 i.setState({ showDropdown: false, expanded: false });
547 UserService.Instance.logout();
550 handleToggleDropdown(i: Navbar) {
551 i.setState({ showDropdown: !i.state.showDropdown });
554 parseMessage(msg: any) {
555 let op = wsUserOp(msg);
558 if (msg.error == "not_logged_in") {
559 UserService.Instance.logout();
562 } else if (msg.reconnect) {
563 console.log(i18n.t("websocket_reconnected"));
564 let auth = myAuth(false);
565 if (UserService.Instance.myUserInfo && auth) {
566 WebSocketService.Instance.send(
573 } else if (op == UserOperation.GetUnreadCount) {
574 let data = wsJsonToRes<GetUnreadCountResponse>(msg);
576 unreadInboxCount: data.replies + data.mentions + data.private_messages,
578 this.sendUnreadCount();
579 } else if (op == UserOperation.GetReportCount) {
580 let data = wsJsonToRes<GetReportCountResponse>(msg);
584 data.comment_reports +
585 (data.private_message_reports ?? 0),
587 this.sendReportUnread();
588 } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
590 wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(msg);
591 this.setState({ unreadApplicationCount: data.registration_applications });
592 this.sendApplicationUnread();
593 } else if (op == UserOperation.CreateComment) {
594 let data = wsJsonToRes<CommentResponse>(msg);
595 let mui = UserService.Instance.myUserInfo;
598 data.recipient_ids.includes(mui.local_user_view.local_user.id)
601 unreadInboxCount: this.state.unreadInboxCount + 1,
603 this.sendUnreadCount();
604 notifyComment(data.comment_view, this.context.router);
606 } else if (op == UserOperation.CreatePrivateMessage) {
607 let data = wsJsonToRes<PrivateMessageResponse>(msg);
610 data.private_message_view.recipient.id ==
611 UserService.Instance.myUserInfo?.local_user_view.person.id
614 unreadInboxCount: this.state.unreadInboxCount + 1,
616 this.sendUnreadCount();
617 notifyPrivateMessage(data.private_message_view, this.context.router);
623 console.log("Fetching inbox unreads...");
627 let unreadForm: GetUnreadCount = {
630 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
632 console.log("Fetching reports...");
634 let reportCountForm: GetReportCount = {
637 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
640 console.log("Fetching applications...");
642 let applicationCountForm: GetUnreadRegistrationApplicationCount = {
645 WebSocketService.Instance.send(
646 wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
652 get currentLocation() {
653 return this.context.router.history.location.pathname;
657 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
661 UserService.Instance.unreadReportCountSub.next(
662 this.state.unreadReportCount
666 sendApplicationUnread() {
667 UserService.Instance.unreadApplicationCountSub.next(
668 this.state.unreadApplicationCount
672 requestNotificationPermission() {
673 if (UserService.Instance.myUserInfo) {
674 document.addEventListener("DOMContentLoaded", function () {
676 toast(i18n.t("notifications_error"), "danger");
680 if (Notification.permission !== "granted")
681 Notification.requestPermission();