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 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
356 title={i18n.t("unread_reports", {
357 count: this.state.unreadReportCount,
358 formattedCount: numToSI(this.state.unreadReportCount),
361 <Icon icon="shield" />
362 {this.state.unreadReportCount > 0 && (
363 <span class="ml-1 badge badge-light">
364 {numToSI(this.state.unreadReportCount)}
371 <ul class="navbar-nav">
372 <li class="nav-item dropdown">
374 class="nav-link btn btn-link dropdown-toggle"
375 onClick={linkEvent(this, this.handleToggleDropdown)}
378 aria-expanded="false"
381 {person.avatar && showAvatars() && (
382 <PictrsImage src={person.avatar} icon />
385 ? person.display_name
389 {this.state.showDropdown && (
391 class="dropdown-content"
392 onMouseLeave={linkEvent(
394 this.handleToggleDropdown
397 <li className="nav-item">
399 to={`/u/${UserService.Instance.myUserInfo.local_user_view.person.name}`}
401 title={i18n.t("profile")}
403 <Icon icon="user" classes="mr-1" />
407 <li className="nav-item">
411 title={i18n.t("settings")}
413 <Icon icon="settings" classes="mr-1" />
418 <hr class="dropdown-divider" />
420 <li className="nav-item">
422 className="nav-link btn btn-link"
423 onClick={linkEvent(this, this.handleLogoutClick)}
426 <Icon icon="log-out" classes="mr-1" />
436 <ul class="navbar-nav my-2">
437 <li className="nav-item">
441 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
442 title={i18n.t("login")}
447 <li className="nav-item">
451 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
452 title={i18n.t("sign_up")}
465 handleToggleExpandNavbar(i: Navbar) {
466 i.state.expanded = !i.state.expanded;
470 handleHideExpandNavbar(i: Navbar) {
471 i.setState({ expanded: false, showDropdown: false });
474 handleSearchParam(i: Navbar, event: any) {
475 i.state.searchParam = event.target.value;
479 handleSearchSubmit(i: Navbar, event: any) {
480 event.preventDefault();
484 handleSearchBtn(i: Navbar, event: any) {
485 event.preventDefault();
486 i.setState({ toggleSearch: true });
488 i.searchTextField.current.focus();
489 const offsetWidth = i.searchTextField.current.offsetWidth;
490 if (i.state.searchParam && offsetWidth > 100) {
495 handleSearchBlur(i: Navbar, event: any) {
496 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
497 i.state.toggleSearch = false;
502 handleLogoutClick(i: Navbar) {
503 i.setState({ showDropdown: false, expanded: false });
504 UserService.Instance.logout();
505 window.location.href = "/";
509 handleToggleDropdown(i: Navbar) {
510 i.state.showDropdown = !i.state.showDropdown;
514 parseMessage(msg: any) {
515 let op = wsUserOp(msg);
518 if (msg.error == "not_logged_in") {
519 UserService.Instance.logout();
523 } else if (msg.reconnect) {
524 console.log(i18n.t("websocket_reconnected"));
525 WebSocketService.Instance.send(
531 } else if (op == UserOperation.GetUnreadCount) {
532 let data = wsJsonToRes<GetUnreadCountResponse>(msg).data;
533 this.state.unreadInboxCount =
534 data.replies + data.mentions + data.private_messages;
535 this.setState(this.state);
536 this.sendUnreadCount();
537 } else if (op == UserOperation.GetReportCount) {
538 let data = wsJsonToRes<GetReportCountResponse>(msg).data;
539 this.state.unreadReportCount = data.post_reports + data.comment_reports;
540 this.setState(this.state);
541 this.sendReportUnread();
542 } else if (op == UserOperation.GetSite) {
543 // This is only called on a successful login
544 let data = wsJsonToRes<GetSiteResponse>(msg).data;
545 console.log(data.my_user);
546 UserService.Instance.myUserInfo = data.my_user;
548 UserService.Instance.myUserInfo.local_user_view.local_user.theme
550 i18n.changeLanguage(getLanguage());
551 this.state.isLoggedIn = true;
552 this.setState(this.state);
553 } else if (op == UserOperation.CreateComment) {
554 let data = wsJsonToRes<CommentResponse>(msg).data;
556 if (this.state.isLoggedIn) {
558 data.recipient_ids.includes(
559 UserService.Instance.myUserInfo.local_user_view.local_user.id
562 this.state.unreadInboxCount++;
563 this.setState(this.state);
564 this.sendUnreadCount();
565 notifyComment(data.comment_view, this.context.router);
568 } else if (op == UserOperation.CreatePrivateMessage) {
569 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
571 if (this.state.isLoggedIn) {
573 data.private_message_view.recipient.id ==
574 UserService.Instance.myUserInfo.local_user_view.person.id
576 this.state.unreadInboxCount++;
577 this.setState(this.state);
578 this.sendUnreadCount();
579 notifyPrivateMessage(data.private_message_view, this.context.router);
586 console.log("Fetching inbox unreads...");
588 let unreadForm: GetUnreadCount = {
592 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
594 console.log("Fetching reports...");
596 let reportCountForm: GetReportCount = {
600 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
603 get currentLocation() {
604 return this.context.router.history.location.pathname;
608 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
612 UserService.Instance.unreadReportCountSub.next(
613 this.state.unreadReportCount
617 get canAdmin(): boolean {
619 UserService.Instance.myUserInfo &&
620 this.props.site_res.admins
621 .map(a => a.person.id)
622 .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
626 get canCreateCommunity(): boolean {
628 this.props.site_res.site_view?.site.community_creation_admin_only;
629 return !adminOnly || this.canAdmin;
632 /// Listens for some websocket errors
634 let msg = i18n.t("websocket_disconnected");
635 WebSocketService.Instance.closeEventListener(() => {
640 requestNotificationPermission() {
641 if (UserService.Instance.myUserInfo) {
642 document.addEventListener("DOMContentLoaded", function () {
644 toast(i18n.t("notifications_error"), "danger");
648 if (Notification.permission !== "granted")
649 Notification.requestPermission();