1 import { Component, createRef, 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 {
42 unreadInboxCount: number;
43 unreadReportCount: number;
44 unreadApplicationCount: number;
45 onSiteBanner?(url: string): any;
48 function handleCollapseClick(i: Navbar) {
49 i.collapseButtonRef.current?.click();
52 function handleLogOut(i: Navbar) {
53 UserService.Instance.logout();
54 handleCollapseClick(i);
57 export class Navbar extends Component<NavbarProps, NavbarState> {
58 private wsSub: Subscription;
59 private userSub: Subscription;
60 private unreadInboxCountSub: Subscription;
61 private unreadReportCountSub: Subscription;
62 private unreadApplicationCountSub: Subscription;
63 state: NavbarState = {
66 unreadApplicationCount: 0,
69 collapseButtonRef = createRef<HTMLButtonElement>();
70 mobileMenuRef = createRef<HTMLDivElement>();
72 constructor(props: any, context: any) {
73 super(props, context);
75 this.parseMessage = this.parseMessage.bind(this);
76 this.subscription = wsSubscribe(this.parseMessage);
77 this.handleOutsideMenuClick = this.handleOutsideMenuClick.bind(this);
81 // Subscribe to jwt changes
83 // On the first load, check the unreads
84 const auth = myAuth(false);
85 if (auth && UserService.Instance.myUserInfo) {
86 this.requestNotificationPermission();
87 WebSocketService.Instance.send(
96 this.requestNotificationPermission();
98 // Subscribe to unread count changes
99 this.unreadInboxCountSub =
100 UserService.Instance.unreadInboxCountSub.subscribe(res => {
101 this.setState({ unreadInboxCount: res });
103 // Subscribe to unread report count changes
104 this.unreadReportCountSub =
105 UserService.Instance.unreadReportCountSub.subscribe(res => {
106 this.setState({ unreadReportCount: res });
108 // Subscribe to unread application count
109 this.unreadApplicationCountSub =
110 UserService.Instance.unreadApplicationCountSub.subscribe(res => {
111 this.setState({ unreadApplicationCount: res });
114 document.addEventListener("click", this.handleOutsideMenuClick);
118 componentWillUnmount() {
119 this.wsSub.unsubscribe();
120 this.userSub.unsubscribe();
121 this.unreadInboxCountSub.unsubscribe();
122 this.unreadReportCountSub.unsubscribe();
123 this.unreadApplicationCountSub.unsubscribe();
124 document.removeEventListener("click", this.handleOutsideMenuClick);
127 // TODO class active corresponding to current page
129 const siteView = this.props.siteRes?.site_view;
130 const person = UserService.Instance.myUserInfo?.local_user_view.person;
132 <nav className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3 container-lg">
135 title={siteView?.site.description ?? siteView?.site.name}
136 className="d-flex align-items-center navbar-brand mr-md-3"
137 onMouseUp={linkEvent(this, handleCollapseClick)}
139 {siteView?.site.icon && showAvatars() && (
140 <PictrsImage src={siteView.site.icon} icon />
142 {siteView?.site.name}
145 <ul className="navbar-nav d-flex flex-row ml-auto d-md-none">
146 <li className="nav-item">
149 className="p-1 nav-link border-0"
150 title={i18n.t("unread_messages", {
151 count: Number(this.state.unreadInboxCount),
152 formattedCount: numToSI(this.state.unreadInboxCount),
154 onMouseUp={linkEvent(this, handleCollapseClick)}
157 {this.state.unreadInboxCount > 0 && (
158 <span className="mx-1 badge badge-light">
159 {numToSI(this.state.unreadInboxCount)}
164 {this.moderatesSomething && (
165 <li className="nav-item">
168 className="p-1 nav-link border-0"
169 title={i18n.t("unread_reports", {
170 count: Number(this.state.unreadReportCount),
171 formattedCount: numToSI(this.state.unreadReportCount),
173 onMouseUp={linkEvent(this, handleCollapseClick)}
175 <Icon icon="shield" />
176 {this.state.unreadReportCount > 0 && (
177 <span className="mx-1 badge badge-light">
178 {numToSI(this.state.unreadReportCount)}
185 <li className="nav-item">
187 to="/registration_applications"
188 className="p-1 nav-link border-0"
189 title={i18n.t("unread_registration_applications", {
190 count: Number(this.state.unreadApplicationCount),
191 formattedCount: numToSI(this.state.unreadApplicationCount),
193 onMouseUp={linkEvent(this, handleCollapseClick)}
195 <Icon icon="clipboard" />
196 {this.state.unreadApplicationCount > 0 && (
197 <span className="mx-1 badge badge-light">
198 {numToSI(this.state.unreadApplicationCount)}
207 className="navbar-toggler border-0 p-1"
210 data-tippy-content={i18n.t("expand_here")}
211 data-bs-toggle="collapse"
212 data-bs-target="#navbarDropdown"
213 aria-controls="navbarDropdown"
214 aria-expanded="false"
215 ref={this.collapseButtonRef}
220 className="collapse navbar-collapse my-2"
222 ref={this.mobileMenuRef}
224 <ul className="mr-auto navbar-nav">
225 <li className="nav-item">
229 title={i18n.t("communities")}
230 onMouseUp={linkEvent(this, handleCollapseClick)}
232 {i18n.t("communities")}
235 <li className="nav-item">
236 {/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
239 pathname: "/create_post",
243 state: { prevPath: this.currentLocation },
246 title={i18n.t("create_post")}
247 onMouseUp={linkEvent(this, handleCollapseClick)}
249 {i18n.t("create_post")}
252 {this.props.siteRes && canCreateCommunity(this.props.siteRes) && (
253 <li className="nav-item">
255 to="/create_community"
257 title={i18n.t("create_community")}
258 onMouseUp={linkEvent(this, handleCollapseClick)}
260 {i18n.t("create_community")}
264 <li className="nav-item">
267 title={i18n.t("support_lemmy")}
268 href={donateLemmyUrl}
270 <Icon icon="heart" classes="small" />
274 <ul className="navbar-nav">
275 {!this.context.router.history.location.pathname.match(
278 <li className="nav-item">
282 title={i18n.t("search")}
283 onMouseUp={linkEvent(this, handleCollapseClick)}
285 <Icon icon="search" />
290 <li className="nav-item">
294 title={i18n.t("admin_settings")}
295 onMouseUp={linkEvent(this, handleCollapseClick)}
297 <Icon icon="settings" />
303 <li className="nav-item">
307 title={i18n.t("unread_messages", {
308 count: Number(this.state.unreadInboxCount),
309 formattedCount: numToSI(this.state.unreadInboxCount),
311 onMouseUp={linkEvent(this, handleCollapseClick)}
314 {this.state.unreadInboxCount > 0 && (
315 <span className="ml-1 badge badge-light">
316 {numToSI(this.state.unreadInboxCount)}
321 {this.moderatesSomething && (
322 <li className="nav-item">
326 title={i18n.t("unread_reports", {
327 count: Number(this.state.unreadReportCount),
328 formattedCount: numToSI(this.state.unreadReportCount),
330 onMouseUp={linkEvent(this, handleCollapseClick)}
332 <Icon icon="shield" />
333 {this.state.unreadReportCount > 0 && (
334 <span className="ml-1 badge badge-light">
335 {numToSI(this.state.unreadReportCount)}
342 <li className="nav-item">
344 to="/registration_applications"
346 title={i18n.t("unread_registration_applications", {
347 count: Number(this.state.unreadApplicationCount),
348 formattedCount: numToSI(
349 this.state.unreadApplicationCount
352 onMouseUp={linkEvent(this, handleCollapseClick)}
354 <Icon icon="clipboard" />
355 {this.state.unreadApplicationCount > 0 && (
356 <span className="mx-1 badge badge-light">
357 {numToSI(this.state.unreadApplicationCount)}
364 <div className="dropdown">
366 className="btn dropdown-toggle"
368 aria-expanded="false"
369 data-bs-toggle="dropdown"
371 {showAvatars() && person.avatar && (
372 <PictrsImage src={person.avatar} icon />
374 {person.display_name ?? person.name}
377 className="dropdown-menu"
378 style={{ "min-width": "fit-content" }}
382 to={`/u/${person.name}`}
383 className="dropdown-item px-2"
384 title={i18n.t("profile")}
385 onMouseUp={linkEvent(this, handleCollapseClick)}
387 <Icon icon="user" classes="mr-1" />
394 className="dropdown-item px-2"
395 title={i18n.t("settings")}
396 onMouseUp={linkEvent(this, handleCollapseClick)}
398 <Icon icon="settings" classes="mr-1" />
403 <hr className="dropdown-divider" />
407 className="dropdown-item btn btn-link px-2"
408 onClick={linkEvent(this, handleLogOut)}
410 <Icon icon="log-out" classes="mr-1" />
420 <li className="nav-item">
424 title={i18n.t("login")}
425 onMouseUp={linkEvent(this, handleCollapseClick)}
430 <li className="nav-item">
434 title={i18n.t("sign_up")}
435 onMouseUp={linkEvent(this, handleCollapseClick)}
448 handleOutsideMenuClick(event: MouseEvent) {
449 if (!this.mobileMenuRef.current?.contains(event.target as Node | null)) {
450 handleCollapseClick(this);
454 get moderatesSomething(): boolean {
455 const mods = UserService.Instance.myUserInfo?.moderates;
456 const moderatesS = (mods && mods.length > 0) || false;
457 return amAdmin() || moderatesS;
460 parseMessage(msg: any) {
461 const op = wsUserOp(msg);
464 if (msg.error == "not_logged_in") {
465 UserService.Instance.logout();
468 } else if (msg.reconnect) {
469 console.log(i18n.t("websocket_reconnected"));
470 const auth = myAuth(false);
471 if (UserService.Instance.myUserInfo && auth) {
472 WebSocketService.Instance.send(
479 } else if (op == UserOperation.GetUnreadCount) {
480 const data = wsJsonToRes<GetUnreadCountResponse>(msg);
482 unreadInboxCount: data.replies + data.mentions + data.private_messages,
484 this.sendUnreadCount();
485 } else if (op == UserOperation.GetReportCount) {
486 const data = wsJsonToRes<GetReportCountResponse>(msg);
490 data.comment_reports +
491 (data.private_message_reports ?? 0),
493 this.sendReportUnread();
494 } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
496 wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(msg);
497 this.setState({ unreadApplicationCount: data.registration_applications });
498 this.sendApplicationUnread();
499 } else if (op == UserOperation.CreateComment) {
500 const data = wsJsonToRes<CommentResponse>(msg);
501 const mui = UserService.Instance.myUserInfo;
504 data.recipient_ids.includes(mui.local_user_view.local_user.id)
507 unreadInboxCount: this.state.unreadInboxCount + 1,
509 this.sendUnreadCount();
510 notifyComment(data.comment_view, this.context.router);
512 } else if (op == UserOperation.CreatePrivateMessage) {
513 const data = wsJsonToRes<PrivateMessageResponse>(msg);
516 data.private_message_view.recipient.id ==
517 UserService.Instance.myUserInfo?.local_user_view.person.id
520 unreadInboxCount: this.state.unreadInboxCount + 1,
522 this.sendUnreadCount();
523 notifyPrivateMessage(data.private_message_view, this.context.router);
529 console.log("Fetching inbox unreads...");
531 const auth = myAuth();
533 const unreadForm: GetUnreadCount = {
536 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
538 console.log("Fetching reports...");
540 const reportCountForm: GetReportCount = {
543 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
546 console.log("Fetching applications...");
548 const applicationCountForm: GetUnreadRegistrationApplicationCount = {
551 WebSocketService.Instance.send(
552 wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
558 get currentLocation() {
559 return this.context.router.history.location.pathname;
563 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
567 UserService.Instance.unreadReportCountSub.next(
568 this.state.unreadReportCount
572 sendApplicationUnread() {
573 UserService.Instance.unreadApplicationCountSub.next(
574 this.state.unreadApplicationCount
578 requestNotificationPermission() {
579 if (UserService.Instance.myUserInfo) {
580 document.addEventListener("DOMContentLoaded", function () {
582 toast(i18n.t("notifications_error"), "danger");
586 if (Notification.permission !== "granted")
587 Notification.requestPermission();