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 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
162 this.props.site_res.site_view.site.description ||
163 this.props.site_res.site_view.site.name
165 className="d-flex align-items-center navbar-brand mr-md-3"
167 {this.props.site_res.site_view.site.icon && showAvatars() && (
169 src={this.props.site_res.site_view.site.icon}
173 {this.props.site_res.site_view.site.name}
176 {this.state.isLoggedIn && (
178 <ul class="navbar-nav ml-auto">
179 <li className="nav-item">
182 className="p-1 navbar-toggler nav-link border-0"
183 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
184 title={i18n.t("unread_messages", {
185 count: this.state.unreadInboxCount,
186 formattedCount: numToSI(this.state.unreadInboxCount),
190 {this.state.unreadInboxCount > 0 && (
191 <span class="mx-1 badge badge-light">
192 {numToSI(this.state.unreadInboxCount)}
198 {UserService.Instance.myUserInfo?.moderates.length > 0 && (
199 <ul class="navbar-nav ml-1">
200 <li className="nav-item">
203 className="p-1 navbar-toggler nav-link border-0"
204 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
205 title={i18n.t("unread_reports", {
206 count: this.state.unreadReportCount,
207 formattedCount: numToSI(this.state.unreadReportCount),
210 <Icon icon="shield" />
211 {this.state.unreadReportCount > 0 && (
212 <span class="mx-1 badge badge-light">
213 {numToSI(this.state.unreadReportCount)}
223 class="navbar-toggler border-0 p-1"
226 onClick={linkEvent(this, this.handleToggleExpandNavbar)}
227 data-tippy-content={i18n.t("expand_here")}
232 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
234 <ul class="navbar-nav my-2 mr-auto">
235 <li class="nav-item">
239 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
240 title={i18n.t("communities")}
242 {i18n.t("communities")}
245 <li class="nav-item">
248 pathname: "/create_post",
249 prevPath: this.currentLocation,
252 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
253 title={i18n.t("create_post")}
255 {i18n.t("create_post")}
258 {this.canCreateCommunity && (
259 <li class="nav-item">
261 to="/create_community"
263 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
264 title={i18n.t("create_community")}
266 {i18n.t("create_community")}
270 <li class="nav-item">
273 title={i18n.t("support_lemmy")}
274 href={donateLemmyUrl}
276 <Icon icon="heart" classes="small" />
280 <ul class="navbar-nav my-2">
282 <li className="nav-item">
286 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
287 title={i18n.t("admin_settings")}
289 <Icon icon="settings" />
294 {!this.context.router.history.location.pathname.match(
298 class="form-inline mr-2"
299 onSubmit={linkEvent(this, this.handleSearchSubmit)}
303 class={`form-control mr-0 search-input ${
304 this.state.toggleSearch ? "show-input" : "hide-input"
306 onInput={linkEvent(this, this.handleSearchParam)}
307 value={this.state.searchParam}
308 ref={this.searchTextField}
310 placeholder={i18n.t("search")}
311 onBlur={linkEvent(this, this.handleSearchBlur)}
313 <label class="sr-only" htmlFor="search-input">
318 onClick={linkEvent(this, this.handleSearchBtn)}
319 class="px-1 btn btn-link"
320 style="color: var(--gray)"
321 aria-label={i18n.t("search")}
323 <Icon icon="search" />
327 {this.state.isLoggedIn ? (
329 <ul class="navbar-nav my-2">
330 <li className="nav-item">
334 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
335 title={i18n.t("unread_messages", {
336 count: this.state.unreadInboxCount,
337 formattedCount: numToSI(this.state.unreadInboxCount),
341 {this.state.unreadInboxCount > 0 && (
342 <span class="ml-1 badge badge-light">
343 {numToSI(this.state.unreadInboxCount)}
349 {UserService.Instance.myUserInfo?.moderates.length > 0 && (
350 <ul class="navbar-nav my-2">
351 <li className="nav-item">
355 onTouchEnd={linkEvent(
357 this.handleHideExpandNavbar
359 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
360 title={i18n.t("unread_reports", {
361 count: this.state.unreadReportCount,
362 formattedCount: numToSI(this.state.unreadReportCount),
365 <Icon icon="shield" />
366 {this.state.unreadReportCount > 0 && (
367 <span class="ml-1 badge badge-light">
368 {numToSI(this.state.unreadReportCount)}
375 <ul class="navbar-nav">
376 <li class="nav-item dropdown">
378 class="nav-link btn btn-link dropdown-toggle"
379 onClick={linkEvent(this, this.handleToggleDropdown)}
382 aria-expanded="false"
385 {person.avatar && showAvatars() && (
386 <PictrsImage src={person.avatar} icon />
389 ? person.display_name
393 {this.state.showDropdown && (
395 class="dropdown-content"
396 onMouseLeave={linkEvent(
398 this.handleToggleDropdown
401 <li className="nav-item">
403 to={`/u/${UserService.Instance.myUserInfo.local_user_view.person.name}`}
405 title={i18n.t("profile")}
407 <Icon icon="user" classes="mr-1" />
411 <li className="nav-item">
415 title={i18n.t("settings")}
417 <Icon icon="settings" classes="mr-1" />
422 <hr class="dropdown-divider" />
424 <li className="nav-item">
426 className="nav-link btn btn-link"
427 onClick={linkEvent(this, this.handleLogoutClick)}
430 <Icon icon="log-out" classes="mr-1" />
440 <ul class="navbar-nav my-2">
441 <li className="nav-item">
445 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
446 title={i18n.t("login")}
451 <li className="nav-item">
455 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
456 title={i18n.t("sign_up")}
469 handleToggleExpandNavbar(i: Navbar) {
470 i.state.expanded = !i.state.expanded;
474 handleHideExpandNavbar(i: Navbar) {
475 i.setState({ expanded: false, showDropdown: false });
478 handleSearchParam(i: Navbar, event: any) {
479 i.state.searchParam = event.target.value;
483 handleSearchSubmit(i: Navbar, event: any) {
484 event.preventDefault();
488 handleSearchBtn(i: Navbar, event: any) {
489 event.preventDefault();
490 i.setState({ toggleSearch: true });
492 i.searchTextField.current.focus();
493 const offsetWidth = i.searchTextField.current.offsetWidth;
494 if (i.state.searchParam && offsetWidth > 100) {
499 handleSearchBlur(i: Navbar, event: any) {
500 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
501 i.state.toggleSearch = false;
506 handleLogoutClick(i: Navbar) {
507 i.setState({ showDropdown: false, expanded: false });
508 UserService.Instance.logout();
509 window.location.href = "/";
513 handleToggleDropdown(i: Navbar) {
514 i.state.showDropdown = !i.state.showDropdown;
518 parseMessage(msg: any) {
519 let op = wsUserOp(msg);
522 if (msg.error == "not_logged_in") {
523 UserService.Instance.logout();
527 } else if (msg.reconnect) {
528 console.log(i18n.t("websocket_reconnected"));
529 WebSocketService.Instance.send(
535 } else if (op == UserOperation.GetUnreadCount) {
536 let data = wsJsonToRes<GetUnreadCountResponse>(msg).data;
537 this.state.unreadInboxCount =
538 data.replies + data.mentions + data.private_messages;
539 this.setState(this.state);
540 this.sendUnreadCount();
541 } else if (op == UserOperation.GetReportCount) {
542 let data = wsJsonToRes<GetReportCountResponse>(msg).data;
543 this.state.unreadReportCount = data.post_reports + data.comment_reports;
544 this.setState(this.state);
545 this.sendReportUnread();
546 } else if (op == UserOperation.GetSite) {
547 // This is only called on a successful login
548 let data = wsJsonToRes<GetSiteResponse>(msg).data;
549 console.log(data.my_user);
550 UserService.Instance.myUserInfo = data.my_user;
552 UserService.Instance.myUserInfo.local_user_view.local_user.theme
554 i18n.changeLanguage(getLanguage());
555 this.state.isLoggedIn = true;
556 this.setState(this.state);
557 } else if (op == UserOperation.CreateComment) {
558 let data = wsJsonToRes<CommentResponse>(msg).data;
560 if (this.state.isLoggedIn) {
562 data.recipient_ids.includes(
563 UserService.Instance.myUserInfo.local_user_view.local_user.id
566 this.state.unreadInboxCount++;
567 this.setState(this.state);
568 this.sendUnreadCount();
569 notifyComment(data.comment_view, this.context.router);
572 } else if (op == UserOperation.CreatePrivateMessage) {
573 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
575 if (this.state.isLoggedIn) {
577 data.private_message_view.recipient.id ==
578 UserService.Instance.myUserInfo.local_user_view.person.id
580 this.state.unreadInboxCount++;
581 this.setState(this.state);
582 this.sendUnreadCount();
583 notifyPrivateMessage(data.private_message_view, this.context.router);
590 console.log("Fetching inbox unreads...");
592 let unreadForm: GetUnreadCount = {
596 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
598 console.log("Fetching reports...");
600 let reportCountForm: GetReportCount = {
604 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
607 get currentLocation() {
608 return this.context.router.history.location.pathname;
612 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
616 UserService.Instance.unreadReportCountSub.next(
617 this.state.unreadReportCount
621 get canAdmin(): boolean {
623 UserService.Instance.myUserInfo &&
624 this.props.site_res.admins
625 .map(a => a.person.id)
626 .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
630 get canCreateCommunity(): boolean {
632 this.props.site_res.site_view?.site.community_creation_admin_only;
633 return !adminOnly || this.canAdmin;
636 /// Listens for some websocket errors
638 let msg = i18n.t("websocket_disconnected");
639 WebSocketService.Instance.closeEventListener(() => {
644 requestNotificationPermission() {
645 if (UserService.Instance.myUserInfo) {
646 document.addEventListener("DOMContentLoaded", function () {
648 toast(i18n.t("notifications_error"), "danger");
652 if (Notification.permission !== "granted")
653 Notification.requestPermission();