1 import { Component, createRef, linkEvent, RefObject } from "inferno";
2 import { Link } from "inferno-router";
7 GetPersonMentionsResponse,
12 PrivateMessageResponse,
13 PrivateMessagesResponse,
17 } from "lemmy-js-client";
18 import { Subscription } from "rxjs";
19 import { i18n } from "../../i18next";
20 import { UserService, WebSocketService } from "../../services";
38 import { Icon } from "../common/icon";
39 import { PictrsImage } from "../common/pictrs-image";
41 interface NavbarProps {
42 site_res: GetSiteResponse;
45 interface NavbarState {
48 replies: CommentView[];
49 mentions: CommentView[];
50 messages: PrivateMessageView[];
53 toggleSearch: boolean;
54 showDropdown: boolean;
55 onSiteBanner?(url: string): any;
58 export class Navbar extends Component<NavbarProps, NavbarState> {
59 private wsSub: Subscription;
60 private userSub: Subscription;
61 private unreadCountSub: Subscription;
62 private searchTextField: RefObject<HTMLInputElement>;
63 emptyState: NavbarState = {
64 isLoggedIn: !!this.props.site_res.my_user,
76 constructor(props: any, context: any) {
77 super(props, context);
78 this.state = this.emptyState;
80 this.parseMessage = this.parseMessage.bind(this);
81 this.subscription = wsSubscribe(this.parseMessage);
85 // Subscribe to jwt changes
87 this.websocketEvents();
89 this.searchTextField = createRef();
90 console.log(`isLoggedIn = ${this.state.isLoggedIn}`);
92 // On the first load, check the unreads
93 if (this.state.isLoggedIn == false) {
94 // setTheme(data.my_user.theme, true);
95 // i18n.changeLanguage(getLanguage());
96 // i18n.changeLanguage('de');
98 this.requestNotificationPermission();
99 WebSocketService.Instance.send(
107 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
109 if (res !== undefined) {
110 this.requestNotificationPermission();
111 WebSocketService.Instance.send(
112 wsClient.getSite({ auth: authField() })
115 this.setState({ isLoggedIn: false });
119 // Subscribe to unread count changes
120 this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(
122 this.setState({ unreadCount: res });
128 componentWillUnmount() {
129 this.wsSub.unsubscribe();
130 this.userSub.unsubscribe();
131 this.unreadCountSub.unsubscribe();
135 const searchParam = this.state.searchParam;
136 this.setState({ searchParam: "" });
137 this.setState({ toggleSearch: false });
138 this.setState({ showDropdown: false, expanded: false });
139 if (searchParam === "") {
140 this.context.router.history.push(`/search/`);
142 const searchParamEncoded = encodeURIComponent(searchParam);
143 this.context.router.history.push(
144 `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
150 return this.navbar();
153 // TODO class active corresponding to current page
156 UserService.Instance.myUserInfo || this.props.site_res.my_user;
157 let person = myUserInfo?.local_user_view.person;
159 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
160 <div class="container">
161 {this.props.site_res.site_view && (
164 this.props.site_res.site_view.site.description ||
165 this.props.site_res.site_view.site.name
167 className="d-flex align-items-center navbar-brand mr-md-3 btn btn-link"
168 onClick={linkEvent(this, this.handleGotoHome)}
170 {this.props.site_res.site_view.site.icon && showAvatars() && (
172 src={this.props.site_res.site_view.site.icon}
176 {this.props.site_res.site_view.site.name}
179 {this.state.isLoggedIn && (
181 className="ml-auto p-1 navbar-toggler nav-link border-0 btn btn-link"
182 onClick={linkEvent(this, this.handleGotoInbox)}
183 title={i18n.t("inbox")}
186 {this.state.unreadCount > 0 && (
188 class="mx-1 badge badge-light"
189 aria-label={`${this.state.unreadCount} ${i18n.t(
193 {numToSI(this.state.unreadCount)}
199 class="navbar-toggler border-0 p-1"
202 onClick={linkEvent(this, this.expandNavbar)}
203 data-tippy-content={i18n.t("expand_here")}
208 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
210 <ul class="navbar-nav my-2 mr-auto">
211 <li class="nav-item">
213 className="nav-link btn btn-link"
214 onClick={linkEvent(this, this.handleGotoCommunities)}
215 title={i18n.t("communities")}
217 {i18n.t("communities")}
220 <li class="nav-item">
222 className="nav-link btn btn-link"
223 onClick={linkEvent(this, this.handleGotoCreatePost)}
224 title={i18n.t("create_post")}
226 {i18n.t("create_post")}
229 {this.canCreateCommunity && (
230 <li class="nav-item">
232 className="nav-link btn btn-link"
233 onClick={linkEvent(this, this.handleGotoCreateCommunity)}
234 title={i18n.t("create_community")}
236 {i18n.t("create_community")}
240 <li class="nav-item">
243 title={i18n.t("support_lemmy")}
244 href={donateLemmyUrl}
246 <Icon icon="heart" classes="small" />
250 <ul class="navbar-nav my-2">
252 <li className="nav-item">
254 className="nav-link btn btn-link"
255 onClick={linkEvent(this, this.handleGotoAdmin)}
256 title={i18n.t("admin_settings")}
258 <Icon icon="settings" />
263 {!this.context.router.history.location.pathname.match(
267 class="form-inline mr-2"
268 onSubmit={linkEvent(this, this.handleSearchSubmit)}
272 class={`form-control mr-0 search-input ${
273 this.state.toggleSearch ? "show-input" : "hide-input"
275 onInput={linkEvent(this, this.handleSearchParam)}
276 value={this.state.searchParam}
277 ref={this.searchTextField}
279 placeholder={i18n.t("search")}
280 onBlur={linkEvent(this, this.handleSearchBlur)}
282 <label class="sr-only" htmlFor="search-input">
287 onClick={linkEvent(this, this.handleSearchBtn)}
288 class="px-1 btn btn-link"
289 style="color: var(--gray)"
290 aria-label={i18n.t("search")}
292 <Icon icon="search" />
296 {this.state.isLoggedIn ? (
298 <ul class="navbar-nav my-2">
299 <li className="nav-item">
303 title={i18n.t("inbox")}
306 {this.state.unreadCount > 0 && (
308 class="ml-1 badge badge-light"
309 aria-label={`${this.state.unreadCount} ${i18n.t(
313 {numToSI(this.state.unreadCount)}
319 <ul class="navbar-nav">
320 <li class="nav-item dropdown">
322 class="nav-link btn btn-link dropdown-toggle"
323 onClick={linkEvent(this, this.handleShowDropdown)}
326 aria-expanded="false"
329 {person.avatar && showAvatars() && (
330 <PictrsImage src={person.avatar} icon />
333 ? person.display_name
337 {this.state.showDropdown && (
338 <div class="dropdown-content">
339 <li className="nav-item">
341 className="nav-link btn btn-link"
342 onClick={linkEvent(this, this.handleGotoProfile)}
343 title={i18n.t("profile")}
345 <Icon icon="user" classes="mr-1" />
349 <li className="nav-item">
351 className="nav-link btn btn-link"
352 onClick={linkEvent(this, this.handleGotoSettings)}
353 title={i18n.t("settings")}
355 <Icon icon="settings" classes="mr-1" />
360 <hr class="dropdown-divider" />
362 <li className="nav-item">
364 className="nav-link btn btn-link"
365 onClick={linkEvent(this, this.handleLogoutClick)}
368 <Icon icon="log-out" classes="mr-1" />
378 <ul class="navbar-nav my-2">
379 <li className="nav-item">
381 className="nav-link btn btn-link"
382 onClick={linkEvent(this, this.handleGotoLogin)}
383 title={i18n.t("login")}
388 <li className="nav-item">
390 className="nav-link btn btn-link"
391 onClick={linkEvent(this, this.handleGotoSignup)}
392 title={i18n.t("sign_up")}
405 expandNavbar(i: Navbar) {
406 i.state.expanded = !i.state.expanded;
410 handleSearchParam(i: Navbar, event: any) {
411 i.state.searchParam = event.target.value;
415 handleSearchSubmit(i: Navbar, event: any) {
416 event.preventDefault();
420 handleSearchBtn(i: Navbar, event: any) {
421 event.preventDefault();
422 i.setState({ toggleSearch: true });
424 i.searchTextField.current.focus();
425 const offsetWidth = i.searchTextField.current.offsetWidth;
426 if (i.state.searchParam && offsetWidth > 100) {
431 handleSearchBlur(i: Navbar, event: any) {
432 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
433 i.state.toggleSearch = false;
438 handleLogoutClick(i: Navbar) {
439 i.setState({ showDropdown: false, expanded: false });
440 UserService.Instance.logout();
441 window.location.href = "/";
445 handleGotoSettings(i: Navbar) {
446 i.setState({ showDropdown: false, expanded: false });
447 i.context.router.history.push("/settings");
450 handleGotoProfile(i: Navbar) {
451 i.setState({ showDropdown: false, expanded: false });
452 i.context.router.history.push(
453 `/u/${UserService.Instance.myUserInfo.local_user_view.person.name}`
457 handleGotoCreatePost(i: Navbar) {
458 i.setState({ showDropdown: false, expanded: false });
459 i.context.router.history.push("/create_post", {
460 prevPath: i.currentLocation,
464 handleGotoCreateCommunity(i: Navbar) {
465 i.setState({ showDropdown: false, expanded: false });
466 i.context.router.history.push(`/create_community`);
469 handleGotoCommunities(i: Navbar) {
470 i.setState({ showDropdown: false, expanded: false });
471 i.context.router.history.push(`/communities`);
474 handleGotoHome(i: Navbar) {
475 i.setState({ showDropdown: false, expanded: false });
476 i.context.router.history.push(`/`);
479 handleGotoInbox(i: Navbar) {
480 i.setState({ showDropdown: false, expanded: false });
481 i.context.router.history.push(`/inbox`);
484 handleGotoAdmin(i: Navbar) {
485 i.setState({ showDropdown: false, expanded: false });
486 i.context.router.history.push(`/admin`);
489 handleGotoLogin(i: Navbar) {
490 i.setState({ showDropdown: false, expanded: false });
491 i.context.router.history.push(`/login`);
494 handleGotoSignup(i: Navbar) {
495 i.setState({ showDropdown: false, expanded: false });
496 i.context.router.history.push(`/signup`);
499 handleShowDropdown(i: Navbar) {
500 i.state.showDropdown = !i.state.showDropdown;
504 parseMessage(msg: any) {
505 let op = wsUserOp(msg);
508 if (msg.error == "not_logged_in") {
509 UserService.Instance.logout();
513 } else if (msg.reconnect) {
514 console.log(i18n.t("websocket_reconnected"));
515 WebSocketService.Instance.send(
521 } else if (op == UserOperation.GetReplies) {
522 let data = wsJsonToRes<GetRepliesResponse>(msg).data;
523 let unreadReplies = data.replies.filter(r => !r.comment.read);
525 this.state.replies = unreadReplies;
526 this.state.unreadCount = this.calculateUnreadCount();
527 this.setState(this.state);
528 this.sendUnreadCount();
529 } else if (op == UserOperation.GetPersonMentions) {
530 let data = wsJsonToRes<GetPersonMentionsResponse>(msg).data;
531 let unreadMentions = data.mentions.filter(r => !r.comment.read);
533 this.state.mentions = unreadMentions;
534 this.state.unreadCount = this.calculateUnreadCount();
535 this.setState(this.state);
536 this.sendUnreadCount();
537 } else if (op == UserOperation.GetPrivateMessages) {
538 let data = wsJsonToRes<PrivateMessagesResponse>(msg).data;
539 let unreadMessages = data.private_messages.filter(
540 r => !r.private_message.read
543 this.state.messages = unreadMessages;
544 this.state.unreadCount = this.calculateUnreadCount();
545 this.setState(this.state);
546 this.sendUnreadCount();
547 } else if (op == UserOperation.GetSite) {
548 // This is only called on a successful login
549 let data = wsJsonToRes<GetSiteResponse>(msg).data;
550 console.log(data.my_user);
551 UserService.Instance.myUserInfo = data.my_user;
553 UserService.Instance.myUserInfo.local_user_view.local_user.theme
555 i18n.changeLanguage(getLanguage());
556 this.state.isLoggedIn = true;
557 this.setState(this.state);
558 } else if (op == UserOperation.CreateComment) {
559 let data = wsJsonToRes<CommentResponse>(msg).data;
561 if (this.state.isLoggedIn) {
563 data.recipient_ids.includes(
564 UserService.Instance.myUserInfo.local_user_view.local_user.id
567 this.state.replies.push(data.comment_view);
568 this.state.unreadCount++;
569 this.setState(this.state);
570 this.sendUnreadCount();
571 notifyComment(data.comment_view, this.context.router);
574 } else if (op == UserOperation.CreatePrivateMessage) {
575 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
577 if (this.state.isLoggedIn) {
579 data.private_message_view.recipient.id ==
580 UserService.Instance.myUserInfo.local_user_view.person.id
582 this.state.messages.push(data.private_message_view);
583 this.state.unreadCount++;
584 this.setState(this.state);
585 this.sendUnreadCount();
586 notifyPrivateMessage(data.private_message_view, this.context.router);
593 console.log("Fetching unreads...");
594 let repliesForm: GetReplies = {
602 let personMentionsForm: GetPersonMentions = {
610 let privateMessagesForm: GetPrivateMessages = {
617 if (this.currentLocation !== "/inbox") {
618 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
619 WebSocketService.Instance.send(
620 wsClient.getPersonMentions(personMentionsForm)
622 WebSocketService.Instance.send(
623 wsClient.getPrivateMessages(privateMessagesForm)
628 get currentLocation() {
629 return this.context.router.history.location.pathname;
633 UserService.Instance.unreadCountSub.next(this.state.unreadCount);
636 calculateUnreadCount(): number {
638 this.state.replies.filter(r => !r.comment.read).length +
639 this.state.mentions.filter(r => !r.comment.read).length +
640 this.state.messages.filter(r => !r.private_message.read).length
644 get canAdmin(): boolean {
646 UserService.Instance.myUserInfo &&
647 this.props.site_res.admins
648 .map(a => a.person.id)
649 .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
653 get canCreateCommunity(): boolean {
655 this.props.site_res.site_view?.site.community_creation_admin_only;
656 return !adminOnly || this.canAdmin;
659 /// Listens for some websocket errors
661 let msg = i18n.t("websocket_disconnected");
662 WebSocketService.Instance.closeEventListener(() => {
667 requestNotificationPermission() {
668 if (UserService.Instance.myUserInfo) {
669 document.addEventListener("DOMContentLoaded", function () {
671 toast(i18n.t("notifications_error"), "danger");
675 if (Notification.permission !== "granted")
676 Notification.requestPermission();