1 import { Component, linkEvent, createRef, RefObject } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { Subscription } from 'rxjs';
4 import { retryWhen, delay, take } from 'rxjs/operators';
5 import { WebSocketService, UserService } from '../services';
11 GetUserMentionsResponse,
12 GetPrivateMessagesForm,
13 PrivateMessagesResponse,
19 PrivateMessageResponse,
20 WebSocketJsonResponse,
21 } from 'lemmy-js-client';
24 pictrsAvatarThumbnail,
34 import { i18n } from '../i18next';
36 interface NavbarState {
41 messages: PrivateMessage[];
44 toggleSearch: boolean;
46 siteRes: GetSiteResponse;
47 onSiteBanner?(url: string): any;
50 export class Navbar extends Component<any, NavbarState> {
51 private wsSub: Subscription;
52 private userSub: Subscription;
53 private unreadCountSub: Subscription;
54 private searchTextField: RefObject<HTMLInputElement>;
55 emptyState: NavbarState = {
69 number_of_users: null,
70 number_of_posts: null,
71 number_of_comments: null,
72 number_of_communities: null,
73 enable_downvotes: null,
74 open_registration: null,
78 creator_preferred_username: null,
85 federated_instances: null,
92 constructor(props: any, context: any) {
93 super(props, context);
94 this.state = this.emptyState;
97 this.wsSub = WebSocketService.Instance.subject
98 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
100 msg => this.parseMessage(msg),
101 err => console.error(err),
102 () => console.log('complete')
105 WebSocketService.Instance.getSite();
107 this.searchTextField = createRef();
111 componentDidMount() {
113 // Subscribe to jwt changes
114 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
116 if (res !== undefined) {
117 this.requestNotificationPermission();
119 this.state.isLoggedIn = false;
121 WebSocketService.Instance.getSite();
122 this.setState(this.state);
125 // Subscribe to unread count changes
126 this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(
128 this.setState({ unreadCount: res });
134 handleSearchParam(i: Navbar, event: any) {
135 i.state.searchParam = event.target.value;
140 /* const searchParam = this.state.searchParam; */
141 /* this.setState({ searchParam: '' }); */
142 /* this.setState({ toggleSearch: false }); */
143 /* if (searchParam === '') { */
144 /* this.context.router.history.push(`/search/`); */
146 /* this.context.router.history.push( */
147 /* `/search/q/${searchParam}/type/All/sort/TopAll/page/1` */
152 handleSearchSubmit(i: Navbar, event: any) {
153 event.preventDefault();
157 handleSearchBtn(i: Navbar, event: any) {
158 event.preventDefault();
159 i.setState({ toggleSearch: true });
161 i.searchTextField.current.focus();
162 const offsetWidth = i.searchTextField.current.offsetWidth;
163 if (i.state.searchParam && offsetWidth > 100) {
168 handleSearchBlur(i: Navbar, event: any) {
169 if (!(event.relatedTarget && event.relatedTarget.name !== 'search-btn')) {
170 i.state.toggleSearch = false;
176 return this.navbar();
179 componentWillUnmount() {
180 this.wsSub.unsubscribe();
181 this.userSub.unsubscribe();
182 this.unreadCountSub.unsubscribe();
185 // TODO class active corresponding to current page
187 let user = UserService.Instance.user;
188 let expandedClass = `${!this.state.expanded && 'collapse'} navbar-collapse`;
191 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
192 <div class="container">
193 {!this.state.siteLoading ? (
195 title={this.state.siteRes.version}
196 class="d-flex align-items-center navbar-brand mr-md-3"
199 {this.state.siteRes.site.icon && showAvatars() && (
201 src={pictrsAvatarThumbnail(this.state.siteRes.site.icon)}
204 class="rounded-circle mr-2"
207 {this.state.siteRes.site.name}
210 <div class="navbar-item">
211 <svg class="icon icon-spinner spin">
212 <use xlinkHref="#icon-spinner"></use>
216 {this.state.isLoggedIn && (
218 class="ml-auto p-0 navbar-toggler nav-link border-0"
220 title={i18n.t('inbox')}
223 <use xlinkHref="#icon-bell"></use>
225 {this.state.unreadCount > 0 && (
226 <span class="mx-1 badge badge-light">
227 {this.state.unreadCount}
233 class="navbar-toggler border-0 p-1"
236 onClick={linkEvent(this, this.expandNavbar)}
237 data-tippy-content={i18n.t('expand_here')}
239 <span class="navbar-toggler-icon"></span>
241 {/* TODO this isn't working
242 className={`${!this.state.expanded && 'collapse'
245 {!this.state.siteLoading && (
246 <div class="navbar-collapse">
247 <ul class="navbar-nav my-2 mr-auto">
248 <li class="nav-item">
252 title={i18n.t('communities')}
254 {i18n.t('communities')}
257 <li class="nav-item">
261 pathname: '/create_post',
262 state: { prevPath: this.currentLocation },
264 title={i18n.t('create_post')}
266 {i18n.t('create_post')}
269 <li class="nav-item">
272 to="/create_community"
273 title={i18n.t('create_community')}
275 {i18n.t('create_community')}
278 <li className="nav-item">
282 title={i18n.t('donate_to_lemmy')}
285 <use xlinkHref="#icon-coffee"></use>
290 <ul class="navbar-nav my-2">
292 <li className="nav-item">
296 title={i18n.t('admin_settings')}
299 <use xlinkHref="#icon-settings"></use>
305 {!this.context.router.history.location.pathname.match(
310 onSubmit={linkEvent(this, this.handleSearchSubmit)}
312 {/* TODO No idea why, but this class here fails
313 class={`form-control mr-0 search-input ${
314 this.state.toggleSearch ? 'show-input' : 'hide-input'
319 onInput={linkEvent(this, this.handleSearchParam)}
320 value={this.state.searchParam}
322 placeholder={i18n.t('search')}
323 onBlur={linkEvent(this, this.handleSearchBlur)}
327 onClick={linkEvent(this, this.handleSearchBtn)}
328 class="px-1 btn btn-link"
329 style="color: var(--gray)"
332 <use xlinkHref="#icon-search"></use>
337 {this.state.isLoggedIn ? (
339 <ul class="navbar-nav my-2">
340 <li className="nav-item">
344 title={i18n.t('inbox')}
347 <use xlinkHref="#icon-bell"></use>
349 {this.state.unreadCount > 0 && (
350 <span class="ml-1 badge badge-light">
351 {this.state.unreadCount}
357 <ul class="navbar-nav">
358 <li className="nav-item">
361 to={`/u/${user.name}`}
362 title={i18n.t('settings')}
365 {user.avatar && showAvatars() && (
367 src={pictrsAvatarThumbnail(user.avatar)}
370 class="rounded-circle mr-2"
373 {user.preferred_username
374 ? user.preferred_username
382 <ul class="navbar-nav my-2">
383 <li className="ml-2 nav-item">
385 class="btn btn-success"
387 title={i18n.t('login_sign_up')}
389 {i18n.t('login_sign_up')}
401 expandNavbar(i: Navbar) {
402 i.state.expanded = !i.state.expanded;
406 parseMessage(msg: WebSocketJsonResponse) {
407 let res = wsJsonToRes(msg);
410 if (msg.error == 'not_logged_in') {
411 UserService.Instance.logout();
415 } else if (msg.reconnect) {
417 } else if (res.op == UserOperation.GetReplies) {
418 let data = res.data as GetRepliesResponse;
419 let unreadReplies = data.replies.filter(r => !r.read);
421 this.state.replies = unreadReplies;
422 this.state.unreadCount = this.calculateUnreadCount();
423 this.setState(this.state);
424 this.sendUnreadCount();
425 } else if (res.op == UserOperation.GetUserMentions) {
426 let data = res.data as GetUserMentionsResponse;
427 let unreadMentions = data.mentions.filter(r => !r.read);
429 this.state.mentions = unreadMentions;
430 this.state.unreadCount = this.calculateUnreadCount();
431 this.setState(this.state);
432 this.sendUnreadCount();
433 } else if (res.op == UserOperation.GetPrivateMessages) {
434 let data = res.data as PrivateMessagesResponse;
435 let unreadMessages = data.messages.filter(r => !r.read);
437 this.state.messages = unreadMessages;
438 this.state.unreadCount = this.calculateUnreadCount();
439 this.setState(this.state);
440 this.sendUnreadCount();
441 } else if (res.op == UserOperation.CreateComment) {
442 let data = res.data as CommentResponse;
444 if (this.state.isLoggedIn) {
445 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
446 this.state.replies.push(data.comment);
447 this.state.unreadCount++;
448 this.setState(this.state);
449 this.sendUnreadCount();
450 notifyComment(data.comment, this.context.router);
453 } else if (res.op == UserOperation.CreatePrivateMessage) {
454 let data = res.data as PrivateMessageResponse;
456 if (this.state.isLoggedIn) {
457 if (data.message.recipient_id == UserService.Instance.user.id) {
458 this.state.messages.push(data.message);
459 this.state.unreadCount++;
460 this.setState(this.state);
461 this.sendUnreadCount();
462 notifyPrivateMessage(data.message, this.context.router);
465 } else if (res.op == UserOperation.GetSite) {
466 let data = res.data as GetSiteResponse;
468 this.state.siteRes = data;
472 UserService.Instance.user = data.my_user;
473 WebSocketService.Instance.userJoin();
474 // On the first load, check the unreads
475 if (this.state.isLoggedIn == false) {
476 this.requestNotificationPermission();
478 setTheme(data.my_user.theme, true);
479 i18n.changeLanguage(getLanguage());
481 this.state.isLoggedIn = true;
485 this.state.siteLoading = false;
486 this.setState(this.state);
490 console.log('Fetching unreads...');
491 let repliesForm: GetRepliesForm = {
498 let userMentionsForm: GetUserMentionsForm = {
505 let privateMessagesForm: GetPrivateMessagesForm = {
511 if (this.currentLocation !== '/inbox') {
512 WebSocketService.Instance.getReplies(repliesForm);
513 WebSocketService.Instance.getUserMentions(userMentionsForm);
514 WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
518 get currentLocation() {
519 return this.context.router.history.location.pathname;
523 UserService.Instance.unreadCountSub.next(this.state.unreadCount);
526 calculateUnreadCount(): number {
528 this.state.replies.filter(r => !r.read).length +
529 this.state.mentions.filter(r => !r.read).length +
530 this.state.messages.filter(r => !r.read).length
534 get canAdmin(): boolean {
536 UserService.Instance.user &&
537 this.state.siteRes.admins
539 .includes(UserService.Instance.user.id)
543 requestNotificationPermission() {
544 if (UserService.Instance.user) {
545 document.addEventListener('DOMContentLoaded', function () {
547 toast(i18n.t('notifications_error'), 'danger');
551 if (Notification.permission !== 'granted')
552 Notification.requestPermission();