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 '../interfaces';
24 pictrsAvatarThumbnail,
33 import { i18n } from '../i18next';
35 interface NavbarState {
38 replies: Array<Comment>;
39 mentions: Array<Comment>;
40 messages: Array<PrivateMessage>;
43 toggleSearch: boolean;
45 siteRes: GetSiteResponse;
46 onSiteBanner?(url: string): any;
49 export class Navbar extends Component<any, NavbarState> {
50 private wsSub: Subscription;
51 private userSub: Subscription;
52 private unreadCountSub: Subscription;
53 private searchTextField: RefObject<HTMLInputElement>;
54 emptyState: NavbarState = {
68 number_of_users: null,
69 number_of_posts: null,
70 number_of_comments: null,
71 number_of_communities: null,
72 enable_downvotes: null,
73 open_registration: null,
77 creator_preferred_username: null,
84 federated_instances: null,
91 constructor(props: any, context: any) {
92 super(props, context);
93 this.state = this.emptyState;
95 this.wsSub = WebSocketService.Instance.subject
96 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
98 msg => this.parseMessage(msg),
99 err => console.error(err),
100 () => console.log('complete')
103 WebSocketService.Instance.getSite();
105 this.searchTextField = createRef();
108 componentDidMount() {
109 // Subscribe to jwt changes
110 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
112 if (res !== undefined) {
113 this.requestNotificationPermission();
115 this.state.isLoggedIn = false;
117 WebSocketService.Instance.getSite();
118 this.setState(this.state);
121 // Subscribe to unread count changes
122 this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(res => {
123 this.setState({ unreadCount: res });
127 handleSearchParam(i: Navbar, event: any) {
128 i.state.searchParam = event.target.value;
133 const searchParam = this.state.searchParam;
134 this.setState({ searchParam: '' });
135 this.setState({ toggleSearch: false });
136 if (searchParam === '') {
137 this.context.router.history.push(`/search/`);
139 this.context.router.history.push(
140 `/search/q/${searchParam}/type/all/sort/topall/page/1`
145 handleSearchSubmit(i: Navbar, event: any) {
146 event.preventDefault();
150 handleSearchBtn(i: Navbar, event: any) {
151 event.preventDefault();
152 i.setState({ toggleSearch: true });
154 i.searchTextField.current.focus();
155 const offsetWidth = i.searchTextField.current.offsetWidth;
156 if (i.state.searchParam && offsetWidth > 100) {
161 handleSearchBlur(i: Navbar, event: any) {
162 if (!(event.relatedTarget && event.relatedTarget.name !== 'search-btn')) {
163 i.state.toggleSearch = false;
169 return this.navbar();
172 componentWillUnmount() {
173 this.wsSub.unsubscribe();
174 this.userSub.unsubscribe();
175 this.unreadCountSub.unsubscribe();
178 // TODO class active corresponding to current page
180 let user = UserService.Instance.user;
182 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
183 <div class="container">
184 {!this.state.siteLoading ? (
186 title={this.state.siteRes.version}
187 class="d-flex align-items-center navbar-brand mr-md-3"
190 {this.state.siteRes.site.icon && showAvatars() && (
192 src={pictrsAvatarThumbnail(this.state.siteRes.site.icon)}
195 class="rounded-circle mr-2"
198 {this.state.siteRes.site.name}
201 <div class="navbar-item">
202 <svg class="icon icon-spinner spin">
203 <use xlinkHref="#icon-spinner"></use>
207 {this.state.isLoggedIn && (
209 class="ml-auto p-0 navbar-toggler nav-link border-0"
211 title={i18n.t('inbox')}
214 <use xlinkHref="#icon-bell"></use>
216 {this.state.unreadCount > 0 && (
217 <span class="mx-1 badge badge-light">
218 {this.state.unreadCount}
224 class="navbar-toggler border-0 p-1"
227 onClick={linkEvent(this, this.expandNavbar)}
228 data-tippy-content={i18n.t('expand_here')}
230 <span class="navbar-toggler-icon"></span>
232 {!this.state.siteLoading && (
235 !this.state.expanded && 'collapse'
238 <ul class="navbar-nav my-2 mr-auto">
239 <li class="nav-item">
243 title={i18n.t('communities')}
245 {i18n.t('communities')}
248 <li class="nav-item">
252 pathname: '/create_post',
253 state: { prevPath: this.currentLocation },
255 title={i18n.t('create_post')}
257 {i18n.t('create_post')}
260 <li class="nav-item">
263 to="/create_community"
264 title={i18n.t('create_community')}
266 {i18n.t('create_community')}
269 <li className="nav-item">
273 title={i18n.t('donate_to_lemmy')}
276 <use xlinkHref="#icon-coffee"></use>
281 <ul class="navbar-nav my-2">
283 <li className="nav-item">
287 title={i18n.t('admin_settings')}
290 <use xlinkHref="#icon-settings"></use>
296 {!this.context.router.history.location.pathname.match(
301 onSubmit={linkEvent(this, this.handleSearchSubmit)}
304 class={`form-control mr-0 search-input ${
305 this.state.toggleSearch ? 'show-input' : 'hide-input'
307 onInput={linkEvent(this, this.handleSearchParam)}
308 value={this.state.searchParam}
309 ref={this.searchTextField}
311 placeholder={i18n.t('search')}
312 onBlur={linkEvent(this, this.handleSearchBlur)}
316 onClick={linkEvent(this, this.handleSearchBtn)}
317 class="px-1 btn btn-link"
318 style="color: var(--gray)"
321 <use xlinkHref="#icon-search"></use>
326 {this.state.isLoggedIn ? (
328 <ul class="navbar-nav my-2">
329 <li className="nav-item">
333 title={i18n.t('inbox')}
336 <use xlinkHref="#icon-bell"></use>
338 {this.state.unreadCount > 0 && (
339 <span class="ml-1 badge badge-light">
340 {this.state.unreadCount}
346 <ul class="navbar-nav">
347 <li className="nav-item">
350 to={`/u/${user.name}`}
351 title={i18n.t('settings')}
354 {user.avatar && showAvatars() && (
356 src={pictrsAvatarThumbnail(user.avatar)}
359 class="rounded-circle mr-2"
362 {user.preferred_username
363 ? user.preferred_username
371 <ul class="navbar-nav my-2">
372 <li className="ml-2 nav-item">
374 class="btn btn-success"
376 title={i18n.t('login_sign_up')}
378 {i18n.t('login_sign_up')}
390 expandNavbar(i: Navbar) {
391 i.state.expanded = !i.state.expanded;
395 parseMessage(msg: WebSocketJsonResponse) {
396 let res = wsJsonToRes(msg);
398 if (msg.error == 'not_logged_in') {
399 UserService.Instance.logout();
403 } else if (msg.reconnect) {
405 } else if (res.op == UserOperation.GetReplies) {
406 let data = res.data as GetRepliesResponse;
407 let unreadReplies = data.replies.filter(r => !r.read);
409 this.state.replies = unreadReplies;
410 this.state.unreadCount = this.calculateUnreadCount();
411 this.setState(this.state);
412 this.sendUnreadCount();
413 } else if (res.op == UserOperation.GetUserMentions) {
414 let data = res.data as GetUserMentionsResponse;
415 let unreadMentions = data.mentions.filter(r => !r.read);
417 this.state.mentions = unreadMentions;
418 this.state.unreadCount = this.calculateUnreadCount();
419 this.setState(this.state);
420 this.sendUnreadCount();
421 } else if (res.op == UserOperation.GetPrivateMessages) {
422 let data = res.data as PrivateMessagesResponse;
423 let unreadMessages = data.messages.filter(r => !r.read);
425 this.state.messages = unreadMessages;
426 this.state.unreadCount = this.calculateUnreadCount();
427 this.setState(this.state);
428 this.sendUnreadCount();
429 } else if (res.op == UserOperation.CreateComment) {
430 let data = res.data as CommentResponse;
432 if (this.state.isLoggedIn) {
433 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
434 this.state.replies.push(data.comment);
435 this.state.unreadCount++;
436 this.setState(this.state);
437 this.sendUnreadCount();
438 notifyComment(data.comment, this.context.router);
441 } else if (res.op == UserOperation.CreatePrivateMessage) {
442 let data = res.data as PrivateMessageResponse;
444 if (this.state.isLoggedIn) {
445 if (data.message.recipient_id == UserService.Instance.user.id) {
446 this.state.messages.push(data.message);
447 this.state.unreadCount++;
448 this.setState(this.state);
449 this.sendUnreadCount();
450 notifyPrivateMessage(data.message, this.context.router);
453 } else if (res.op == UserOperation.GetSite) {
454 let data = res.data as GetSiteResponse;
456 this.state.siteRes = data;
460 UserService.Instance.user = data.my_user;
461 WebSocketService.Instance.userJoin();
462 // On the first load, check the unreads
463 if (this.state.isLoggedIn == false) {
464 this.requestNotificationPermission();
466 setTheme(data.my_user.theme, true);
467 i18n.changeLanguage(getLanguage());
469 this.state.isLoggedIn = true;
472 this.state.siteLoading = false;
473 this.setState(this.state);
478 console.log('Fetching unreads...');
479 let repliesForm: GetRepliesForm = {
480 sort: SortType[SortType.New],
486 let userMentionsForm: GetUserMentionsForm = {
487 sort: SortType[SortType.New],
493 let privateMessagesForm: GetPrivateMessagesForm = {
499 if (this.currentLocation !== '/inbox') {
500 WebSocketService.Instance.getReplies(repliesForm);
501 WebSocketService.Instance.getUserMentions(userMentionsForm);
502 WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
506 get currentLocation() {
507 return this.context.router.history.location.pathname;
511 UserService.Instance.unreadCountSub.next(this.state.unreadCount);
514 calculateUnreadCount(): number {
516 this.state.replies.filter(r => !r.read).length +
517 this.state.mentions.filter(r => !r.read).length +
518 this.state.messages.filter(r => !r.read).length
522 get canAdmin(): boolean {
524 UserService.Instance.user &&
525 this.state.siteRes.admins
527 .includes(UserService.Instance.user.id)
531 requestNotificationPermission() {
532 if (UserService.Instance.user) {
533 document.addEventListener('DOMContentLoaded', function () {
535 toast(i18n.t('notifications_error'), 'danger');
539 if (Notification.permission !== 'granted')
540 Notification.requestPermission();