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,
34 import { i18n } from '../i18next';
36 interface NavbarState {
39 replies: Array<Comment>;
40 mentions: Array<Comment>;
41 messages: Array<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;
96 this.wsSub = WebSocketService.Instance.subject
97 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
99 msg => this.parseMessage(msg),
100 err => console.error(err),
101 () => console.log('complete')
104 WebSocketService.Instance.getSite();
106 this.searchTextField = createRef();
109 componentDidMount() {
110 // Subscribe to jwt changes
111 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
113 if (res !== undefined) {
114 this.requestNotificationPermission();
116 this.state.isLoggedIn = false;
118 WebSocketService.Instance.getSite();
119 this.setState(this.state);
122 // Subscribe to unread count changes
123 this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(res => {
124 this.setState({ unreadCount: res });
128 handleSearchParam(i: Navbar, event: any) {
129 i.state.searchParam = event.target.value;
134 const searchParam = this.state.searchParam;
135 this.setState({ searchParam: '' });
136 this.setState({ toggleSearch: false });
137 if (searchParam === '') {
138 this.context.router.history.push(`/search/`);
140 this.context.router.history.push(
141 `/search/q/${searchParam}/type/all/sort/topall/page/1`
146 handleSearchSubmit(i: Navbar, event: any) {
147 event.preventDefault();
151 handleSearchBtn(i: Navbar, event: any) {
152 event.preventDefault();
153 i.setState({ toggleSearch: true });
155 i.searchTextField.current.focus();
156 const offsetWidth = i.searchTextField.current.offsetWidth;
157 if (i.state.searchParam && offsetWidth > 100) {
162 handleSearchBlur(i: Navbar, event: any) {
163 if (!(event.relatedTarget && event.relatedTarget.name !== 'search-btn')) {
164 i.state.toggleSearch = false;
170 return this.navbar();
173 componentWillUnmount() {
174 this.wsSub.unsubscribe();
175 this.userSub.unsubscribe();
176 this.unreadCountSub.unsubscribe();
179 // TODO class active corresponding to current page
181 let user = UserService.Instance.user;
183 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
184 <div class="container">
185 {!this.state.siteLoading ? (
187 title={this.state.siteRes.version}
188 class="d-flex align-items-center navbar-brand mr-md-3"
191 {this.state.siteRes.site.icon && showAvatars() && (
193 src={pictrsAvatarThumbnail(this.state.siteRes.site.icon)}
196 class="rounded-circle mr-2"
199 {this.state.siteRes.site.name}
202 <div class="navbar-item">
203 <svg class="icon icon-spinner spin">
204 <use xlinkHref="#icon-spinner"></use>
208 {this.state.isLoggedIn && (
210 class="ml-auto p-0 navbar-toggler nav-link border-0"
212 title={i18n.t('inbox')}
215 <use xlinkHref="#icon-bell"></use>
217 {this.state.unreadCount > 0 && (
218 <span class="mx-1 badge badge-light">
219 {this.state.unreadCount}
225 class="navbar-toggler border-0 p-1"
228 onClick={linkEvent(this, this.expandNavbar)}
229 data-tippy-content={i18n.t('expand_here')}
231 <span class="navbar-toggler-icon"></span>
233 {!this.state.siteLoading && (
236 !this.state.expanded && 'collapse'
239 <ul class="navbar-nav my-2 mr-auto">
240 <li class="nav-item">
244 title={i18n.t('communities')}
246 {i18n.t('communities')}
249 <li class="nav-item">
253 pathname: '/create_post',
254 state: { prevPath: this.currentLocation },
256 title={i18n.t('create_post')}
258 {i18n.t('create_post')}
261 <li class="nav-item">
264 to="/create_community"
265 title={i18n.t('create_community')}
267 {i18n.t('create_community')}
270 <li className="nav-item">
274 title={i18n.t('donate_to_lemmy')}
277 <use xlinkHref="#icon-coffee"></use>
282 <ul class="navbar-nav my-2">
284 <li className="nav-item">
288 title={i18n.t('admin_settings')}
291 <use xlinkHref="#icon-settings"></use>
297 {!this.context.router.history.location.pathname.match(
302 onSubmit={linkEvent(this, this.handleSearchSubmit)}
305 class={`form-control mr-0 search-input ${
306 this.state.toggleSearch ? 'show-input' : 'hide-input'
308 onInput={linkEvent(this, this.handleSearchParam)}
309 value={this.state.searchParam}
310 ref={this.searchTextField}
312 placeholder={i18n.t('search')}
313 onBlur={linkEvent(this, this.handleSearchBlur)}
317 onClick={linkEvent(this, this.handleSearchBtn)}
318 class="px-1 btn btn-link"
319 style="color: var(--gray)"
322 <use xlinkHref="#icon-search"></use>
327 {this.state.isLoggedIn ? (
329 <ul class="navbar-nav my-2">
330 <li className="nav-item">
334 title={i18n.t('inbox')}
337 <use xlinkHref="#icon-bell"></use>
339 {this.state.unreadCount > 0 && (
340 <span class="ml-1 badge badge-light">
341 {this.state.unreadCount}
347 <ul class="navbar-nav">
348 <li className="nav-item">
351 to={`/u/${user.name}`}
352 title={i18n.t('settings')}
355 {user.avatar && showAvatars() && (
357 src={pictrsAvatarThumbnail(user.avatar)}
360 class="rounded-circle mr-2"
363 {user.preferred_username
364 ? user.preferred_username
372 <ul class="navbar-nav my-2">
373 <li className="ml-2 nav-item">
375 class="btn btn-success"
377 title={i18n.t('login_sign_up')}
379 {i18n.t('login_sign_up')}
391 expandNavbar(i: Navbar) {
392 i.state.expanded = !i.state.expanded;
396 parseMessage(msg: WebSocketJsonResponse) {
397 let res = wsJsonToRes(msg);
399 if (msg.error == 'not_logged_in') {
400 UserService.Instance.logout();
404 } else if (msg.reconnect) {
406 } else if (res.op == UserOperation.GetReplies) {
407 let data = res.data as GetRepliesResponse;
408 let unreadReplies = data.replies.filter(r => !r.read);
410 this.state.replies = unreadReplies;
411 this.state.unreadCount = this.calculateUnreadCount();
412 this.setState(this.state);
413 this.sendUnreadCount();
414 } else if (res.op == UserOperation.GetUserMentions) {
415 let data = res.data as GetUserMentionsResponse;
416 let unreadMentions = data.mentions.filter(r => !r.read);
418 this.state.mentions = unreadMentions;
419 this.state.unreadCount = this.calculateUnreadCount();
420 this.setState(this.state);
421 this.sendUnreadCount();
422 } else if (res.op == UserOperation.GetPrivateMessages) {
423 let data = res.data as PrivateMessagesResponse;
424 let unreadMessages = data.messages.filter(r => !r.read);
426 this.state.messages = unreadMessages;
427 this.state.unreadCount = this.calculateUnreadCount();
428 this.setState(this.state);
429 this.sendUnreadCount();
430 } else if (res.op == UserOperation.CreateComment) {
431 let data = res.data as CommentResponse;
433 if (this.state.isLoggedIn) {
434 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
435 this.state.replies.push(data.comment);
436 this.state.unreadCount++;
437 this.setState(this.state);
438 this.sendUnreadCount();
439 this.notify(data.comment);
442 } else if (res.op == UserOperation.CreatePrivateMessage) {
443 let data = res.data as PrivateMessageResponse;
445 if (this.state.isLoggedIn) {
446 if (data.message.recipient_id == UserService.Instance.user.id) {
447 this.state.messages.push(data.message);
448 this.state.unreadCount++;
449 this.setState(this.state);
450 this.sendUnreadCount();
451 this.notify(data.message);
454 } else if (res.op == UserOperation.GetSite) {
455 let data = res.data as GetSiteResponse;
457 this.state.siteRes = data;
461 UserService.Instance.user = data.my_user;
462 WebSocketService.Instance.userJoin();
463 // On the first load, check the unreads
464 if (this.state.isLoggedIn == false) {
465 this.requestNotificationPermission();
467 setTheme(data.my_user.theme, true);
468 i18n.changeLanguage(getLanguage());
470 this.state.isLoggedIn = true;
473 this.state.siteLoading = false;
474 this.setState(this.state);
479 console.log('Fetching unreads...');
480 let repliesForm: GetRepliesForm = {
481 sort: SortType[SortType.New],
487 let userMentionsForm: GetUserMentionsForm = {
488 sort: SortType[SortType.New],
494 let privateMessagesForm: GetPrivateMessagesForm = {
500 if (this.currentLocation !== '/inbox') {
501 WebSocketService.Instance.getReplies(repliesForm);
502 WebSocketService.Instance.getUserMentions(userMentionsForm);
503 WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
507 get currentLocation() {
508 return this.context.router.history.location.pathname;
512 UserService.Instance.unreadCountSub.next(this.state.unreadCount);
515 calculateUnreadCount(): number {
517 this.state.replies.filter(r => !r.read).length +
518 this.state.mentions.filter(r => !r.read).length +
519 this.state.messages.filter(r => !r.read).length
523 get canAdmin(): boolean {
525 UserService.Instance.user &&
526 this.state.siteRes.admins
528 .includes(UserService.Instance.user.id)
532 requestNotificationPermission() {
533 if (UserService.Instance.user) {
534 document.addEventListener('DOMContentLoaded', function () {
536 toast(i18n.t('notifications_error'), 'danger');
540 if (Notification.permission !== 'granted')
541 Notification.requestPermission();
546 notify(reply: Comment | PrivateMessage) {
547 let creator_name = reply.creator_name;
548 let creator_avatar = reply.creator_avatar
549 ? reply.creator_avatar
550 : `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`;
551 let link = isCommentType(reply)
552 ? `/post/${reply.post_id}/comment/${reply.id}`
554 let htmlBody = md.render(reply.content);
555 let body = reply.content; // Unfortunately the notifications API can't do html
565 if (Notification.permission !== 'granted') Notification.requestPermission();
567 var notification = new Notification(reply.creator_name, {
568 icon: creator_avatar,
572 notification.onclick = () => {
573 event.preventDefault();
574 this.context.router.history.push(link);