1 import { Component, createRef, linkEvent, RefObject } from "inferno";
2 import { NavLink } 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.searchTextField = createRef();
79 console.log(`isLoggedIn = ${this.state.isLoggedIn}`);
81 // On the first load, check the unreads
82 if (this.state.isLoggedIn == false) {
83 // setTheme(data.my_user.theme, true);
84 // i18n.changeLanguage(getLanguage());
85 // i18n.changeLanguage('de');
87 this.requestNotificationPermission();
88 WebSocketService.Instance.send(
96 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
98 if (res !== undefined) {
99 this.requestNotificationPermission();
100 WebSocketService.Instance.send(
101 wsClient.getSite({ auth: authField() })
104 this.setState({ isLoggedIn: false });
108 // Subscribe to unread count changes
109 this.unreadInboxCountSub =
110 UserService.Instance.unreadInboxCountSub.subscribe(res => {
111 this.setState({ unreadInboxCount: res });
113 // Subscribe to unread report count changes
114 this.unreadReportCountSub =
115 UserService.Instance.unreadReportCountSub.subscribe(res => {
116 this.setState({ unreadReportCount: res });
121 componentWillUnmount() {
122 this.wsSub.unsubscribe();
123 this.userSub.unsubscribe();
124 this.unreadInboxCountSub.unsubscribe();
125 this.unreadReportCountSub.unsubscribe();
129 const searchParam = this.state.searchParam;
130 this.setState({ searchParam: "" });
131 this.setState({ toggleSearch: false });
132 this.setState({ showDropdown: false, expanded: false });
133 if (searchParam === "") {
134 this.context.router.history.push(`/search/`);
136 const searchParamEncoded = encodeURIComponent(searchParam);
137 this.context.router.history.push(
138 `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
144 return this.navbar();
147 // TODO class active corresponding to current page
150 UserService.Instance.myUserInfo || this.props.site_res.my_user;
151 let person = myUserInfo?.local_user_view.person;
153 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
154 <div class="container">
155 {this.props.site_res.site_view && (
158 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
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"
165 {this.props.site_res.site_view.site.icon && showAvatars() && (
167 src={this.props.site_res.site_view.site.icon}
171 {this.props.site_res.site_view.site.name}
174 {this.state.isLoggedIn && (
176 <ul class="navbar-nav ml-auto">
177 <li className="nav-item">
180 className="p-1 navbar-toggler nav-link border-0"
181 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
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">
201 className="p-1 navbar-toggler nav-link border-0"
202 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
203 title={i18n.t("unread_reports", {
204 count: this.state.unreadReportCount,
205 formattedCount: numToSI(this.state.unreadReportCount),
208 <Icon icon="shield" />
209 {this.state.unreadReportCount > 0 && (
210 <span class="mx-1 badge badge-light">
211 {numToSI(this.state.unreadReportCount)}
221 class="navbar-toggler border-0 p-1"
224 onClick={linkEvent(this, this.handleToggleExpandNavbar)}
225 data-tippy-content={i18n.t("expand_here")}
230 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
232 <ul class="navbar-nav my-2 mr-auto">
233 <li class="nav-item">
237 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
238 title={i18n.t("communities")}
240 {i18n.t("communities")}
243 <li class="nav-item">
246 pathname: "/create_post",
247 prevPath: this.currentLocation,
250 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
251 title={i18n.t("create_post")}
253 {i18n.t("create_post")}
256 {this.canCreateCommunity && (
257 <li class="nav-item">
259 to="/create_community"
261 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
262 title={i18n.t("create_community")}
264 {i18n.t("create_community")}
268 <li class="nav-item">
271 title={i18n.t("support_lemmy")}
272 href={donateLemmyUrl}
274 <Icon icon="heart" classes="small" />
278 <ul class="navbar-nav my-2">
280 <li className="nav-item">
284 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
285 title={i18n.t("admin_settings")}
287 <Icon icon="settings" />
292 {!this.context.router.history.location.pathname.match(
296 class="form-inline mr-2"
297 onSubmit={linkEvent(this, this.handleSearchSubmit)}
301 class={`form-control mr-0 search-input ${
302 this.state.toggleSearch ? "show-input" : "hide-input"
304 onInput={linkEvent(this, this.handleSearchParam)}
305 value={this.state.searchParam}
306 ref={this.searchTextField}
308 placeholder={i18n.t("search")}
309 onBlur={linkEvent(this, this.handleSearchBlur)}
311 <label class="sr-only" htmlFor="search-input">
316 onClick={linkEvent(this, this.handleSearchBtn)}
317 class="px-1 btn btn-link"
318 style="color: var(--gray)"
319 aria-label={i18n.t("search")}
321 <Icon icon="search" />
325 {this.state.isLoggedIn ? (
327 <ul class="navbar-nav my-2">
328 <li className="nav-item">
332 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
333 title={i18n.t("unread_messages", {
334 count: this.state.unreadInboxCount,
335 formattedCount: numToSI(this.state.unreadInboxCount),
339 {this.state.unreadInboxCount > 0 && (
340 <span class="ml-1 badge badge-light">
341 {numToSI(this.state.unreadInboxCount)}
347 {UserService.Instance.myUserInfo?.moderates.length > 0 && (
348 <ul class="navbar-nav my-2">
349 <li className="nav-item">
353 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
354 title={i18n.t("unread_reports", {
355 count: this.state.unreadReportCount,
356 formattedCount: numToSI(this.state.unreadReportCount),
359 <Icon icon="shield" />
360 {this.state.unreadReportCount > 0 && (
361 <span class="ml-1 badge badge-light">
362 {numToSI(this.state.unreadReportCount)}
369 <ul class="navbar-nav">
370 <li class="nav-item dropdown">
372 class="nav-link btn btn-link dropdown-toggle"
373 onClick={linkEvent(this, this.handleToggleDropdown)}
376 aria-expanded="false"
379 {person.avatar && showAvatars() && (
380 <PictrsImage src={person.avatar} icon />
383 ? person.display_name
387 {this.state.showDropdown && (
389 class="dropdown-content"
390 onMouseLeave={linkEvent(
392 this.handleToggleDropdown
395 <li className="nav-item">
397 to={`/u/${UserService.Instance.myUserInfo.local_user_view.person.name}`}
399 title={i18n.t("profile")}
401 <Icon icon="user" classes="mr-1" />
405 <li className="nav-item">
409 title={i18n.t("settings")}
411 <Icon icon="settings" classes="mr-1" />
416 <hr class="dropdown-divider" />
418 <li className="nav-item">
420 className="nav-link btn btn-link"
421 onClick={linkEvent(this, this.handleLogoutClick)}
424 <Icon icon="log-out" classes="mr-1" />
434 <ul class="navbar-nav my-2">
435 <li className="nav-item">
439 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
440 title={i18n.t("login")}
445 <li className="nav-item">
449 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
450 title={i18n.t("sign_up")}
463 handleToggleExpandNavbar(i: Navbar) {
464 i.state.expanded = !i.state.expanded;
468 handleHideExpandNavbar(i: Navbar) {
469 i.setState({ expanded: false, showDropdown: false });
472 handleSearchParam(i: Navbar, event: any) {
473 i.state.searchParam = event.target.value;
477 handleSearchSubmit(i: Navbar, event: any) {
478 event.preventDefault();
482 handleSearchBtn(i: Navbar, event: any) {
483 event.preventDefault();
484 i.setState({ toggleSearch: true });
486 i.searchTextField.current.focus();
487 const offsetWidth = i.searchTextField.current.offsetWidth;
488 if (i.state.searchParam && offsetWidth > 100) {
493 handleSearchBlur(i: Navbar, event: any) {
494 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
495 i.state.toggleSearch = false;
500 handleLogoutClick(i: Navbar) {
501 i.setState({ showDropdown: false, expanded: false });
502 UserService.Instance.logout();
503 window.location.href = "/";
507 handleToggleDropdown(i: Navbar) {
508 i.state.showDropdown = !i.state.showDropdown;
512 parseMessage(msg: any) {
513 let op = wsUserOp(msg);
516 if (msg.error == "not_logged_in") {
517 UserService.Instance.logout();
521 } else if (msg.reconnect) {
522 console.log(i18n.t("websocket_reconnected"));
523 WebSocketService.Instance.send(
529 } else if (op == UserOperation.GetUnreadCount) {
530 let data = wsJsonToRes<GetUnreadCountResponse>(msg).data;
531 this.state.unreadInboxCount =
532 data.replies + data.mentions + data.private_messages;
533 this.setState(this.state);
534 this.sendUnreadCount();
535 } else if (op == UserOperation.GetReportCount) {
536 let data = wsJsonToRes<GetReportCountResponse>(msg).data;
537 this.state.unreadReportCount = data.post_reports + data.comment_reports;
538 this.setState(this.state);
539 this.sendReportUnread();
540 } else if (op == UserOperation.GetSite) {
541 // This is only called on a successful login
542 let data = wsJsonToRes<GetSiteResponse>(msg).data;
543 console.log(data.my_user);
544 UserService.Instance.myUserInfo = data.my_user;
546 UserService.Instance.myUserInfo.local_user_view.local_user.theme
548 i18n.changeLanguage(getLanguage());
549 this.state.isLoggedIn = true;
550 this.setState(this.state);
551 } else if (op == UserOperation.CreateComment) {
552 let data = wsJsonToRes<CommentResponse>(msg).data;
554 if (this.state.isLoggedIn) {
556 data.recipient_ids.includes(
557 UserService.Instance.myUserInfo.local_user_view.local_user.id
560 this.state.unreadInboxCount++;
561 this.setState(this.state);
562 this.sendUnreadCount();
563 notifyComment(data.comment_view, this.context.router);
566 } else if (op == UserOperation.CreatePrivateMessage) {
567 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
569 if (this.state.isLoggedIn) {
571 data.private_message_view.recipient.id ==
572 UserService.Instance.myUserInfo.local_user_view.person.id
574 this.state.unreadInboxCount++;
575 this.setState(this.state);
576 this.sendUnreadCount();
577 notifyPrivateMessage(data.private_message_view, this.context.router);
584 console.log("Fetching inbox unreads...");
586 let unreadForm: GetUnreadCount = {
590 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
592 console.log("Fetching reports...");
594 let reportCountForm: GetReportCount = {
598 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
601 get currentLocation() {
602 return this.context.router.history.location.pathname;
606 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
610 UserService.Instance.unreadReportCountSub.next(
611 this.state.unreadReportCount
615 get canAdmin(): boolean {
617 UserService.Instance.myUserInfo &&
618 this.props.site_res.admins
619 .map(a => a.person.id)
620 .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
624 get canCreateCommunity(): boolean {
626 this.props.site_res.site_view?.site.community_creation_admin_only;
627 return !adminOnly || this.canAdmin;
630 requestNotificationPermission() {
631 if (UserService.Instance.myUserInfo) {
632 document.addEventListener("DOMContentLoaded", function () {
634 toast(i18n.t("notifications_error"), "danger");
638 if (Notification.permission !== "granted")
639 Notification.requestPermission();