1 import { Component, linkEvent } from "inferno";
2 import { NavLink } from "inferno-router";
6 GetReportCountResponse,
9 GetUnreadCountResponse,
10 GetUnreadRegistrationApplicationCount,
11 GetUnreadRegistrationApplicationCountResponse,
12 PrivateMessageResponse,
16 } from "lemmy-js-client";
17 import { Subscription } from "rxjs";
18 import { i18n } from "../../i18next";
19 import { UserService, WebSocketService } from "../../services";
34 import { Icon } from "../common/icon";
35 import { PictrsImage } from "../common/pictrs-image";
37 interface NavbarProps {
38 siteRes?: GetSiteResponse;
41 interface NavbarState {
43 unreadInboxCount: number;
44 unreadReportCount: number;
45 unreadApplicationCount: number;
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 unreadApplicationCountSub: Subscription;
56 state: NavbarState = {
59 unreadApplicationCount: 0,
65 constructor(props: any, context: any) {
66 super(props, context);
68 this.parseMessage = this.parseMessage.bind(this);
69 this.subscription = wsSubscribe(this.parseMessage);
73 // Subscribe to jwt changes
75 // On the first load, check the unreads
76 let auth = myAuth(false);
77 if (auth && UserService.Instance.myUserInfo) {
78 this.requestNotificationPermission();
79 WebSocketService.Instance.send(
88 this.requestNotificationPermission();
90 // Subscribe to unread count changes
91 this.unreadInboxCountSub =
92 UserService.Instance.unreadInboxCountSub.subscribe(res => {
93 this.setState({ unreadInboxCount: res });
95 // Subscribe to unread report count changes
96 this.unreadReportCountSub =
97 UserService.Instance.unreadReportCountSub.subscribe(res => {
98 this.setState({ unreadReportCount: res });
100 // Subscribe to unread application count
101 this.unreadApplicationCountSub =
102 UserService.Instance.unreadApplicationCountSub.subscribe(res => {
103 this.setState({ unreadApplicationCount: res });
108 componentWillUnmount() {
109 this.wsSub.unsubscribe();
110 this.userSub.unsubscribe();
111 this.unreadInboxCountSub.unsubscribe();
112 this.unreadReportCountSub.unsubscribe();
113 this.unreadApplicationCountSub.unsubscribe();
117 return this.navbar();
120 // TODO class active corresponding to current page
122 let siteView = this.props.siteRes?.site_view;
123 let person = UserService.Instance.myUserInfo?.local_user_view.person;
125 <nav className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3">
126 <div className="container-lg">
129 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
130 title={siteView?.site.description ?? siteView?.site.name ?? "Lemmy"}
131 className="d-flex align-items-center navbar-brand mr-md-3"
133 {siteView?.site.icon && showAvatars() && (
134 <PictrsImage src={siteView.site.icon} icon />
136 {siteView?.site.name ?? "Lemmy"}
138 {UserService.Instance.myUserInfo && (
140 <ul className="navbar-nav ml-auto">
141 <li className="nav-item">
144 className="p-1 navbar-toggler nav-link border-0"
145 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
146 title={i18n.t("unread_messages", {
147 count: Number(this.state.unreadInboxCount),
148 formattedCount: numToSI(this.state.unreadInboxCount),
152 {this.state.unreadInboxCount > 0 && (
153 <span className="mx-1 badge badge-light">
154 {numToSI(this.state.unreadInboxCount)}
160 {this.moderatesSomething && (
161 <ul className="navbar-nav ml-1">
162 <li className="nav-item">
165 className="p-1 navbar-toggler nav-link border-0"
166 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
167 title={i18n.t("unread_reports", {
168 count: Number(this.state.unreadReportCount),
169 formattedCount: numToSI(this.state.unreadReportCount),
172 <Icon icon="shield" />
173 {this.state.unreadReportCount > 0 && (
174 <span className="mx-1 badge badge-light">
175 {numToSI(this.state.unreadReportCount)}
183 <ul className="navbar-nav ml-1">
184 <li className="nav-item">
186 to="/registration_applications"
187 className="p-1 navbar-toggler nav-link border-0"
188 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
189 title={i18n.t("unread_registration_applications", {
190 count: Number(this.state.unreadApplicationCount),
191 formattedCount: numToSI(
192 this.state.unreadApplicationCount
196 <Icon icon="clipboard" />
197 {this.state.unreadApplicationCount > 0 && (
198 <span className="mx-1 badge badge-light">
199 {numToSI(this.state.unreadApplicationCount)}
209 className="navbar-toggler border-0 p-1"
212 onClick={linkEvent(this, this.handleToggleExpandNavbar)}
213 data-tippy-content={i18n.t("expand_here")}
218 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
220 <ul className="navbar-nav my-2 mr-auto">
221 <li className="nav-item">
225 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
226 title={i18n.t("communities")}
228 {i18n.t("communities")}
231 <li className="nav-item">
232 {/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
235 pathname: "/create_post",
239 state: { prevPath: this.currentLocation },
242 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
243 title={i18n.t("create_post")}
245 {i18n.t("create_post")}
248 {this.props.siteRes && canCreateCommunity(this.props.siteRes) && (
249 <li className="nav-item">
251 to="/create_community"
253 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
254 title={i18n.t("create_community")}
256 {i18n.t("create_community")}
260 <li className="nav-item">
263 title={i18n.t("support_lemmy")}
264 href={donateLemmyUrl}
266 <Icon icon="heart" classes="small" />
270 <ul className="navbar-nav my-2">
272 <li className="nav-item">
276 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
277 title={i18n.t("admin_settings")}
279 <Icon icon="settings" />
284 {!this.context.router.history.location.pathname.match(
287 <ul className="navbar-nav">
288 <li className="nav-item">
292 title={i18n.t("search")}
294 <Icon icon="search" />
299 {UserService.Instance.myUserInfo ? (
301 <ul className="navbar-nav my-2">
302 <li className="nav-item">
306 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
307 title={i18n.t("unread_messages", {
308 count: Number(this.state.unreadInboxCount),
309 formattedCount: numToSI(this.state.unreadInboxCount),
313 {this.state.unreadInboxCount > 0 && (
314 <span className="ml-1 badge badge-light">
315 {numToSI(this.state.unreadInboxCount)}
321 {this.moderatesSomething && (
322 <ul className="navbar-nav my-2">
323 <li className="nav-item">
327 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
328 title={i18n.t("unread_reports", {
329 count: Number(this.state.unreadReportCount),
330 formattedCount: numToSI(this.state.unreadReportCount),
333 <Icon icon="shield" />
334 {this.state.unreadReportCount > 0 && (
335 <span className="ml-1 badge badge-light">
336 {numToSI(this.state.unreadReportCount)}
344 <ul className="navbar-nav my-2">
345 <li className="nav-item">
347 to="/registration_applications"
349 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
350 title={i18n.t("unread_registration_applications", {
351 count: Number(this.state.unreadApplicationCount),
352 formattedCount: numToSI(
353 this.state.unreadApplicationCount
357 <Icon icon="clipboard" />
358 {this.state.unreadApplicationCount > 0 && (
359 <span className="mx-1 badge badge-light">
360 {numToSI(this.state.unreadApplicationCount)}
368 <ul className="navbar-nav">
369 <li className="nav-item dropdown">
371 className="nav-link btn btn-link dropdown-toggle"
372 onClick={linkEvent(this, this.handleToggleDropdown)}
375 aria-expanded="false"
378 {showAvatars() && person.avatar && (
379 <PictrsImage src={person.avatar} icon />
381 {person.display_name ?? person.name}
384 {this.state.showDropdown && (
386 className="dropdown-content"
387 onMouseLeave={linkEvent(
389 this.handleToggleDropdown
392 <li className="nav-item">
394 to={`/u/${person.name}`}
396 title={i18n.t("profile")}
398 <Icon icon="user" classes="mr-1" />
402 <li className="nav-item">
406 title={i18n.t("settings")}
408 <Icon icon="settings" classes="mr-1" />
413 <hr className="dropdown-divider" />
415 <li className="nav-item">
417 className="nav-link btn btn-link"
418 onClick={linkEvent(this, this.handleLogoutClick)}
421 <Icon icon="log-out" classes="mr-1" />
432 <ul className="navbar-nav my-2">
433 <li className="nav-item">
437 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
438 title={i18n.t("login")}
443 <li className="nav-item">
447 onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
448 title={i18n.t("sign_up")}
461 get moderatesSomething(): boolean {
462 let mods = UserService.Instance.myUserInfo?.moderates;
463 let moderatesS = (mods && mods.length > 0) || false;
464 return amAdmin() || moderatesS;
467 handleToggleExpandNavbar(i: Navbar) {
468 i.setState({ expanded: !i.state.expanded });
471 handleHideExpandNavbar(i: Navbar) {
472 i.setState({ expanded: false, showDropdown: false });
475 handleLogoutClick(i: Navbar) {
476 i.setState({ showDropdown: false, expanded: false });
477 UserService.Instance.logout();
480 handleToggleDropdown(i: Navbar) {
481 i.setState({ showDropdown: !i.state.showDropdown });
484 parseMessage(msg: any) {
485 let op = wsUserOp(msg);
488 if (msg.error == "not_logged_in") {
489 UserService.Instance.logout();
492 } else if (msg.reconnect) {
493 console.log(i18n.t("websocket_reconnected"));
494 let auth = myAuth(false);
495 if (UserService.Instance.myUserInfo && auth) {
496 WebSocketService.Instance.send(
503 } else if (op == UserOperation.GetUnreadCount) {
504 let data = wsJsonToRes<GetUnreadCountResponse>(msg);
506 unreadInboxCount: data.replies + data.mentions + data.private_messages,
508 this.sendUnreadCount();
509 } else if (op == UserOperation.GetReportCount) {
510 let data = wsJsonToRes<GetReportCountResponse>(msg);
514 data.comment_reports +
515 (data.private_message_reports ?? 0),
517 this.sendReportUnread();
518 } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
520 wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(msg);
521 this.setState({ unreadApplicationCount: data.registration_applications });
522 this.sendApplicationUnread();
523 } else if (op == UserOperation.CreateComment) {
524 let data = wsJsonToRes<CommentResponse>(msg);
525 let mui = UserService.Instance.myUserInfo;
528 data.recipient_ids.includes(mui.local_user_view.local_user.id)
531 unreadInboxCount: this.state.unreadInboxCount + 1,
533 this.sendUnreadCount();
534 notifyComment(data.comment_view, this.context.router);
536 } else if (op == UserOperation.CreatePrivateMessage) {
537 let data = wsJsonToRes<PrivateMessageResponse>(msg);
540 data.private_message_view.recipient.id ==
541 UserService.Instance.myUserInfo?.local_user_view.person.id
544 unreadInboxCount: this.state.unreadInboxCount + 1,
546 this.sendUnreadCount();
547 notifyPrivateMessage(data.private_message_view, this.context.router);
553 console.log("Fetching inbox unreads...");
557 let unreadForm: GetUnreadCount = {
560 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
562 console.log("Fetching reports...");
564 let reportCountForm: GetReportCount = {
567 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
570 console.log("Fetching applications...");
572 let applicationCountForm: GetUnreadRegistrationApplicationCount = {
575 WebSocketService.Instance.send(
576 wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
582 get currentLocation() {
583 return this.context.router.history.location.pathname;
587 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
591 UserService.Instance.unreadReportCountSub.next(
592 this.state.unreadReportCount
596 sendApplicationUnread() {
597 UserService.Instance.unreadApplicationCountSub.next(
598 this.state.unreadApplicationCount
602 requestNotificationPermission() {
603 if (UserService.Instance.myUserInfo) {
604 document.addEventListener("DOMContentLoaded", function () {
606 toast(i18n.t("notifications_error"), "danger");
610 if (Notification.permission !== "granted")
611 Notification.requestPermission();