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 onSiteBanner?(url: string): any;
56 export class Navbar extends Component<NavbarProps, NavbarState> {
57 private wsSub: Subscription;
58 private userSub: Subscription;
59 private unreadCountSub: Subscription;
60 private searchTextField: RefObject<HTMLInputElement>;
61 emptyState: NavbarState = {
62 isLoggedIn: !!this.props.site_res.my_user,
73 constructor(props: any, context: any) {
74 super(props, context);
75 this.state = this.emptyState;
77 this.parseMessage = this.parseMessage.bind(this);
78 this.subscription = wsSubscribe(this.parseMessage);
82 // Subscribe to jwt changes
84 this.websocketEvents();
86 this.searchTextField = createRef();
87 console.log(`isLoggedIn = ${this.state.isLoggedIn}`);
89 // On the first load, check the unreads
90 if (this.state.isLoggedIn == false) {
91 // setTheme(data.my_user.theme, true);
92 // i18n.changeLanguage(getLanguage());
93 // i18n.changeLanguage('de');
95 this.requestNotificationPermission();
96 WebSocketService.Instance.send(
104 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
106 if (res !== undefined) {
107 this.requestNotificationPermission();
108 WebSocketService.Instance.send(
109 wsClient.getSite({ auth: authField() })
112 this.setState({ isLoggedIn: false });
116 // Subscribe to unread count changes
117 this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(
119 this.setState({ unreadCount: res });
125 handleSearchParam(i: Navbar, event: any) {
126 i.state.searchParam = event.target.value;
131 const searchParam = this.state.searchParam;
132 this.setState({ searchParam: "" });
133 this.setState({ toggleSearch: false });
134 if (searchParam === "") {
135 this.context.router.history.push(`/search/`);
137 const searchParamEncoded = encodeURIComponent(searchParam);
138 this.context.router.history.push(
139 `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
144 handleSearchSubmit(i: Navbar, event: any) {
145 event.preventDefault();
149 handleSearchBtn(i: Navbar, event: any) {
150 event.preventDefault();
151 i.setState({ toggleSearch: true });
153 i.searchTextField.current.focus();
154 const offsetWidth = i.searchTextField.current.offsetWidth;
155 if (i.state.searchParam && offsetWidth > 100) {
160 handleSearchBlur(i: Navbar, event: any) {
161 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
162 i.state.toggleSearch = false;
168 return this.navbar();
171 componentWillUnmount() {
172 this.wsSub.unsubscribe();
173 this.userSub.unsubscribe();
174 this.unreadCountSub.unsubscribe();
177 // TODO class active corresponding to current page
180 UserService.Instance.localUserView || this.props.site_res.my_user;
182 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
183 <div class="container">
184 {this.props.site_res.site_view && (
187 this.props.site_res.site_view.site.description ||
188 this.props.site_res.site_view.site.name
190 className="d-flex align-items-center navbar-brand mr-md-3"
193 {this.props.site_res.site_view.site.icon && showAvatars() && (
195 src={this.props.site_res.site_view.site.icon}
199 {this.props.site_res.site_view.site.name}
202 {this.state.isLoggedIn && (
204 className="ml-auto p-1 navbar-toggler nav-link border-0"
206 title={i18n.t("inbox")}
209 {this.state.unreadCount > 0 && (
211 class="mx-1 badge badge-light"
212 aria-label={`${this.state.unreadCount} ${i18n.t(
216 {this.state.unreadCount}
222 class="navbar-toggler border-0 p-1"
225 onClick={linkEvent(this, this.expandNavbar)}
226 data-tippy-content={i18n.t("expand_here")}
231 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
233 <ul class="navbar-nav my-2 mr-auto">
234 <li class="nav-item">
238 title={i18n.t("communities")}
240 {i18n.t("communities")}
243 <li class="nav-item">
247 pathname: "/create_post",
248 state: { prevPath: this.currentLocation },
250 title={i18n.t("create_post")}
252 {i18n.t("create_post")}
255 {this.canCreateCommunity && (
256 <li class="nav-item">
259 to="/create_community"
260 title={i18n.t("create_community")}
262 {i18n.t("create_community")}
266 <li class="nav-item">
269 title={i18n.t("support_lemmy")}
270 href={supportLemmyUrl}
272 <Icon icon="heart" classes="small" />
276 <ul class="navbar-nav my-2">
278 <li className="nav-item">
282 title={i18n.t("admin_settings")}
284 <Icon icon="settings" />
289 {!this.context.router.history.location.pathname.match(
294 onSubmit={linkEvent(this, this.handleSearchSubmit)}
298 class={`form-control mr-0 search-input ${
299 this.state.toggleSearch ? "show-input" : "hide-input"
301 onInput={linkEvent(this, this.handleSearchParam)}
302 value={this.state.searchParam}
303 ref={this.searchTextField}
305 placeholder={i18n.t("search")}
306 onBlur={linkEvent(this, this.handleSearchBlur)}
308 <label class="sr-only" htmlFor="search-input">
313 onClick={linkEvent(this, this.handleSearchBtn)}
314 class="px-1 btn btn-link"
315 style="color: var(--gray)"
316 aria-label={i18n.t("search")}
318 <Icon icon="search" />
322 {this.state.isLoggedIn ? (
324 <ul class="navbar-nav my-2">
325 <li className="nav-item">
329 title={i18n.t("inbox")}
332 {this.state.unreadCount > 0 && (
334 class="ml-1 badge badge-light"
335 aria-label={`${this.state.unreadCount} ${i18n.t(
339 {this.state.unreadCount}
345 <ul class="navbar-nav">
346 <li className="nav-item">
349 to={`/u/${localUserView.person.name}`}
350 title={i18n.t("settings")}
353 {localUserView.person.avatar && showAvatars() && (
354 <PictrsImage src={localUserView.person.avatar} icon />
356 {localUserView.person.display_name
357 ? localUserView.person.display_name
358 : localUserView.person.name}
365 <ul class="navbar-nav my-2">
366 <li className="ml-2 nav-item">
368 className="btn btn-success"
370 title={i18n.t("login_sign_up")}
372 {i18n.t("login_sign_up")}
383 expandNavbar(i: Navbar) {
384 i.state.expanded = !i.state.expanded;
388 parseMessage(msg: any) {
389 let op = wsUserOp(msg);
392 if (msg.error == "not_logged_in") {
393 UserService.Instance.logout();
397 } else if (msg.reconnect) {
398 toast(i18n.t("websocket_reconnected"), "success");
399 WebSocketService.Instance.send(
405 } else if (op == UserOperation.GetReplies) {
406 let data = wsJsonToRes<GetRepliesResponse>(msg).data;
407 let unreadReplies = data.replies.filter(r => !r.comment.read);
409 this.state.replies = unreadReplies;
410 this.state.unreadCount = this.calculateUnreadCount();
411 this.setState(this.state);
412 this.sendUnreadCount();
413 } else if (op == UserOperation.GetPersonMentions) {
414 let data = wsJsonToRes<GetPersonMentionsResponse>(msg).data;
415 let unreadMentions = data.mentions.filter(r => !r.comment.read);
417 this.state.mentions = unreadMentions;
418 this.state.unreadCount = this.calculateUnreadCount();
419 this.setState(this.state);
420 this.sendUnreadCount();
421 } else if (op == UserOperation.GetPrivateMessages) {
422 let data = wsJsonToRes<PrivateMessagesResponse>(msg).data;
423 let unreadMessages = data.private_messages.filter(
424 r => !r.private_message.read
427 this.state.messages = unreadMessages;
428 this.state.unreadCount = this.calculateUnreadCount();
429 this.setState(this.state);
430 this.sendUnreadCount();
431 } else if (op == UserOperation.GetSite) {
432 // This is only called on a successful login
433 let data = wsJsonToRes<GetSiteResponse>(msg).data;
434 console.log(data.my_user);
435 UserService.Instance.localUserView = data.my_user;
436 setTheme(UserService.Instance.localUserView.local_user.theme);
437 i18n.changeLanguage(getLanguage());
438 this.state.isLoggedIn = true;
439 this.setState(this.state);
440 } else if (op == UserOperation.CreateComment) {
441 let data = wsJsonToRes<CommentResponse>(msg).data;
443 if (this.state.isLoggedIn) {
445 data.recipient_ids.includes(
446 UserService.Instance.localUserView.local_user.id
449 this.state.replies.push(data.comment_view);
450 this.state.unreadCount++;
451 this.setState(this.state);
452 this.sendUnreadCount();
453 notifyComment(data.comment_view, this.context.router);
456 } else if (op == UserOperation.CreatePrivateMessage) {
457 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
459 if (this.state.isLoggedIn) {
461 data.private_message_view.recipient.id ==
462 UserService.Instance.localUserView.person.id
464 this.state.messages.push(data.private_message_view);
465 this.state.unreadCount++;
466 this.setState(this.state);
467 this.sendUnreadCount();
468 notifyPrivateMessage(data.private_message_view, this.context.router);
475 console.log("Fetching unreads...");
476 let repliesForm: GetReplies = {
484 let personMentionsForm: GetPersonMentions = {
492 let privateMessagesForm: GetPrivateMessages = {
499 if (this.currentLocation !== "/inbox") {
500 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
501 WebSocketService.Instance.send(
502 wsClient.getPersonMentions(personMentionsForm)
504 WebSocketService.Instance.send(
505 wsClient.getPrivateMessages(privateMessagesForm)
510 get currentLocation() {
511 return this.context.router.history.location.pathname;
515 UserService.Instance.unreadCountSub.next(this.state.unreadCount);
518 calculateUnreadCount(): number {
520 this.state.replies.filter(r => !r.comment.read).length +
521 this.state.mentions.filter(r => !r.comment.read).length +
522 this.state.messages.filter(r => !r.private_message.read).length
526 get canAdmin(): boolean {
528 UserService.Instance.localUserView &&
529 this.props.site_res.admins
530 .map(a => a.person.id)
531 .includes(UserService.Instance.localUserView.person.id)
535 get canCreateCommunity(): boolean {
537 this.props.site_res.site_view?.site.community_creation_admin_only;
538 return !adminOnly || this.canAdmin;
541 /// Listens for some websocket errors
543 let msg = i18n.t("websocket_disconnected");
544 WebSocketService.Instance.closeEventListener(() => {
545 toast(msg, "danger");
549 requestNotificationPermission() {
550 if (UserService.Instance.localUserView) {
551 document.addEventListener("DOMContentLoaded", function () {
553 toast(i18n.t("notifications_error"), "danger");
557 if (Notification.permission !== "granted")
558 Notification.requestPermission();