1 import { Component, createRef, linkEvent, RefObject } from "inferno";
2 import { Link } from "inferno-router";
7 GetPersonMentionsResponse,
12 GetReportCountResponse,
14 PrivateMessageResponse,
15 PrivateMessagesResponse,
19 } from "lemmy-js-client";
20 import { Subscription } from "rxjs";
21 import { i18n } from "../../i18next";
22 import { UserService, WebSocketService } from "../../services";
40 import { Icon } from "../common/icon";
41 import { PictrsImage } from "../common/pictrs-image";
43 interface NavbarProps {
44 site_res: GetSiteResponse;
47 interface NavbarState {
50 replies: CommentView[];
51 mentions: CommentView[];
52 messages: PrivateMessageView[];
53 unreadInboxCount: number;
54 unreadReportCount: number;
56 toggleSearch: boolean;
57 showDropdown: boolean;
58 onSiteBanner?(url: string): any;
61 export class Navbar extends Component<NavbarProps, NavbarState> {
62 private wsSub: Subscription;
63 private userSub: Subscription;
64 private unreadInboxCountSub: Subscription;
65 private unreadReportCountSub: Subscription;
66 private searchTextField: RefObject<HTMLInputElement>;
67 emptyState: NavbarState = {
68 isLoggedIn: !!this.props.site_res.my_user,
81 constructor(props: any, context: any) {
82 super(props, context);
83 this.state = this.emptyState;
85 this.parseMessage = this.parseMessage.bind(this);
86 this.subscription = wsSubscribe(this.parseMessage);
90 // Subscribe to jwt changes
92 this.websocketEvents();
94 this.searchTextField = createRef();
95 console.log(`isLoggedIn = ${this.state.isLoggedIn}`);
97 // On the first load, check the unreads
98 if (this.state.isLoggedIn == false) {
99 // setTheme(data.my_user.theme, true);
100 // i18n.changeLanguage(getLanguage());
101 // i18n.changeLanguage('de');
103 this.requestNotificationPermission();
104 WebSocketService.Instance.send(
112 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
114 if (res !== undefined) {
115 this.requestNotificationPermission();
116 WebSocketService.Instance.send(
117 wsClient.getSite({ auth: authField() })
120 this.setState({ isLoggedIn: false });
124 // Subscribe to unread count changes
125 this.unreadInboxCountSub =
126 UserService.Instance.unreadInboxCountSub.subscribe(res => {
127 this.setState({ unreadInboxCount: res });
129 // Subscribe to unread report count changes
130 this.unreadReportCountSub =
131 UserService.Instance.unreadReportCountSub.subscribe(res => {
132 this.setState({ unreadReportCount: res });
137 componentWillUnmount() {
138 this.wsSub.unsubscribe();
139 this.userSub.unsubscribe();
140 this.unreadInboxCountSub.unsubscribe();
141 this.unreadReportCountSub.unsubscribe();
145 const searchParam = this.state.searchParam;
146 this.setState({ searchParam: "" });
147 this.setState({ toggleSearch: false });
148 this.setState({ showDropdown: false, expanded: false });
149 if (searchParam === "") {
150 this.context.router.history.push(`/search/`);
152 const searchParamEncoded = encodeURIComponent(searchParam);
153 this.context.router.history.push(
154 `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
160 return this.navbar();
163 // TODO class active corresponding to current page
166 UserService.Instance.myUserInfo || this.props.site_res.my_user;
167 let person = myUserInfo?.local_user_view.person;
169 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
170 <div class="container">
171 {this.props.site_res.site_view && (
174 this.props.site_res.site_view.site.description ||
175 this.props.site_res.site_view.site.name
177 className="d-flex align-items-center navbar-brand mr-md-3 btn btn-link"
178 onClick={linkEvent(this, this.handleGotoHome)}
180 {this.props.site_res.site_view.site.icon && showAvatars() && (
182 src={this.props.site_res.site_view.site.icon}
186 {this.props.site_res.site_view.site.name}
189 {this.state.isLoggedIn && (
191 <ul class="navbar-nav ml-auto">
192 <li className="nav-item">
194 className="p-1 navbar-toggler nav-link border-0 btn btn-link"
195 onClick={linkEvent(this, this.handleGotoInbox)}
196 title={i18n.t("unread_messages", {
197 count: this.state.unreadInboxCount,
198 formattedCount: numToSI(this.state.unreadInboxCount),
202 {this.state.unreadInboxCount > 0 && (
203 <span class="mx-1 badge badge-light">
204 {numToSI(this.state.unreadInboxCount)}
210 {UserService.Instance.myUserInfo?.moderates.length > 0 && (
211 <ul class="navbar-nav ml-1">
212 <li className="nav-item">
214 className="p-1 navbar-toggler nav-link border-0 btn btn-link"
215 onClick={linkEvent(this, this.handleGotoReports)}
216 title={i18n.t("unread_reports", {
217 count: this.state.unreadReportCount,
218 formattedCount: numToSI(this.state.unreadReportCount),
221 <Icon icon="shield" />
222 {this.state.unreadReportCount > 0 && (
223 <span class="mx-1 badge badge-light">
224 {numToSI(this.state.unreadReportCount)}
234 class="navbar-toggler border-0 p-1"
237 onClick={linkEvent(this, this.expandNavbar)}
238 data-tippy-content={i18n.t("expand_here")}
243 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
245 <ul class="navbar-nav my-2 mr-auto">
246 <li class="nav-item">
248 className="nav-link btn btn-link"
249 onClick={linkEvent(this, this.handleGotoCommunities)}
250 title={i18n.t("communities")}
252 {i18n.t("communities")}
255 <li class="nav-item">
257 className="nav-link btn btn-link"
258 onClick={linkEvent(this, this.handleGotoCreatePost)}
259 title={i18n.t("create_post")}
261 {i18n.t("create_post")}
264 {this.canCreateCommunity && (
265 <li class="nav-item">
267 className="nav-link btn btn-link"
268 onClick={linkEvent(this, this.handleGotoCreateCommunity)}
269 title={i18n.t("create_community")}
271 {i18n.t("create_community")}
275 <li class="nav-item">
278 title={i18n.t("support_lemmy")}
279 href={donateLemmyUrl}
281 <Icon icon="heart" classes="small" />
285 <ul class="navbar-nav my-2">
287 <li className="nav-item">
289 className="nav-link btn btn-link"
290 onClick={linkEvent(this, this.handleGotoAdmin)}
291 title={i18n.t("admin_settings")}
293 <Icon icon="settings" />
298 {!this.context.router.history.location.pathname.match(
302 class="form-inline mr-2"
303 onSubmit={linkEvent(this, this.handleSearchSubmit)}
307 class={`form-control mr-0 search-input ${
308 this.state.toggleSearch ? "show-input" : "hide-input"
310 onInput={linkEvent(this, this.handleSearchParam)}
311 value={this.state.searchParam}
312 ref={this.searchTextField}
314 placeholder={i18n.t("search")}
315 onBlur={linkEvent(this, this.handleSearchBlur)}
317 <label class="sr-only" htmlFor="search-input">
322 onClick={linkEvent(this, this.handleSearchBtn)}
323 class="px-1 btn btn-link"
324 style="color: var(--gray)"
325 aria-label={i18n.t("search")}
327 <Icon icon="search" />
331 {this.state.isLoggedIn ? (
333 <ul class="navbar-nav my-2">
334 <li className="nav-item">
338 title={i18n.t("unread_messages", {
339 count: this.state.unreadInboxCount,
340 formattedCount: numToSI(this.state.unreadInboxCount),
344 {this.state.unreadInboxCount > 0 && (
345 <span class="ml-1 badge badge-light">
346 {numToSI(this.state.unreadInboxCount)}
352 {UserService.Instance.myUserInfo?.moderates.length > 0 && (
353 <ul class="navbar-nav my-2">
354 <li className="nav-item">
358 title={i18n.t("unread_reports", {
359 count: this.state.unreadReportCount,
360 formattedCount: numToSI(this.state.unreadReportCount),
363 <Icon icon="shield" />
364 {this.state.unreadReportCount > 0 && (
365 <span class="ml-1 badge badge-light">
366 {numToSI(this.state.unreadReportCount)}
373 <ul class="navbar-nav">
374 <li class="nav-item dropdown">
376 class="nav-link btn btn-link dropdown-toggle"
377 onClick={linkEvent(this, this.handleShowDropdown)}
380 aria-expanded="false"
383 {person.avatar && showAvatars() && (
384 <PictrsImage src={person.avatar} icon />
387 ? person.display_name
391 {this.state.showDropdown && (
392 <div class="dropdown-content">
393 <li className="nav-item">
395 className="nav-link btn btn-link"
396 onClick={linkEvent(this, this.handleGotoProfile)}
397 title={i18n.t("profile")}
399 <Icon icon="user" classes="mr-1" />
403 <li className="nav-item">
405 className="nav-link btn btn-link"
406 onClick={linkEvent(this, this.handleGotoSettings)}
407 title={i18n.t("settings")}
409 <Icon icon="settings" classes="mr-1" />
414 <hr class="dropdown-divider" />
416 <li className="nav-item">
418 className="nav-link btn btn-link"
419 onClick={linkEvent(this, this.handleLogoutClick)}
422 <Icon icon="log-out" classes="mr-1" />
432 <ul class="navbar-nav my-2">
433 <li className="nav-item">
435 className="nav-link btn btn-link"
436 onClick={linkEvent(this, this.handleGotoLogin)}
437 title={i18n.t("login")}
442 <li className="nav-item">
444 className="nav-link btn btn-link"
445 onClick={linkEvent(this, this.handleGotoSignup)}
446 title={i18n.t("sign_up")}
459 expandNavbar(i: Navbar) {
460 i.state.expanded = !i.state.expanded;
464 handleSearchParam(i: Navbar, event: any) {
465 i.state.searchParam = event.target.value;
469 handleSearchSubmit(i: Navbar, event: any) {
470 event.preventDefault();
474 handleSearchBtn(i: Navbar, event: any) {
475 event.preventDefault();
476 i.setState({ toggleSearch: true });
478 i.searchTextField.current.focus();
479 const offsetWidth = i.searchTextField.current.offsetWidth;
480 if (i.state.searchParam && offsetWidth > 100) {
485 handleSearchBlur(i: Navbar, event: any) {
486 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
487 i.state.toggleSearch = false;
492 handleLogoutClick(i: Navbar) {
493 i.setState({ showDropdown: false, expanded: false });
494 UserService.Instance.logout();
495 window.location.href = "/";
499 handleGotoSettings(i: Navbar) {
500 i.setState({ showDropdown: false, expanded: false });
501 i.context.router.history.push("/settings");
504 handleGotoProfile(i: Navbar) {
505 i.setState({ showDropdown: false, expanded: false });
506 i.context.router.history.push(
507 `/u/${UserService.Instance.myUserInfo.local_user_view.person.name}`
511 handleGotoCreatePost(i: Navbar) {
512 i.setState({ showDropdown: false, expanded: false });
513 i.context.router.history.push("/create_post", {
514 prevPath: i.currentLocation,
518 handleGotoCreateCommunity(i: Navbar) {
519 i.setState({ showDropdown: false, expanded: false });
520 i.context.router.history.push(`/create_community`);
523 handleGotoCommunities(i: Navbar) {
524 i.setState({ showDropdown: false, expanded: false });
525 i.context.router.history.push(`/communities`);
528 handleGotoHome(i: Navbar) {
529 i.setState({ showDropdown: false, expanded: false });
530 i.context.router.history.push(`/`);
533 handleGotoInbox(i: Navbar) {
534 i.setState({ showDropdown: false, expanded: false });
535 i.context.router.history.push(`/inbox`);
538 handleGotoReports(i: Navbar) {
539 i.setState({ showDropdown: false, expanded: false });
540 i.context.router.history.push(`/reports`);
543 handleGotoAdmin(i: Navbar) {
544 i.setState({ showDropdown: false, expanded: false });
545 i.context.router.history.push(`/admin`);
548 handleGotoLogin(i: Navbar) {
549 i.setState({ showDropdown: false, expanded: false });
550 i.context.router.history.push(`/login`);
553 handleGotoSignup(i: Navbar) {
554 i.setState({ showDropdown: false, expanded: false });
555 i.context.router.history.push(`/signup`);
558 handleShowDropdown(i: Navbar) {
559 i.state.showDropdown = !i.state.showDropdown;
563 parseMessage(msg: any) {
564 let op = wsUserOp(msg);
567 if (msg.error == "not_logged_in") {
568 UserService.Instance.logout();
572 } else if (msg.reconnect) {
573 console.log(i18n.t("websocket_reconnected"));
574 WebSocketService.Instance.send(
580 } else if (op == UserOperation.GetReplies) {
581 let data = wsJsonToRes<GetRepliesResponse>(msg).data;
582 let unreadReplies = data.replies.filter(r => !r.comment.read);
584 this.state.replies = unreadReplies;
585 this.state.unreadInboxCount = this.calculateUnreadInboxCount();
586 this.setState(this.state);
587 this.sendUnreadCount();
588 } else if (op == UserOperation.GetPersonMentions) {
589 let data = wsJsonToRes<GetPersonMentionsResponse>(msg).data;
590 let unreadMentions = data.mentions.filter(r => !r.comment.read);
592 this.state.mentions = unreadMentions;
593 this.state.unreadInboxCount = this.calculateUnreadInboxCount();
594 this.setState(this.state);
595 this.sendUnreadCount();
596 } else if (op == UserOperation.GetPrivateMessages) {
597 let data = wsJsonToRes<PrivateMessagesResponse>(msg).data;
598 let unreadMessages = data.private_messages.filter(
599 r => !r.private_message.read
602 this.state.messages = unreadMessages;
603 this.state.unreadInboxCount = this.calculateUnreadInboxCount();
604 this.setState(this.state);
605 this.sendUnreadCount();
606 } else if (op == UserOperation.GetReportCount) {
607 let data = wsJsonToRes<GetReportCountResponse>(msg).data;
608 this.state.unreadReportCount = data.post_reports + data.comment_reports;
609 this.setState(this.state);
610 this.sendReportUnread();
611 } else if (op == UserOperation.GetSite) {
612 // This is only called on a successful login
613 let data = wsJsonToRes<GetSiteResponse>(msg).data;
614 console.log(data.my_user);
615 UserService.Instance.myUserInfo = data.my_user;
617 UserService.Instance.myUserInfo.local_user_view.local_user.theme
619 i18n.changeLanguage(getLanguage());
620 this.state.isLoggedIn = true;
621 this.setState(this.state);
622 } else if (op == UserOperation.CreateComment) {
623 let data = wsJsonToRes<CommentResponse>(msg).data;
625 if (this.state.isLoggedIn) {
627 data.recipient_ids.includes(
628 UserService.Instance.myUserInfo.local_user_view.local_user.id
631 this.state.replies.push(data.comment_view);
632 this.state.unreadInboxCount++;
633 this.setState(this.state);
634 this.sendUnreadCount();
635 notifyComment(data.comment_view, this.context.router);
638 } else if (op == UserOperation.CreatePrivateMessage) {
639 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
641 if (this.state.isLoggedIn) {
643 data.private_message_view.recipient.id ==
644 UserService.Instance.myUserInfo.local_user_view.person.id
646 this.state.messages.push(data.private_message_view);
647 this.state.unreadInboxCount++;
648 this.setState(this.state);
649 this.sendUnreadCount();
650 notifyPrivateMessage(data.private_message_view, this.context.router);
657 // TODO we should just add a count call to the API for these, because this is a limited fetch,
658 // and it shouldn't have to fetch the actual content
659 if (this.currentLocation !== "/inbox") {
660 console.log("Fetching inbox unreads...");
661 let repliesForm: GetReplies = {
669 let personMentionsForm: GetPersonMentions = {
677 let privateMessagesForm: GetPrivateMessages = {
684 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
685 WebSocketService.Instance.send(
686 wsClient.getPersonMentions(personMentionsForm)
688 WebSocketService.Instance.send(
689 wsClient.getPrivateMessages(privateMessagesForm)
693 console.log("Fetching reports...");
695 let reportCountForm: GetReportCount = {
699 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
702 get currentLocation() {
703 return this.context.router.history.location.pathname;
707 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
711 UserService.Instance.unreadReportCountSub.next(
712 this.state.unreadReportCount
716 calculateUnreadInboxCount(): number {
718 this.state.replies.filter(r => !r.comment.read).length +
719 this.state.mentions.filter(r => !r.comment.read).length +
720 this.state.messages.filter(r => !r.private_message.read).length
724 get canAdmin(): boolean {
726 UserService.Instance.myUserInfo &&
727 this.props.site_res.admins
728 .map(a => a.person.id)
729 .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
733 get canCreateCommunity(): boolean {
735 this.props.site_res.site_view?.site.community_creation_admin_only;
736 return !adminOnly || this.canAdmin;
739 /// Listens for some websocket errors
741 let msg = i18n.t("websocket_disconnected");
742 WebSocketService.Instance.closeEventListener(() => {
747 requestNotificationPermission() {
748 if (UserService.Instance.myUserInfo) {
749 document.addEventListener("DOMContentLoaded", function () {
751 toast(i18n.t("notifications_error"), "danger");
755 if (Notification.permission !== "granted")
756 Notification.requestPermission();