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 if (i.collapseButtonRef.current?.ariaExpanded === "true") {
50 i.collapseButtonRef.current?.click();
54 function handleLogOut() {
55 UserService.Instance.logout();
58 export class Navbar extends Component<NavbarProps, NavbarState> {
59 private wsSub: Subscription;
60 private userSub: Subscription;
61 private unreadInboxCountSub: Subscription;
62 private unreadReportCountSub: Subscription;
63 private unreadApplicationCountSub: Subscription;
64 state: NavbarState = {
67 unreadApplicationCount: 0,
70 collapseButtonRef = createRef<HTMLButtonElement>();
72 constructor(props: any, context: any) {
73 super(props, context);
75 this.parseMessage = this.parseMessage.bind(this);
76 this.subscription = wsSubscribe(this.parseMessage);
80 // Subscribe to jwt changes
82 // On the first load, check the unreads
83 let auth = myAuth(false);
84 if (auth && UserService.Instance.myUserInfo) {
85 this.requestNotificationPermission();
86 WebSocketService.Instance.send(
95 this.requestNotificationPermission();
97 // Subscribe to unread count changes
98 this.unreadInboxCountSub =
99 UserService.Instance.unreadInboxCountSub.subscribe(res => {
100 this.setState({ unreadInboxCount: res });
102 // Subscribe to unread report count changes
103 this.unreadReportCountSub =
104 UserService.Instance.unreadReportCountSub.subscribe(res => {
105 this.setState({ unreadReportCount: res });
107 // Subscribe to unread application count
108 this.unreadApplicationCountSub =
109 UserService.Instance.unreadApplicationCountSub.subscribe(res => {
110 this.setState({ unreadApplicationCount: res });
115 componentWillUnmount() {
116 this.wsSub.unsubscribe();
117 this.userSub.unsubscribe();
118 this.unreadInboxCountSub.unsubscribe();
119 this.unreadReportCountSub.unsubscribe();
120 this.unreadApplicationCountSub.unsubscribe();
123 // TODO class active corresponding to current page
125 const siteView = this.props.siteRes?.site_view;
126 const person = UserService.Instance.myUserInfo?.local_user_view.person;
128 <nav className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3 container-lg">
131 title={siteView?.site.description ?? siteView?.site.name}
132 className="d-flex align-items-center navbar-brand mr-md-3"
133 onMouseUp={linkEvent(this, handleCollapseClick)}
135 {siteView?.site.icon && showAvatars() && (
136 <PictrsImage src={siteView.site.icon} icon />
138 {siteView?.site.name}
141 <ul className="navbar-nav d-flex flex-row ml-auto d-md-none">
142 <li className="nav-item">
145 className="p-1 nav-link border-0"
146 title={i18n.t("unread_messages", {
147 count: Number(this.state.unreadInboxCount),
148 formattedCount: numToSI(this.state.unreadInboxCount),
150 onMouseUp={linkEvent(this, handleCollapseClick)}
153 {this.state.unreadInboxCount > 0 && (
154 <span className="mx-1 badge badge-light">
155 {numToSI(this.state.unreadInboxCount)}
160 {this.moderatesSomething && (
161 <li className="nav-item">
164 className="p-1 nav-link border-0"
165 title={i18n.t("unread_reports", {
166 count: Number(this.state.unreadReportCount),
167 formattedCount: numToSI(this.state.unreadReportCount),
169 onMouseUp={linkEvent(this, handleCollapseClick)}
171 <Icon icon="shield" />
172 {this.state.unreadReportCount > 0 && (
173 <span className="mx-1 badge badge-light">
174 {numToSI(this.state.unreadReportCount)}
181 <li className="nav-item">
183 to="/registration_applications"
184 className="p-1 nav-link border-0"
185 title={i18n.t("unread_registration_applications", {
186 count: Number(this.state.unreadApplicationCount),
187 formattedCount: numToSI(this.state.unreadApplicationCount),
189 onMouseUp={linkEvent(this, handleCollapseClick)}
191 <Icon icon="clipboard" />
192 {this.state.unreadApplicationCount > 0 && (
193 <span className="mx-1 badge badge-light">
194 {numToSI(this.state.unreadApplicationCount)}
203 className="navbar-toggler border-0 p-1"
206 data-tippy-content={i18n.t("expand_here")}
207 data-bs-toggle="collapse"
208 data-bs-target="#navbarDropdown"
209 aria-controls="navbarDropdown"
210 aria-expanded="false"
211 ref={this.collapseButtonRef}
215 <div className="collapse navbar-collapse my-2" id="navbarDropdown">
216 <ul className="mr-auto navbar-nav">
217 <li className="nav-item">
221 title={i18n.t("communities")}
222 onMouseUp={linkEvent(this, handleCollapseClick)}
224 {i18n.t("communities")}
227 <li className="nav-item">
228 {/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
231 pathname: "/create_post",
235 state: { prevPath: this.currentLocation },
238 title={i18n.t("create_post")}
239 onMouseUp={linkEvent(this, handleCollapseClick)}
241 {i18n.t("create_post")}
244 {this.props.siteRes && canCreateCommunity(this.props.siteRes) && (
245 <li className="nav-item">
247 to="/create_community"
249 title={i18n.t("create_community")}
250 onMouseUp={linkEvent(this, handleCollapseClick)}
252 {i18n.t("create_community")}
256 <li className="nav-item">
259 title={i18n.t("support_lemmy")}
260 href={donateLemmyUrl}
262 <Icon icon="heart" classes="small" />
266 <ul className="navbar-nav">
267 {!this.context.router.history.location.pathname.match(
270 <li className="nav-item">
274 title={i18n.t("search")}
275 onMouseUp={linkEvent(this, handleCollapseClick)}
277 <Icon icon="search" />
282 <li className="nav-item">
286 title={i18n.t("admin_settings")}
287 onMouseUp={linkEvent(this, handleCollapseClick)}
289 <Icon icon="settings" />
295 <li className="nav-item">
299 title={i18n.t("unread_messages", {
300 count: Number(this.state.unreadInboxCount),
301 formattedCount: numToSI(this.state.unreadInboxCount),
303 onMouseUp={linkEvent(this, handleCollapseClick)}
306 {this.state.unreadInboxCount > 0 && (
307 <span className="ml-1 badge badge-light">
308 {numToSI(this.state.unreadInboxCount)}
313 {this.moderatesSomething && (
314 <li className="nav-item">
318 title={i18n.t("unread_reports", {
319 count: Number(this.state.unreadReportCount),
320 formattedCount: numToSI(this.state.unreadReportCount),
322 onMouseUp={linkEvent(this, handleCollapseClick)}
324 <Icon icon="shield" />
325 {this.state.unreadReportCount > 0 && (
326 <span className="ml-1 badge badge-light">
327 {numToSI(this.state.unreadReportCount)}
334 <li className="nav-item">
336 to="/registration_applications"
338 title={i18n.t("unread_registration_applications", {
339 count: Number(this.state.unreadApplicationCount),
340 formattedCount: numToSI(
341 this.state.unreadApplicationCount
344 onMouseUp={linkEvent(this, handleCollapseClick)}
346 <Icon icon="clipboard" />
347 {this.state.unreadApplicationCount > 0 && (
348 <span className="mx-1 badge badge-light">
349 {numToSI(this.state.unreadApplicationCount)}
356 <div className="dropdown">
358 className="btn dropdown-toggle"
360 aria-expanded="false"
361 data-bs-toggle="dropdown"
363 {showAvatars() && person.avatar && (
364 <PictrsImage src={person.avatar} icon />
366 {person.display_name ?? person.name}
369 className="dropdown-menu"
370 style={{ "min-width": "fit-content" }}
374 to={`/u/${person.name}`}
375 className="dropdown-item px-2"
376 title={i18n.t("profile")}
377 onMouseUp={linkEvent(this, handleCollapseClick)}
379 <Icon icon="user" classes="mr-1" />
386 className="dropdown-item px-2"
387 title={i18n.t("settings")}
388 onMouseUp={linkEvent(this, handleCollapseClick)}
390 <Icon icon="settings" classes="mr-1" />
395 <hr className="dropdown-divider" />
399 className="dropdown-item btn btn-link px-2"
400 onClick={handleLogOut}
402 <Icon icon="log-out" classes="mr-1" />
412 <li className="nav-item">
416 title={i18n.t("login")}
417 onMouseUp={linkEvent(this, handleCollapseClick)}
422 <li className="nav-item">
426 title={i18n.t("sign_up")}
427 onMouseUp={linkEvent(this, handleCollapseClick)}
440 get moderatesSomething(): boolean {
441 let mods = UserService.Instance.myUserInfo?.moderates;
442 let moderatesS = (mods && mods.length > 0) || false;
443 return amAdmin() || moderatesS;
446 parseMessage(msg: any) {
447 let op = wsUserOp(msg);
450 if (msg.error == "not_logged_in") {
451 UserService.Instance.logout();
454 } else if (msg.reconnect) {
455 console.log(i18n.t("websocket_reconnected"));
456 let auth = myAuth(false);
457 if (UserService.Instance.myUserInfo && auth) {
458 WebSocketService.Instance.send(
465 } else if (op == UserOperation.GetUnreadCount) {
466 let data = wsJsonToRes<GetUnreadCountResponse>(msg);
468 unreadInboxCount: data.replies + data.mentions + data.private_messages,
470 this.sendUnreadCount();
471 } else if (op == UserOperation.GetReportCount) {
472 let data = wsJsonToRes<GetReportCountResponse>(msg);
476 data.comment_reports +
477 (data.private_message_reports ?? 0),
479 this.sendReportUnread();
480 } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
482 wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(msg);
483 this.setState({ unreadApplicationCount: data.registration_applications });
484 this.sendApplicationUnread();
485 } else if (op == UserOperation.CreateComment) {
486 let data = wsJsonToRes<CommentResponse>(msg);
487 let mui = UserService.Instance.myUserInfo;
490 data.recipient_ids.includes(mui.local_user_view.local_user.id)
493 unreadInboxCount: this.state.unreadInboxCount + 1,
495 this.sendUnreadCount();
496 notifyComment(data.comment_view, this.context.router);
498 } else if (op == UserOperation.CreatePrivateMessage) {
499 let data = wsJsonToRes<PrivateMessageResponse>(msg);
502 data.private_message_view.recipient.id ==
503 UserService.Instance.myUserInfo?.local_user_view.person.id
506 unreadInboxCount: this.state.unreadInboxCount + 1,
508 this.sendUnreadCount();
509 notifyPrivateMessage(data.private_message_view, this.context.router);
515 console.log("Fetching inbox unreads...");
519 let unreadForm: GetUnreadCount = {
522 WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
524 console.log("Fetching reports...");
526 let reportCountForm: GetReportCount = {
529 WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
532 console.log("Fetching applications...");
534 let applicationCountForm: GetUnreadRegistrationApplicationCount = {
537 WebSocketService.Instance.send(
538 wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
544 get currentLocation() {
545 return this.context.router.history.location.pathname;
549 UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
553 UserService.Instance.unreadReportCountSub.next(
554 this.state.unreadReportCount
558 sendApplicationUnread() {
559 UserService.Instance.unreadApplicationCountSub.next(
560 this.state.unreadApplicationCount
564 requestNotificationPermission() {
565 if (UserService.Instance.myUserInfo) {
566 document.addEventListener("DOMContentLoaded", function () {
568 toast(i18n.t("notifications_error"), "danger");
572 if (Notification.permission !== "granted")
573 Notification.requestPermission();