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";
37 import { Icon } from "../common/icon";
38 import { PictrsImage } from "../common/pictrs-image";
40 interface NavbarProps {
41 site_res: GetSiteResponse;
44 interface NavbarState {
47 replies: CommentView[];
48 mentions: CommentView[];
49 messages: PrivateMessageView[];
52 toggleSearch: boolean;
53 showDropdown: boolean;
54 onSiteBanner?(url: string): any;
57 export class Navbar extends Component<NavbarProps, NavbarState> {
58 private wsSub: Subscription;
59 private userSub: Subscription;
60 private unreadCountSub: Subscription;
61 private searchTextField: RefObject<HTMLInputElement>;
62 emptyState: NavbarState = {
63 isLoggedIn: !!this.props.site_res.my_user,
75 constructor(props: any, context: any) {
76 super(props, context);
77 this.state = this.emptyState;
79 this.parseMessage = this.parseMessage.bind(this);
80 this.subscription = wsSubscribe(this.parseMessage);
84 // Subscribe to jwt changes
86 this.websocketEvents();
88 this.searchTextField = createRef();
89 console.log(`isLoggedIn = ${this.state.isLoggedIn}`);
91 // On the first load, check the unreads
92 if (this.state.isLoggedIn == false) {
93 // setTheme(data.my_user.theme, true);
94 // i18n.changeLanguage(getLanguage());
95 // i18n.changeLanguage('de');
97 this.requestNotificationPermission();
98 WebSocketService.Instance.send(
106 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
108 if (res !== undefined) {
109 this.requestNotificationPermission();
110 WebSocketService.Instance.send(
111 wsClient.getSite({ auth: authField() })
114 this.setState({ isLoggedIn: false });
118 // Subscribe to unread count changes
119 this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(
121 this.setState({ unreadCount: res });
127 componentWillUnmount() {
128 this.wsSub.unsubscribe();
129 this.userSub.unsubscribe();
130 this.unreadCountSub.unsubscribe();
134 const searchParam = this.state.searchParam;
135 this.setState({ searchParam: "" });
136 this.setState({ toggleSearch: false });
137 this.setState({ showDropdown: false, expanded: false });
138 if (searchParam === "") {
139 this.context.router.history.push(`/search/`);
141 const searchParamEncoded = encodeURIComponent(searchParam);
142 this.context.router.history.push(
143 `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
149 return this.navbar();
152 // TODO class active corresponding to current page
155 UserService.Instance.myUserInfo || this.props.site_res.my_user;
156 let person = myUserInfo?.local_user_view.person;
158 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
159 <div class="container">
160 {this.props.site_res.site_view && (
163 this.props.site_res.site_view.site.description ||
164 this.props.site_res.site_view.site.name
166 className="d-flex align-items-center navbar-brand mr-md-3 btn btn-link"
167 onClick={linkEvent(this, this.handleGotoHome)}
169 {this.props.site_res.site_view.site.icon && showAvatars() && (
171 src={this.props.site_res.site_view.site.icon}
175 {this.props.site_res.site_view.site.name}
178 {this.state.isLoggedIn && (
180 className="ml-auto p-1 navbar-toggler nav-link border-0 btn btn-link"
181 onClick={linkEvent(this, this.handleGotoInbox)}
182 title={i18n.t("inbox")}
185 {this.state.unreadCount > 0 && (
187 class="mx-1 badge badge-light"
188 aria-label={`${this.state.unreadCount} ${i18n.t(
192 {this.state.unreadCount}
198 class="navbar-toggler border-0 p-1"
201 onClick={linkEvent(this, this.expandNavbar)}
202 data-tippy-content={i18n.t("expand_here")}
207 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
209 <ul class="navbar-nav my-2 mr-auto">
210 <li class="nav-item">
212 className="nav-link btn btn-link"
213 onClick={linkEvent(this, this.handleGotoCommunities)}
214 title={i18n.t("communities")}
216 {i18n.t("communities")}
219 <li class="nav-item">
221 className="nav-link btn btn-link"
222 onClick={linkEvent(this, this.handleGotoCreatePost)}
223 title={i18n.t("create_post")}
225 {i18n.t("create_post")}
228 {this.canCreateCommunity && (
229 <li class="nav-item">
231 className="nav-link btn btn-link"
232 onClick={linkEvent(this, this.handleGotoCreateCommunity)}
233 title={i18n.t("create_community")}
235 {i18n.t("create_community")}
239 <li class="nav-item">
242 title={i18n.t("support_lemmy")}
243 href={supportLemmyUrl}
245 <Icon icon="heart" classes="small" />
249 <ul class="navbar-nav my-2">
251 <li className="nav-item">
253 className="nav-link btn btn-link"
254 onClick={linkEvent(this, this.handleGotoAdmin)}
255 title={i18n.t("admin_settings")}
257 <Icon icon="settings" />
262 {!this.context.router.history.location.pathname.match(
267 onSubmit={linkEvent(this, this.handleSearchSubmit)}
271 class={`form-control mr-0 search-input ${
272 this.state.toggleSearch ? "show-input" : "hide-input"
274 onInput={linkEvent(this, this.handleSearchParam)}
275 value={this.state.searchParam}
276 ref={this.searchTextField}
278 placeholder={i18n.t("search")}
279 onBlur={linkEvent(this, this.handleSearchBlur)}
281 <label class="sr-only" htmlFor="search-input">
286 onClick={linkEvent(this, this.handleSearchBtn)}
287 class="px-1 btn btn-link"
288 style="color: var(--gray)"
289 aria-label={i18n.t("search")}
291 <Icon icon="search" />
295 {this.state.isLoggedIn ? (
297 <ul class="navbar-nav my-2">
298 <li className="nav-item">
302 title={i18n.t("inbox")}
305 {this.state.unreadCount > 0 && (
307 class="ml-1 badge badge-light"
308 aria-label={`${this.state.unreadCount} ${i18n.t(
312 {this.state.unreadCount}
318 <ul class="navbar-nav">
319 <li class="nav-item dropdown">
321 class="nav-link btn btn-link dropdown-toggle"
322 onClick={linkEvent(this, this.handleShowDropdown)}
325 aria-expanded="false"
328 {person.avatar && showAvatars() && (
329 <PictrsImage src={person.avatar} icon />
332 ? person.display_name
336 {this.state.showDropdown && (
337 <div class="dropdown-content">
338 <li className="nav-item">
340 className="nav-link btn btn-link"
341 onClick={linkEvent(this, this.handleGotoProfile)}
342 title={i18n.t("profile")}
344 <Icon icon="user" classes="mr-1" />
348 <li className="nav-item">
350 className="nav-link btn btn-link"
351 onClick={linkEvent(this, this.handleGotoSettings)}
352 title={i18n.t("settings")}
354 <Icon icon="settings" classes="mr-1" />
359 <hr class="dropdown-divider" />
361 <li className="nav-item">
363 className="nav-link btn btn-link"
364 onClick={linkEvent(this, this.handleLogoutClick)}
367 <Icon icon="log-out" classes="mr-1" />
377 <ul class="navbar-nav my-2">
378 <li className="ml-2 nav-item">
380 className="btn btn-success"
381 onClick={linkEvent(this, this.handleGotoLogin)}
382 title={i18n.t("login_sign_up")}
384 {i18n.t("login_sign_up")}
395 expandNavbar(i: Navbar) {
396 i.state.expanded = !i.state.expanded;
400 handleSearchParam(i: Navbar, event: any) {
401 i.state.searchParam = event.target.value;
405 handleSearchSubmit(i: Navbar, event: any) {
406 event.preventDefault();
410 handleSearchBtn(i: Navbar, event: any) {
411 event.preventDefault();
412 i.setState({ toggleSearch: true });
414 i.searchTextField.current.focus();
415 const offsetWidth = i.searchTextField.current.offsetWidth;
416 if (i.state.searchParam && offsetWidth > 100) {
421 handleSearchBlur(i: Navbar, event: any) {
422 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
423 i.state.toggleSearch = false;
428 handleLogoutClick(i: Navbar) {
429 i.setState({ showDropdown: false, expanded: false });
430 UserService.Instance.logout();
431 i.context.router.history.push("/");
435 handleGotoSettings(i: Navbar) {
436 i.setState({ showDropdown: false, expanded: false });
437 i.context.router.history.push("/settings");
440 handleGotoProfile(i: Navbar) {
441 i.setState({ showDropdown: false, expanded: false });
442 i.context.router.history.push(
443 `/u/${UserService.Instance.myUserInfo.local_user_view.person.name}`
447 handleGotoCreatePost(i: Navbar) {
448 i.setState({ showDropdown: false, expanded: false });
449 i.context.router.history.push("/create_post", {
450 prevPath: i.currentLocation,
454 handleGotoCreateCommunity(i: Navbar) {
455 i.setState({ showDropdown: false, expanded: false });
456 i.context.router.history.push(`/create_community`);
459 handleGotoCommunities(i: Navbar) {
460 i.setState({ showDropdown: false, expanded: false });
461 i.context.router.history.push(`/communities`);
464 handleGotoHome(i: Navbar) {
465 i.setState({ showDropdown: false, expanded: false });
466 i.context.router.history.push(`/`);
469 handleGotoInbox(i: Navbar) {
470 i.setState({ showDropdown: false, expanded: false });
471 i.context.router.history.push(`/inbox`);
474 handleGotoAdmin(i: Navbar) {
475 i.setState({ showDropdown: false, expanded: false });
476 i.context.router.history.push(`/admin`);
479 handleGotoLogin(i: Navbar) {
480 i.setState({ showDropdown: false, expanded: false });
481 i.context.router.history.push(`/login`);
484 handleShowDropdown(i: Navbar) {
485 i.state.showDropdown = !i.state.showDropdown;
489 parseMessage(msg: any) {
490 let op = wsUserOp(msg);
493 if (msg.error == "not_logged_in") {
494 UserService.Instance.logout();
498 } else if (msg.reconnect) {
499 console.log(i18n.t("websocket_reconnected"));
500 WebSocketService.Instance.send(
506 } else if (op == UserOperation.GetReplies) {
507 let data = wsJsonToRes<GetRepliesResponse>(msg).data;
508 let unreadReplies = data.replies.filter(r => !r.comment.read);
510 this.state.replies = unreadReplies;
511 this.state.unreadCount = this.calculateUnreadCount();
512 this.setState(this.state);
513 this.sendUnreadCount();
514 } else if (op == UserOperation.GetPersonMentions) {
515 let data = wsJsonToRes<GetPersonMentionsResponse>(msg).data;
516 let unreadMentions = data.mentions.filter(r => !r.comment.read);
518 this.state.mentions = unreadMentions;
519 this.state.unreadCount = this.calculateUnreadCount();
520 this.setState(this.state);
521 this.sendUnreadCount();
522 } else if (op == UserOperation.GetPrivateMessages) {
523 let data = wsJsonToRes<PrivateMessagesResponse>(msg).data;
524 let unreadMessages = data.private_messages.filter(
525 r => !r.private_message.read
528 this.state.messages = unreadMessages;
529 this.state.unreadCount = this.calculateUnreadCount();
530 this.setState(this.state);
531 this.sendUnreadCount();
532 } else if (op == UserOperation.GetSite) {
533 // This is only called on a successful login
534 let data = wsJsonToRes<GetSiteResponse>(msg).data;
535 console.log(data.my_user);
536 UserService.Instance.myUserInfo = data.my_user;
538 UserService.Instance.myUserInfo.local_user_view.local_user.theme
540 i18n.changeLanguage(getLanguage());
541 this.state.isLoggedIn = true;
542 this.setState(this.state);
543 } else if (op == UserOperation.CreateComment) {
544 let data = wsJsonToRes<CommentResponse>(msg).data;
546 if (this.state.isLoggedIn) {
548 data.recipient_ids.includes(
549 UserService.Instance.myUserInfo.local_user_view.local_user.id
552 this.state.replies.push(data.comment_view);
553 this.state.unreadCount++;
554 this.setState(this.state);
555 this.sendUnreadCount();
556 notifyComment(data.comment_view, this.context.router);
559 } else if (op == UserOperation.CreatePrivateMessage) {
560 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
562 if (this.state.isLoggedIn) {
564 data.private_message_view.recipient.id ==
565 UserService.Instance.myUserInfo.local_user_view.person.id
567 this.state.messages.push(data.private_message_view);
568 this.state.unreadCount++;
569 this.setState(this.state);
570 this.sendUnreadCount();
571 notifyPrivateMessage(data.private_message_view, this.context.router);
578 console.log("Fetching unreads...");
579 let repliesForm: GetReplies = {
587 let personMentionsForm: GetPersonMentions = {
595 let privateMessagesForm: GetPrivateMessages = {
602 if (this.currentLocation !== "/inbox") {
603 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
604 WebSocketService.Instance.send(
605 wsClient.getPersonMentions(personMentionsForm)
607 WebSocketService.Instance.send(
608 wsClient.getPrivateMessages(privateMessagesForm)
613 get currentLocation() {
614 return this.context.router.history.location.pathname;
618 UserService.Instance.unreadCountSub.next(this.state.unreadCount);
621 calculateUnreadCount(): number {
623 this.state.replies.filter(r => !r.comment.read).length +
624 this.state.mentions.filter(r => !r.comment.read).length +
625 this.state.messages.filter(r => !r.private_message.read).length
629 get canAdmin(): boolean {
631 UserService.Instance.myUserInfo &&
632 this.props.site_res.admins
633 .map(a => a.person.id)
634 .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
638 get canCreateCommunity(): boolean {
640 this.props.site_res.site_view?.site.community_creation_admin_only;
641 return !adminOnly || this.canAdmin;
644 /// Listens for some websocket errors
646 let msg = i18n.t("websocket_disconnected");
647 WebSocketService.Instance.closeEventListener(() => {
652 requestNotificationPermission() {
653 if (UserService.Instance.myUserInfo) {
654 document.addEventListener("DOMContentLoaded", function () {
656 toast(i18n.t("notifications_error"), "danger");
660 if (Notification.permission !== "granted")
661 Notification.requestPermission();