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 this.props.site_res.site_view.site.description ||
161 this.props.site_res.site_view.site.name
163 className="d-flex align-items-center navbar-brand mr-md-3 btn btn-link"
164 onClick={linkEvent(this, this.handleGotoHome)}
166 {this.props.site_res.site_view.site.icon && showAvatars() && (
168 src={this.props.site_res.site_view.site.icon}
172 {this.props.site_res.site_view.site.name}
175 {this.state.isLoggedIn && (
177 <ul class="navbar-nav ml-auto">
178 <li className="nav-item">
180 className="p-1 navbar-toggler nav-link border-0 btn btn-link"
181 onClick={linkEvent(this, this.handleGotoInbox)}
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 {UserService.Instance.myUserInfo?.moderates.length > 0 && (
197 <ul class="navbar-nav ml-1">
198 <li className="nav-item">
200 className="p-1 navbar-toggler nav-link border-0 btn btn-link"
201 onClick={linkEvent(this, this.handleGotoReports)}
202 title={i18n.t("unread_reports", {
203 count: this.state.unreadReportCount,
204 formattedCount: numToSI(this.state.unreadReportCount),
207 <Icon icon="shield" />
208 {this.state.unreadReportCount > 0 && (
209 <span class="mx-1 badge badge-light">
210 {numToSI(this.state.unreadReportCount)}
220 class="navbar-toggler border-0 p-1"
223 onClick={linkEvent(this, this.expandNavbar)}
224 data-tippy-content={i18n.t("expand_here")}
229 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
231 <ul class="navbar-nav my-2 mr-auto">
232 <li class="nav-item">
234 className="nav-link btn btn-link"
235 onClick={linkEvent(this, this.handleGotoCommunities)}
236 title={i18n.t("communities")}
238 {i18n.t("communities")}
241 <li class="nav-item">
243 className="nav-link btn btn-link"
244 onClick={linkEvent(this, this.handleGotoCreatePost)}
245 title={i18n.t("create_post")}
247 {i18n.t("create_post")}
250 {this.canCreateCommunity && (
251 <li class="nav-item">
253 className="nav-link btn btn-link"
254 onClick={linkEvent(this, this.handleGotoCreateCommunity)}
255 title={i18n.t("create_community")}
257 {i18n.t("create_community")}
261 <li class="nav-item">
264 title={i18n.t("support_lemmy")}
265 href={donateLemmyUrl}
267 <Icon icon="heart" classes="small" />
271 <ul class="navbar-nav my-2">
273 <li className="nav-item">
275 className="nav-link btn btn-link"
276 onClick={linkEvent(this, this.handleGotoAdmin)}
277 title={i18n.t("admin_settings")}
279 <Icon icon="settings" />
284 {!this.context.router.history.location.pathname.match(
288 class="form-inline mr-2"
289 onSubmit={linkEvent(this, this.handleSearchSubmit)}
293 class={`form-control mr-0 search-input ${
294 this.state.toggleSearch ? "show-input" : "hide-input"
296 onInput={linkEvent(this, this.handleSearchParam)}
297 value={this.state.searchParam}
298 ref={this.searchTextField}
300 placeholder={i18n.t("search")}
301 onBlur={linkEvent(this, this.handleSearchBlur)}
303 <label class="sr-only" htmlFor="search-input">
308 onClick={linkEvent(this, this.handleSearchBtn)}
309 class="px-1 btn btn-link"
310 style="color: var(--gray)"
311 aria-label={i18n.t("search")}
313 <Icon icon="search" />
317 {this.state.isLoggedIn ? (
319 <ul class="navbar-nav my-2">
320 <li className="nav-item">
324 title={i18n.t("unread_messages", {
325 count: this.state.unreadInboxCount,
326 formattedCount: numToSI(this.state.unreadInboxCount),
330 {this.state.unreadInboxCount > 0 && (
331 <span class="ml-1 badge badge-light">
332 {numToSI(this.state.unreadInboxCount)}
338 {UserService.Instance.myUserInfo?.moderates.length > 0 && (
339 <ul class="navbar-nav my-2">
340 <li className="nav-item">
344 title={i18n.t("unread_reports", {
345 count: this.state.unreadReportCount,
346 formattedCount: numToSI(this.state.unreadReportCount),
349 <Icon icon="shield" />
350 {this.state.unreadReportCount > 0 && (
351 <span class="ml-1 badge badge-light">
352 {numToSI(this.state.unreadReportCount)}
359 <ul class="navbar-nav">
360 <li class="nav-item dropdown">
362 class="nav-link btn btn-link dropdown-toggle"
363 onClick={linkEvent(this, this.handleShowDropdown)}
366 aria-expanded="false"
369 {person.avatar && showAvatars() && (
370 <PictrsImage src={person.avatar} icon />
373 ? person.display_name
377 {this.state.showDropdown && (
378 <div class="dropdown-content">
379 <li className="nav-item">
381 className="nav-link btn btn-link"
382 onClick={linkEvent(this, this.handleGotoProfile)}
383 title={i18n.t("profile")}
385 <Icon icon="user" classes="mr-1" />
389 <li className="nav-item">
391 className="nav-link btn btn-link"
392 onClick={linkEvent(this, this.handleGotoSettings)}
393 title={i18n.t("settings")}
395 <Icon icon="settings" classes="mr-1" />
400 <hr class="dropdown-divider" />
402 <li className="nav-item">
404 className="nav-link btn btn-link"
405 onClick={linkEvent(this, this.handleLogoutClick)}
408 <Icon icon="log-out" classes="mr-1" />
418 <ul class="navbar-nav my-2">
419 <li className="nav-item">
421 className="nav-link btn btn-link"
422 onClick={linkEvent(this, this.handleGotoLogin)}
423 title={i18n.t("login")}
428 <li className="nav-item">
430 className="nav-link btn btn-link"
431 onClick={linkEvent(this, this.handleGotoSignup)}
432 title={i18n.t("sign_up")}
445 expandNavbar(i: Navbar) {
446 i.state.expanded = !i.state.expanded;
450 handleSearchParam(i: Navbar, event: any) {
451 i.state.searchParam = event.target.value;
455 handleSearchSubmit(i: Navbar, event: any) {
456 event.preventDefault();
460 handleSearchBtn(i: Navbar, event: any) {
461 event.preventDefault();
462 i.setState({ toggleSearch: true });
464 i.searchTextField.current.focus();
465 const offsetWidth = i.searchTextField.current.offsetWidth;
466 if (i.state.searchParam && offsetWidth > 100) {
471 handleSearchBlur(i: Navbar, event: any) {
472 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
473 i.state.toggleSearch = false;
478 handleLogoutClick(i: Navbar) {
479 i.setState({ showDropdown: false, expanded: false });
480 UserService.Instance.logout();
481 window.location.href = "/";
485 handleGotoSettings(i: Navbar) {
486 i.setState({ showDropdown: false, expanded: false });
487 i.context.router.history.push("/settings");
490 handleGotoProfile(i: Navbar) {
491 i.setState({ showDropdown: false, expanded: false });
492 i.context.router.history.push(
493 `/u/${UserService.Instance.myUserInfo.local_user_view.person.name}`
497 handleGotoCreatePost(i: Navbar) {
498 i.setState({ showDropdown: false, expanded: false });
499 i.context.router.history.push("/create_post", {
500 prevPath: i.currentLocation,
504 handleGotoCreateCommunity(i: Navbar) {
505 i.setState({ showDropdown: false, expanded: false });
506 i.context.router.history.push(`/create_community`);
509 handleGotoCommunities(i: Navbar) {
510 i.setState({ showDropdown: false, expanded: false });
511 i.context.router.history.push(`/communities`);
514 handleGotoHome(i: Navbar) {
515 i.setState({ showDropdown: false, expanded: false });
516 i.context.router.history.push(`/`);
519 handleGotoInbox(i: Navbar) {
520 i.setState({ showDropdown: false, expanded: false });
521 i.context.router.history.push(`/inbox`);
524 handleGotoReports(i: Navbar) {
525 i.setState({ showDropdown: false, expanded: false });
526 i.context.router.history.push(`/reports`);
529 handleGotoAdmin(i: Navbar) {
530 i.setState({ showDropdown: false, expanded: false });
531 i.context.router.history.push(`/admin`);
534 handleGotoLogin(i: Navbar) {
535 i.setState({ showDropdown: false, expanded: false });
536 i.context.router.history.push(`/login`);
539 handleGotoSignup(i: Navbar) {
540 i.setState({ showDropdown: false, expanded: false });
541 i.context.router.history.push(`/signup`);
544 handleShowDropdown(i: Navbar) {
545 i.state.showDropdown = !i.state.showDropdown;
549 parseMessage(msg: any) {
550 let op = wsUserOp(msg);
553 if (msg.error == "not_logged_in") {
554 UserService.Instance.logout();
558 } else if (msg.reconnect) {
559 console.log(i18n.t("websocket_reconnected"));
560 WebSocketService.Instance.send(
566 } else if (op == UserOperation.GetUnreadCount) {
567 let data = wsJsonToRes<GetUnreadCountResponse>(msg).data;
568 this.state.unreadInboxCount =
569 data.replies + data.mentions + data.private_messages;
570 this.setState(this.state);
571 this.sendUnreadCount();
572 } else if (op == UserOperation.GetReportCount) {
573 let data = wsJsonToRes<GetReportCountResponse>(msg).data;
574 this.state.unreadReportCount = data.post_reports + data.comment_reports;
575 this.setState(this.state);
576 this.sendReportUnread();
577 } else if (op == UserOperation.GetSite) {
578 // This is only called on a successful login
579 let data = wsJsonToRes<GetSiteResponse>(msg).data;
580 console.log(data.my_user);
581 UserService.Instance.myUserInfo = data.my_user;
583 UserService.Instance.myUserInfo.local_user_view.local_user.theme
585 i18n.changeLanguage(getLanguage());
586 this.state.isLoggedIn = true;
587 this.setState(this.state);
588 } else if (op == UserOperation.CreateComment) {
589 let data = wsJsonToRes<CommentResponse>(msg).data;
591 if (this.state.isLoggedIn) {
593 data.recipient_ids.includes(
594 UserService.Instance.myUserInfo.local_user_view.local_user.id
597 this.state.unreadInboxCount++;
598 this.setState(this.state);
599 this.sendUnreadCount();
600 notifyComment(data.comment_view, this.context.router);
603 } else if (op == UserOperation.CreatePrivateMessage) {
604 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
606 if (this.state.isLoggedIn) {
608 data.private_message_view.recipient.id ==
609 UserService.Instance.myUserInfo.local_user_view.person.id
611 this.state.unreadInboxCount++;
612 this.setState(this.state);
613 this.sendUnreadCount();
614 notifyPrivateMessage(data.private_message_view, this.context.router);
621 console.log("Fetching inbox unreads...");
623 let unreadForm: GetUnreadCount = {
627 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
629 console.log("Fetching reports...");
631 let reportCountForm: GetReportCount = {
635 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
638 get currentLocation() {
639 return this.context.router.history.location.pathname;
643 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
647 UserService.Instance.unreadReportCountSub.next(
648 this.state.unreadReportCount
652 get canAdmin(): boolean {
654 UserService.Instance.myUserInfo &&
655 this.props.site_res.admins
656 .map(a => a.person.id)
657 .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
661 get canCreateCommunity(): boolean {
663 this.props.site_res.site_view?.site.community_creation_admin_only;
664 return !adminOnly || this.canAdmin;
667 /// Listens for some websocket errors
669 let msg = i18n.t("websocket_disconnected");
670 WebSocketService.Instance.closeEventListener(() => {
675 requestNotificationPermission() {
676 if (UserService.Instance.myUserInfo) {
677 document.addEventListener("DOMContentLoaded", function () {
679 toast(i18n.t("notifications_error"), "danger");
683 if (Notification.permission !== "granted")
684 Notification.requestPermission();