1 import { Component, linkEvent, createRef, RefObject } from "inferno";
2 import { NavLink } from "inferno-router";
3 import { Subscription } from "rxjs";
4 import { WebSocketService, UserService } from "../services";
10 GetPersonMentionsResponse,
12 PrivateMessagesResponse,
17 PrivateMessageResponse,
19 } from "lemmy-js-client";
36 import { i18n } from "../i18next";
37 import { PictrsImage } from "./pictrs-image";
38 import { Icon } from "./icon";
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.searchTextField = createRef();
85 console.log(`isLoggedIn = ${this.state.isLoggedIn}`);
87 // On the first load, check the unreads
88 if (this.state.isLoggedIn == false) {
89 // setTheme(data.my_user.theme, true);
90 // i18n.changeLanguage(getLanguage());
91 // i18n.changeLanguage('de');
93 this.requestNotificationPermission();
94 WebSocketService.Instance.send(
102 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
104 if (res !== undefined) {
105 this.requestNotificationPermission();
106 WebSocketService.Instance.send(
107 wsClient.getSite({ auth: authField() })
110 this.setState({ isLoggedIn: false });
114 // Subscribe to unread count changes
115 this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(
117 this.setState({ unreadCount: res });
123 handleSearchParam(i: Navbar, event: any) {
124 i.state.searchParam = event.target.value;
129 const searchParam = this.state.searchParam;
130 this.setState({ searchParam: "" });
131 this.setState({ toggleSearch: false });
132 if (searchParam === "") {
133 this.context.router.history.push(`/search/`);
135 const searchParamEncoded = encodeURIComponent(searchParam);
136 this.context.router.history.push(
137 `/search/q/${searchParamEncoded}/type/All/sort/TopAll/page/1`
142 handleSearchSubmit(i: Navbar, event: any) {
143 event.preventDefault();
147 handleSearchBtn(i: Navbar, event: any) {
148 event.preventDefault();
149 i.setState({ toggleSearch: true });
151 i.searchTextField.current.focus();
152 const offsetWidth = i.searchTextField.current.offsetWidth;
153 if (i.state.searchParam && offsetWidth > 100) {
158 handleSearchBlur(i: Navbar, event: any) {
159 if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
160 i.state.toggleSearch = false;
166 return this.navbar();
169 componentWillUnmount() {
170 this.wsSub.unsubscribe();
171 this.userSub.unsubscribe();
172 this.unreadCountSub.unsubscribe();
177 UserService.Instance.localUserView || this.props.site_res.my_user;
179 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
180 <div class="container">
181 {this.props.site_res.site_view && (
183 title={this.props.site_res.version}
184 className="d-flex align-items-center navbar-brand mr-md-3"
188 {this.props.site_res.site_view.site.icon && showAvatars() && (
190 src={this.props.site_res.site_view.site.icon}
194 {this.props.site_res.site_view.site.name}
197 {this.state.isLoggedIn && (
199 className="ml-auto p-1 navbar-toggler nav-link border-0"
201 title={i18n.t("inbox")}
204 {this.state.unreadCount > 0 && (
206 class="mx-1 badge badge-light"
207 aria-label={`${this.state.unreadCount} ${i18n.t(
211 {this.state.unreadCount}
217 class="navbar-toggler border-0 p-1"
220 onClick={linkEvent(this, this.expandNavbar)}
221 data-tippy-content={i18n.t("expand_here")}
226 className={`${!this.state.expanded && "collapse"} navbar-collapse`}
228 <ul class="navbar-nav my-2 mr-auto">
229 <li class="nav-item">
233 title={i18n.t("communities")}
235 {i18n.t("communities")}
238 <li class="nav-item">
242 pathname: "/create_post",
243 state: { prevPath: this.currentLocation },
245 title={i18n.t("create_post")}
247 {i18n.t("create_post")}
250 <li class="nav-item">
253 to="/create_community"
254 title={i18n.t("create_community")}
256 {i18n.t("create_community")}
259 <li class="nav-item">
262 title={i18n.t("support_lemmy")}
263 href={supportLemmyUrl}
265 <Icon icon="heart" classes="small" />
269 <ul class="navbar-nav my-2">
271 <li className="nav-item">
275 title={i18n.t("admin_settings")}
277 <Icon icon="settings" />
282 {!this.context.router.history.location.pathname.match(
287 onSubmit={linkEvent(this, this.handleSearchSubmit)}
291 class={`form-control mr-0 search-input ${
292 this.state.toggleSearch ? "show-input" : "hide-input"
294 onInput={linkEvent(this, this.handleSearchParam)}
295 value={this.state.searchParam}
296 ref={this.searchTextField}
298 placeholder={i18n.t("search")}
299 onBlur={linkEvent(this, this.handleSearchBlur)}
301 <label class="sr-only" htmlFor="search-input">
306 onClick={linkEvent(this, this.handleSearchBtn)}
307 class="px-1 btn btn-link"
308 style="color: var(--gray)"
309 aria-label={i18n.t("search")}
311 <Icon icon="search" />
315 {this.state.isLoggedIn ? (
317 <ul class="navbar-nav my-2">
318 <li className="nav-item">
322 title={i18n.t("inbox")}
325 {this.state.unreadCount > 0 && (
327 class="ml-1 badge badge-light"
328 aria-label={`${this.state.unreadCount} ${i18n.t(
332 {this.state.unreadCount}
338 <ul class="navbar-nav">
339 <li className="nav-item">
342 to={`/u/${localUserView.person.name}`}
343 title={i18n.t("settings")}
346 {localUserView.person.avatar && showAvatars() && (
347 <PictrsImage src={localUserView.person.avatar} icon />
349 {localUserView.person.display_name
350 ? localUserView.person.display_name
351 : localUserView.person.name}
358 <ul class="navbar-nav my-2">
359 <li className="ml-2 nav-item">
361 className="btn btn-success"
363 title={i18n.t("login_sign_up")}
365 {i18n.t("login_sign_up")}
376 expandNavbar(i: Navbar) {
377 i.state.expanded = !i.state.expanded;
381 parseMessage(msg: any) {
382 let op = wsUserOp(msg);
385 if (msg.error == "not_logged_in") {
386 UserService.Instance.logout();
390 } else if (msg.reconnect) {
391 WebSocketService.Instance.send(
397 } else if (op == UserOperation.GetReplies) {
398 let data = wsJsonToRes<GetRepliesResponse>(msg).data;
399 let unreadReplies = data.replies.filter(r => !r.comment.read);
401 this.state.replies = unreadReplies;
402 this.state.unreadCount = this.calculateUnreadCount();
403 this.setState(this.state);
404 this.sendUnreadCount();
405 } else if (op == UserOperation.GetPersonMentions) {
406 let data = wsJsonToRes<GetPersonMentionsResponse>(msg).data;
407 let unreadMentions = data.mentions.filter(r => !r.comment.read);
409 this.state.mentions = unreadMentions;
410 this.state.unreadCount = this.calculateUnreadCount();
411 this.setState(this.state);
412 this.sendUnreadCount();
413 } else if (op == UserOperation.GetPrivateMessages) {
414 let data = wsJsonToRes<PrivateMessagesResponse>(msg).data;
415 let unreadMessages = data.private_messages.filter(
416 r => !r.private_message.read
419 this.state.messages = unreadMessages;
420 this.state.unreadCount = this.calculateUnreadCount();
421 this.setState(this.state);
422 this.sendUnreadCount();
423 } else if (op == UserOperation.GetSite) {
424 // This is only called on a successful login
425 let data = wsJsonToRes<GetSiteResponse>(msg).data;
426 console.log(data.my_user);
427 UserService.Instance.localUserView = data.my_user;
428 setTheme(UserService.Instance.localUserView.local_user.theme);
429 i18n.changeLanguage(getLanguage());
430 this.state.isLoggedIn = true;
431 this.setState(this.state);
432 } else if (op == UserOperation.CreateComment) {
433 let data = wsJsonToRes<CommentResponse>(msg).data;
435 if (this.state.isLoggedIn) {
437 data.recipient_ids.includes(
438 UserService.Instance.localUserView.local_user.id
441 this.state.replies.push(data.comment_view);
442 this.state.unreadCount++;
443 this.setState(this.state);
444 this.sendUnreadCount();
445 notifyComment(data.comment_view, this.context.router);
448 } else if (op == UserOperation.CreatePrivateMessage) {
449 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
451 if (this.state.isLoggedIn) {
453 data.private_message_view.recipient.id ==
454 UserService.Instance.localUserView.person.id
456 this.state.messages.push(data.private_message_view);
457 this.state.unreadCount++;
458 this.setState(this.state);
459 this.sendUnreadCount();
460 notifyPrivateMessage(data.private_message_view, this.context.router);
467 console.log("Fetching unreads...");
468 let repliesForm: GetReplies = {
476 let personMentionsForm: GetPersonMentions = {
484 let privateMessagesForm: GetPrivateMessages = {
491 if (this.currentLocation !== "/inbox") {
492 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
493 WebSocketService.Instance.send(
494 wsClient.getPersonMentions(personMentionsForm)
496 WebSocketService.Instance.send(
497 wsClient.getPrivateMessages(privateMessagesForm)
502 get currentLocation() {
503 return this.context.router.history.location.pathname;
507 UserService.Instance.unreadCountSub.next(this.state.unreadCount);
510 calculateUnreadCount(): number {
512 this.state.replies.filter(r => !r.comment.read).length +
513 this.state.mentions.filter(r => !r.comment.read).length +
514 this.state.messages.filter(r => !r.private_message.read).length
518 get canAdmin(): boolean {
520 UserService.Instance.localUserView &&
521 this.props.site_res.admins
522 .map(a => a.person.id)
523 .includes(UserService.Instance.localUserView.person.id)
527 requestNotificationPermission() {
528 if (UserService.Instance.localUserView) {
529 document.addEventListener("DOMContentLoaded", function () {
531 toast(i18n.t("notifications_error"), "danger");
535 if (Notification.permission !== "granted")
536 Notification.requestPermission();