1 import { Component, createRef, linkEvent, RefObject } from "inferno";
2 import { Link } from "inferno-router";
6 GetReportCountResponse,
9 GetUnreadCountResponse,
10 PrivateMessageResponse,
12 } from "lemmy-js-client";
13 import { Subscription } from "rxjs";
14 import { i18n } from "../../i18next";
15 import { UserService, WebSocketService } from "../../services";
32 import { Icon } from "../common/icon";
33 import { PictrsImage } from "../common/pictrs-image";
35 interface NavbarProps {
36 site_res: GetSiteResponse;
39 interface NavbarState {
42 unreadInboxCount: number;
43 unreadReportCount: number;
45 toggleSearch: boolean;
46 showDropdown: boolean;
47 onSiteBanner?(url: string): any;
50 export class Navbar extends Component<NavbarProps, NavbarState> {
51 private wsSub: Subscription;
52 private userSub: Subscription;
53 private unreadInboxCountSub: Subscription;
54 private unreadReportCountSub: Subscription;
55 private searchTextField: RefObject<HTMLInputElement>;
56 emptyState: NavbarState = {
57 isLoggedIn: !!this.props.site_res.my_user,
67 constructor(props: any, context: any) {
68 super(props, context);
69 this.state = this.emptyState;
71 this.parseMessage = this.parseMessage.bind(this);
72 this.subscription = wsSubscribe(this.parseMessage);
76 // Subscribe to jwt changes
78 this.websocketEvents();
80 this.searchTextField = createRef();
81 console.log(`isLoggedIn = ${this.state.isLoggedIn}`);
83 // On the first load, check the unreads
84 if (this.state.isLoggedIn == false) {
85 // setTheme(data.my_user.theme, true);
86 // i18n.changeLanguage(getLanguage());
87 // i18n.changeLanguage('de');
89 this.requestNotificationPermission();
90 WebSocketService.Instance.send(
98 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
100 if (res !== undefined) {
101 this.requestNotificationPermission();
102 WebSocketService.Instance.send(
103 wsClient.getSite({ auth: authField() })
106 this.setState({ isLoggedIn: false });
110 // Subscribe to unread count changes
111 this.unreadInboxCountSub =
112 UserService.Instance.unreadInboxCountSub.subscribe(res => {
113 this.setState({ unreadInboxCount: res });
115 // Subscribe to unread report count changes
116 this.unreadReportCountSub =
117 UserService.Instance.unreadReportCountSub.subscribe(res => {
118 this.setState({ unreadReportCount: res });
123 componentWillUnmount() {
124 this.wsSub.unsubscribe();
125 this.userSub.unsubscribe();
126 this.unreadInboxCountSub.unsubscribe();
127 this.unreadReportCountSub.unsubscribe();
131 const searchParam = this.state.searchParam;
132 this.setState({ searchParam: "" });
133 this.setState({ toggleSearch: false });
134 this.setState({ showDropdown: false, expanded: false });
135 if (searchParam === "") {
136 this.context.router.history.push(`/search/`);
138 const searchParamEncoded = encodeURIComponent(searchParam);
139 this.context.router.history.push(
140 `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
146 return this.navbar();
149 // TODO class active corresponding to current page
152 UserService.Instance.myUserInfo || this.props.site_res.my_user;
153 let person = myUserInfo?.local_user_view.person;
155 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
156 <div class="container">
157 {this.props.site_res.site_view && (
160 onTouchEnd={linkEvent(this, this.handleHideExpandNavbar)}
161 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
163 this.props.site_res.site_view.site.description ||
164 this.props.site_res.site_view.site.name
166 className="d-flex align-items-center navbar-brand mr-md-3"
168 {this.props.site_res.site_view.site.icon && showAvatars() && (
170 src={this.props.site_res.site_view.site.icon}
174 {this.props.site_res.site_view.site.name}
177 {this.state.isLoggedIn && (
179 <ul class="navbar-nav ml-auto">
180 <li className="nav-item">
183 className="p-1 navbar-toggler nav-link border-0"
184 onTouchEnd={linkEvent(this, this.handleHideExpandNavbar)}
185 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
186 title={i18n.t("unread_messages", {
187 count: this.state.unreadInboxCount,
188 formattedCount: numToSI(this.state.unreadInboxCount),
192 {this.state.unreadInboxCount > 0 && (
193 <span class="mx-1 badge badge-light">
194 {numToSI(this.state.unreadInboxCount)}
200 {UserService.Instance.myUserInfo?.moderates.length > 0 && (
201 <ul class="navbar-nav ml-1">
202 <li className="nav-item">
205 className="p-1 navbar-toggler nav-link border-0"
206 onTouchEnd={linkEvent(this, this.handleHideExpandNavbar)}
207 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
208 title={i18n.t("unread_reports", {
209 count: this.state.unreadReportCount,
210 formattedCount: numToSI(this.state.unreadReportCount),
213 <Icon icon="shield" />
214 {this.state.unreadReportCount > 0 && (
215 <span class="mx-1 badge badge-light">
216 {numToSI(this.state.unreadReportCount)}
226 class="navbar-toggler border-0 p-1"
229 onClick={linkEvent(this, this.handleToggleExpandNavbar)}
230 data-tippy-content={i18n.t("expand_here")}
235 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
237 <ul class="navbar-nav my-2 mr-auto">
238 <li class="nav-item">
242 onTouchEnd={linkEvent(this, this.handleHideExpandNavbar)}
243 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
244 title={i18n.t("communities")}
246 {i18n.t("communities")}
249 <li class="nav-item">
252 pathname: "/create_post",
253 prevPath: this.currentLocation,
256 onTouchEnd={linkEvent(this, this.handleHideExpandNavbar)}
257 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
258 title={i18n.t("create_post")}
260 {i18n.t("create_post")}
263 {this.canCreateCommunity && (
264 <li class="nav-item">
266 to="/create_community"
268 onTouchEnd={linkEvent(this, this.handleHideExpandNavbar)}
269 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
270 title={i18n.t("create_community")}
272 {i18n.t("create_community")}
276 <li class="nav-item">
279 title={i18n.t("support_lemmy")}
280 href={donateLemmyUrl}
282 <Icon icon="heart" classes="small" />
286 <ul class="navbar-nav my-2">
288 <li className="nav-item">
292 onTouchEnd={linkEvent(this, this.handleHideExpandNavbar)}
293 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
294 title={i18n.t("admin_settings")}
296 <Icon icon="settings" />
301 {!this.context.router.history.location.pathname.match(
305 class="form-inline mr-2"
306 onSubmit={linkEvent(this, this.handleSearchSubmit)}
310 class={`form-control mr-0 search-input ${
311 this.state.toggleSearch ? "show-input" : "hide-input"
313 onInput={linkEvent(this, this.handleSearchParam)}
314 value={this.state.searchParam}
315 ref={this.searchTextField}
317 placeholder={i18n.t("search")}
318 onBlur={linkEvent(this, this.handleSearchBlur)}
320 <label class="sr-only" htmlFor="search-input">
325 onClick={linkEvent(this, this.handleSearchBtn)}
326 class="px-1 btn btn-link"
327 style="color: var(--gray)"
328 aria-label={i18n.t("search")}
330 <Icon icon="search" />
334 {this.state.isLoggedIn ? (
336 <ul class="navbar-nav my-2">
337 <li className="nav-item">
341 onTouchEnd={linkEvent(this, this.handleHideExpandNavbar)}
342 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
343 title={i18n.t("unread_messages", {
344 count: this.state.unreadInboxCount,
345 formattedCount: numToSI(this.state.unreadInboxCount),
349 {this.state.unreadInboxCount > 0 && (
350 <span class="ml-1 badge badge-light">
351 {numToSI(this.state.unreadInboxCount)}
357 {UserService.Instance.myUserInfo?.moderates.length > 0 && (
358 <ul class="navbar-nav my-2">
359 <li className="nav-item">
363 onTouchEnd={linkEvent(
365 this.handleHideExpandNavbar
367 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
368 title={i18n.t("unread_reports", {
369 count: this.state.unreadReportCount,
370 formattedCount: numToSI(this.state.unreadReportCount),
373 <Icon icon="shield" />
374 {this.state.unreadReportCount > 0 && (
375 <span class="ml-1 badge badge-light">
376 {numToSI(this.state.unreadReportCount)}
383 <ul class="navbar-nav">
384 <li class="nav-item dropdown">
386 class="nav-link btn btn-link dropdown-toggle"
387 onClick={linkEvent(this, this.handleToggleDropdown)}
390 aria-expanded="false"
393 {person.avatar && showAvatars() && (
394 <PictrsImage src={person.avatar} icon />
397 ? person.display_name
401 {this.state.showDropdown && (
403 class="dropdown-content"
404 onMouseLeave={linkEvent(
406 this.handleToggleDropdown
409 <li className="nav-item">
411 to={`/u/${UserService.Instance.myUserInfo.local_user_view.person.name}`}
413 title={i18n.t("profile")}
415 <Icon icon="user" classes="mr-1" />
419 <li className="nav-item">
423 title={i18n.t("settings")}
425 <Icon icon="settings" classes="mr-1" />
430 <hr class="dropdown-divider" />
432 <li className="nav-item">
434 className="nav-link btn btn-link"
435 onClick={linkEvent(this, this.handleLogoutClick)}
438 <Icon icon="log-out" classes="mr-1" />
448 <ul class="navbar-nav my-2">
449 <li className="nav-item">
453 onTouchEnd={linkEvent(this, this.handleHideExpandNavbar)}
454 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
455 title={i18n.t("login")}
460 <li className="nav-item">
464 onTouchEnd={linkEvent(this, this.handleHideExpandNavbar)}
465 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
466 title={i18n.t("sign_up")}
479 handleToggleExpandNavbar(i: Navbar) {
480 i.state.expanded = !i.state.expanded;
484 handleHideExpandNavbar(i: Navbar) {
485 i.setState({ expanded: false, showDropdown: false });
488 handleSearchParam(i: Navbar, event: any) {
489 i.state.searchParam = event.target.value;
493 handleSearchSubmit(i: Navbar, event: any) {
494 event.preventDefault();
498 handleSearchBtn(i: Navbar, event: any) {
499 event.preventDefault();
500 i.setState({ toggleSearch: true });
502 i.searchTextField.current.focus();
503 const offsetWidth = i.searchTextField.current.offsetWidth;
504 if (i.state.searchParam && offsetWidth > 100) {
509 handleSearchBlur(i: Navbar, event: any) {
510 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
511 i.state.toggleSearch = false;
516 handleLogoutClick(i: Navbar) {
517 i.setState({ showDropdown: false, expanded: false });
518 UserService.Instance.logout();
519 window.location.href = "/";
523 handleToggleDropdown(i: Navbar) {
524 i.state.showDropdown = !i.state.showDropdown;
528 parseMessage(msg: any) {
529 let op = wsUserOp(msg);
532 if (msg.error == "not_logged_in") {
533 UserService.Instance.logout();
537 } else if (msg.reconnect) {
538 console.log(i18n.t("websocket_reconnected"));
539 WebSocketService.Instance.send(
545 } else if (op == UserOperation.GetUnreadCount) {
546 let data = wsJsonToRes<GetUnreadCountResponse>(msg).data;
547 this.state.unreadInboxCount =
548 data.replies + data.mentions + data.private_messages;
549 this.setState(this.state);
550 this.sendUnreadCount();
551 } else if (op == UserOperation.GetReportCount) {
552 let data = wsJsonToRes<GetReportCountResponse>(msg).data;
553 this.state.unreadReportCount = data.post_reports + data.comment_reports;
554 this.setState(this.state);
555 this.sendReportUnread();
556 } else if (op == UserOperation.GetSite) {
557 // This is only called on a successful login
558 let data = wsJsonToRes<GetSiteResponse>(msg).data;
559 console.log(data.my_user);
560 UserService.Instance.myUserInfo = data.my_user;
562 UserService.Instance.myUserInfo.local_user_view.local_user.theme
564 i18n.changeLanguage(getLanguage());
565 this.state.isLoggedIn = true;
566 this.setState(this.state);
567 } else if (op == UserOperation.CreateComment) {
568 let data = wsJsonToRes<CommentResponse>(msg).data;
570 if (this.state.isLoggedIn) {
572 data.recipient_ids.includes(
573 UserService.Instance.myUserInfo.local_user_view.local_user.id
576 this.state.unreadInboxCount++;
577 this.setState(this.state);
578 this.sendUnreadCount();
579 notifyComment(data.comment_view, this.context.router);
582 } else if (op == UserOperation.CreatePrivateMessage) {
583 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
585 if (this.state.isLoggedIn) {
587 data.private_message_view.recipient.id ==
588 UserService.Instance.myUserInfo.local_user_view.person.id
590 this.state.unreadInboxCount++;
591 this.setState(this.state);
592 this.sendUnreadCount();
593 notifyPrivateMessage(data.private_message_view, this.context.router);
600 console.log("Fetching inbox unreads...");
602 let unreadForm: GetUnreadCount = {
606 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
608 console.log("Fetching reports...");
610 let reportCountForm: GetReportCount = {
614 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
617 get currentLocation() {
618 return this.context.router.history.location.pathname;
622 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
626 UserService.Instance.unreadReportCountSub.next(
627 this.state.unreadReportCount
631 get canAdmin(): boolean {
633 UserService.Instance.myUserInfo &&
634 this.props.site_res.admins
635 .map(a => a.person.id)
636 .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
640 get canCreateCommunity(): boolean {
642 this.props.site_res.site_view?.site.community_creation_admin_only;
643 return !adminOnly || this.canAdmin;
646 /// Listens for some websocket errors
648 let msg = i18n.t("websocket_disconnected");
649 WebSocketService.Instance.closeEventListener(() => {
654 requestNotificationPermission() {
655 if (UserService.Instance.myUserInfo) {
656 document.addEventListener("DOMContentLoaded", function () {
658 toast(i18n.t("notifications_error"), "danger");
662 if (Notification.permission !== "granted")
663 Notification.requestPermission();