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={supportLemmyUrl}
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(
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="ml-2 nav-item">
381 className="btn btn-success"
382 onClick={linkEvent(this, this.handleGotoLogin)}
383 title={i18n.t("login_sign_up")}
385 {i18n.t("login_sign_up")}
396 expandNavbar(i: Navbar) {
397 i.state.expanded = !i.state.expanded;
401 handleSearchParam(i: Navbar, event: any) {
402 i.state.searchParam = event.target.value;
406 handleSearchSubmit(i: Navbar, event: any) {
407 event.preventDefault();
411 handleSearchBtn(i: Navbar, event: any) {
412 event.preventDefault();
413 i.setState({ toggleSearch: true });
415 i.searchTextField.current.focus();
416 const offsetWidth = i.searchTextField.current.offsetWidth;
417 if (i.state.searchParam && offsetWidth > 100) {
422 handleSearchBlur(i: Navbar, event: any) {
423 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
424 i.state.toggleSearch = false;
429 handleLogoutClick(i: Navbar) {
430 i.setState({ showDropdown: false, expanded: false });
431 UserService.Instance.logout();
432 i.context.router.history.push("/");
436 handleGotoSettings(i: Navbar) {
437 i.setState({ showDropdown: false, expanded: false });
438 i.context.router.history.push("/settings");
441 handleGotoProfile(i: Navbar) {
442 i.setState({ showDropdown: false, expanded: false });
443 i.context.router.history.push(
444 `/u/${UserService.Instance.myUserInfo.local_user_view.person.name}`
448 handleGotoCreatePost(i: Navbar) {
449 i.setState({ showDropdown: false, expanded: false });
450 i.context.router.history.push("/create_post", {
451 prevPath: i.currentLocation,
455 handleGotoCreateCommunity(i: Navbar) {
456 i.setState({ showDropdown: false, expanded: false });
457 i.context.router.history.push(`/create_community`);
460 handleGotoCommunities(i: Navbar) {
461 i.setState({ showDropdown: false, expanded: false });
462 i.context.router.history.push(`/communities`);
465 handleGotoHome(i: Navbar) {
466 i.setState({ showDropdown: false, expanded: false });
467 i.context.router.history.push(`/`);
470 handleGotoInbox(i: Navbar) {
471 i.setState({ showDropdown: false, expanded: false });
472 i.context.router.history.push(`/inbox`);
475 handleGotoAdmin(i: Navbar) {
476 i.setState({ showDropdown: false, expanded: false });
477 i.context.router.history.push(`/admin`);
480 handleGotoLogin(i: Navbar) {
481 i.setState({ showDropdown: false, expanded: false });
482 i.context.router.history.push(`/login`);
485 handleShowDropdown(i: Navbar) {
486 i.state.showDropdown = !i.state.showDropdown;
490 parseMessage(msg: any) {
491 let op = wsUserOp(msg);
494 if (msg.error == "not_logged_in") {
495 UserService.Instance.logout();
499 } else if (msg.reconnect) {
500 console.log(i18n.t("websocket_reconnected"));
501 WebSocketService.Instance.send(
507 } else if (op == UserOperation.GetReplies) {
508 let data = wsJsonToRes<GetRepliesResponse>(msg).data;
509 let unreadReplies = data.replies.filter(r => !r.comment.read);
511 this.state.replies = unreadReplies;
512 this.state.unreadCount = this.calculateUnreadCount();
513 this.setState(this.state);
514 this.sendUnreadCount();
515 } else if (op == UserOperation.GetPersonMentions) {
516 let data = wsJsonToRes<GetPersonMentionsResponse>(msg).data;
517 let unreadMentions = data.mentions.filter(r => !r.comment.read);
519 this.state.mentions = unreadMentions;
520 this.state.unreadCount = this.calculateUnreadCount();
521 this.setState(this.state);
522 this.sendUnreadCount();
523 } else if (op == UserOperation.GetPrivateMessages) {
524 let data = wsJsonToRes<PrivateMessagesResponse>(msg).data;
525 let unreadMessages = data.private_messages.filter(
526 r => !r.private_message.read
529 this.state.messages = unreadMessages;
530 this.state.unreadCount = this.calculateUnreadCount();
531 this.setState(this.state);
532 this.sendUnreadCount();
533 } else if (op == UserOperation.GetSite) {
534 // This is only called on a successful login
535 let data = wsJsonToRes<GetSiteResponse>(msg).data;
536 console.log(data.my_user);
537 UserService.Instance.myUserInfo = data.my_user;
539 UserService.Instance.myUserInfo.local_user_view.local_user.theme
541 i18n.changeLanguage(getLanguage());
542 this.state.isLoggedIn = true;
543 this.setState(this.state);
544 } else if (op == UserOperation.CreateComment) {
545 let data = wsJsonToRes<CommentResponse>(msg).data;
547 if (this.state.isLoggedIn) {
549 data.recipient_ids.includes(
550 UserService.Instance.myUserInfo.local_user_view.local_user.id
553 this.state.replies.push(data.comment_view);
554 this.state.unreadCount++;
555 this.setState(this.state);
556 this.sendUnreadCount();
557 notifyComment(data.comment_view, this.context.router);
560 } else if (op == UserOperation.CreatePrivateMessage) {
561 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
563 if (this.state.isLoggedIn) {
565 data.private_message_view.recipient.id ==
566 UserService.Instance.myUserInfo.local_user_view.person.id
568 this.state.messages.push(data.private_message_view);
569 this.state.unreadCount++;
570 this.setState(this.state);
571 this.sendUnreadCount();
572 notifyPrivateMessage(data.private_message_view, this.context.router);
579 console.log("Fetching unreads...");
580 let repliesForm: GetReplies = {
588 let personMentionsForm: GetPersonMentions = {
596 let privateMessagesForm: GetPrivateMessages = {
603 if (this.currentLocation !== "/inbox") {
604 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
605 WebSocketService.Instance.send(
606 wsClient.getPersonMentions(personMentionsForm)
608 WebSocketService.Instance.send(
609 wsClient.getPrivateMessages(privateMessagesForm)
614 get currentLocation() {
615 return this.context.router.history.location.pathname;
619 UserService.Instance.unreadCountSub.next(this.state.unreadCount);
622 calculateUnreadCount(): number {
624 this.state.replies.filter(r => !r.comment.read).length +
625 this.state.mentions.filter(r => !r.comment.read).length +
626 this.state.messages.filter(r => !r.private_message.read).length
630 get canAdmin(): boolean {
632 UserService.Instance.myUserInfo &&
633 this.props.site_res.admins
634 .map(a => a.person.id)
635 .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
639 get canCreateCommunity(): boolean {
641 this.props.site_res.site_view?.site.community_creation_admin_only;
642 return !adminOnly || this.canAdmin;
645 /// Listens for some websocket errors
647 let msg = i18n.t("websocket_disconnected");
648 WebSocketService.Instance.closeEventListener(() => {
653 requestNotificationPermission() {
654 if (UserService.Instance.myUserInfo) {
655 document.addEventListener("DOMContentLoaded", function () {
657 toast(i18n.t("notifications_error"), "danger");
661 if (Notification.permission !== "granted")
662 Notification.requestPermission();