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,
20 PrivateMessageResponse,
21 WebSocketJsonResponse,
22 } from '../interfaces';
25 pictrsAvatarThumbnail,
34 import { i18n, i18nextSetup } from '../i18next';
36 interface NavbarState {
39 replies: Array<Comment>;
40 mentions: Array<Comment>;
41 messages: Array<PrivateMessage>;
45 admins: Array<UserView>;
47 toggleSearch: boolean;
51 export class Navbar extends Component<any, NavbarState> {
52 private wsSub: Subscription;
53 private userSub: Subscription;
54 private unreadCountSub: Subscription;
55 private searchTextField: RefObject<HTMLInputElement>;
56 emptyState: NavbarState = {
71 constructor(props: any, context: any) {
72 super(props, context);
73 this.state = this.emptyState;
75 this.wsSub = WebSocketService.Instance.subject
76 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
78 msg => this.parseMessage(msg),
79 err => console.error(err),
80 () => console.log('complete')
83 WebSocketService.Instance.getSite();
85 this.searchTextField = createRef();
89 // Subscribe to jwt changes
90 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
92 if (res !== undefined) {
93 this.requestNotificationPermission();
95 this.state.isLoggedIn = false;
97 WebSocketService.Instance.getSite();
98 this.setState(this.state);
101 // Subscribe to unread count changes
102 this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(res => {
103 this.setState({ unreadCount: res });
107 handleSearchParam(i: Navbar, event: any) {
108 i.state.searchParam = event.target.value;
113 const searchParam = this.state.searchParam;
114 this.setState({ searchParam: '' });
115 this.setState({ toggleSearch: false });
116 if (searchParam === '') {
117 this.context.router.history.push(`/search/`);
119 this.context.router.history.push(
120 `/search/q/${searchParam}/type/all/sort/topall/page/1`
125 handleSearchSubmit(i: Navbar, event: any) {
126 event.preventDefault();
130 handleSearchBtn(i: Navbar, event: any) {
131 event.preventDefault();
132 i.setState({ toggleSearch: true });
134 i.searchTextField.current.focus();
135 const offsetWidth = i.searchTextField.current.offsetWidth;
136 if (i.state.searchParam && offsetWidth > 100) {
141 handleSearchBlur(i: Navbar, event: any) {
142 if (!(event.relatedTarget && event.relatedTarget.name !== 'search-btn')) {
143 i.state.toggleSearch = false;
149 return this.navbar();
152 componentWillUnmount() {
153 this.wsSub.unsubscribe();
154 this.userSub.unsubscribe();
155 this.unreadCountSub.unsubscribe();
158 // TODO class active corresponding to current page
161 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
162 <div class="container">
163 {!this.state.siteLoading ? (
164 <Link title={this.state.version} class="navbar-brand" to="/">
165 {this.state.siteName}
168 <div class="navbar-item">
169 <svg class="icon icon-spinner spin">
170 <use xlinkHref="#icon-spinner"></use>
174 {this.state.isLoggedIn && (
176 class="ml-auto p-0 navbar-toggler nav-link border-0"
178 title={i18n.t('inbox')}
181 <use xlinkHref="#icon-bell"></use>
183 {this.state.unreadCount > 0 && (
184 <span class="ml-1 badge badge-light">
185 {this.state.unreadCount}
191 class="navbar-toggler border-0"
194 onClick={linkEvent(this, this.expandNavbar)}
195 data-tippy-content={i18n.t('expand_here')}
197 <span class="navbar-toggler-icon"></span>
199 {!this.state.siteLoading && (
202 !this.state.expanded && 'collapse'
205 <ul class="navbar-nav my-2 mr-auto">
206 <li class="nav-item">
210 title={i18n.t('communities')}
212 {i18n.t('communities')}
215 <li class="nav-item">
219 pathname: '/create_post',
220 state: { prevPath: this.currentLocation },
222 title={i18n.t('create_post')}
224 {i18n.t('create_post')}
227 <li class="nav-item">
230 to="/create_community"
231 title={i18n.t('create_community')}
233 {i18n.t('create_community')}
236 <li className="nav-item">
240 title={i18n.t('donate_to_lemmy')}
243 <use xlinkHref="#icon-coffee"></use>
248 {!this.context.router.history.location.pathname.match(
253 onSubmit={linkEvent(this, this.handleSearchSubmit)}
256 class={`form-control mr-0 search-input ${
257 this.state.toggleSearch ? 'show-input' : 'hide-input'
259 onInput={linkEvent(this, this.handleSearchParam)}
260 value={this.state.searchParam}
261 ref={this.searchTextField}
263 placeholder={i18n.t('search')}
264 onBlur={linkEvent(this, this.handleSearchBlur)}
268 onClick={linkEvent(this, this.handleSearchBtn)}
270 style="color: var(--gray)"
273 <use xlinkHref="#icon-search"></use>
278 <ul class="navbar-nav my-2">
280 <li className="nav-item">
284 title={i18n.t('admin_settings')}
287 <use xlinkHref="#icon-settings"></use>
293 {this.state.isLoggedIn ? (
295 <ul class="navbar-nav my-2">
296 <li className="nav-item">
300 title={i18n.t('inbox')}
303 <use xlinkHref="#icon-bell"></use>
305 {this.state.unreadCount > 0 && (
306 <span class="ml-1 badge badge-light">
307 {this.state.unreadCount}
313 <ul class="navbar-nav">
314 <li className="nav-item">
317 to={`/u/${UserService.Instance.user.name}`}
318 title={i18n.t('settings')}
321 {UserService.Instance.user.avatar &&
324 src={pictrsAvatarThumbnail(
325 UserService.Instance.user.avatar
329 class="rounded-circle mr-2"
332 {UserService.Instance.user.name}
339 <ul class="navbar-nav my-2">
340 <li className="nav-item">
342 class="btn btn-success"
344 title={i18n.t('login_sign_up')}
346 {i18n.t('login_sign_up')}
358 expandNavbar(i: Navbar) {
359 i.state.expanded = !i.state.expanded;
363 parseMessage(msg: WebSocketJsonResponse) {
364 let res = wsJsonToRes(msg);
366 if (msg.error == 'not_logged_in') {
367 UserService.Instance.logout();
371 } else if (msg.reconnect) {
373 } else if (res.op == UserOperation.GetReplies) {
374 let data = res.data as GetRepliesResponse;
375 let unreadReplies = data.replies.filter(r => !r.read);
377 this.state.replies = unreadReplies;
378 this.state.unreadCount = this.calculateUnreadCount();
379 this.setState(this.state);
380 this.sendUnreadCount();
381 } else if (res.op == UserOperation.GetUserMentions) {
382 let data = res.data as GetUserMentionsResponse;
383 let unreadMentions = data.mentions.filter(r => !r.read);
385 this.state.mentions = unreadMentions;
386 this.state.unreadCount = this.calculateUnreadCount();
387 this.setState(this.state);
388 this.sendUnreadCount();
389 } else if (res.op == UserOperation.GetPrivateMessages) {
390 let data = res.data as PrivateMessagesResponse;
391 let unreadMessages = data.messages.filter(r => !r.read);
393 this.state.messages = unreadMessages;
394 this.state.unreadCount = this.calculateUnreadCount();
395 this.setState(this.state);
396 this.sendUnreadCount();
397 } else if (res.op == UserOperation.CreateComment) {
398 let data = res.data as CommentResponse;
400 if (this.state.isLoggedIn) {
401 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
402 this.state.replies.push(data.comment);
403 this.state.unreadCount++;
404 this.setState(this.state);
405 this.sendUnreadCount();
406 this.notify(data.comment);
409 } else if (res.op == UserOperation.CreatePrivateMessage) {
410 let data = res.data as PrivateMessageResponse;
412 if (this.state.isLoggedIn) {
413 if (data.message.recipient_id == UserService.Instance.user.id) {
414 this.state.messages.push(data.message);
415 this.state.unreadCount++;
416 this.setState(this.state);
417 this.sendUnreadCount();
418 this.notify(data.message);
421 } else if (res.op == UserOperation.GetSite) {
422 let data = res.data as GetSiteResponse;
424 if (data.site && !this.state.siteName) {
425 this.state.siteName = data.site.name;
426 this.state.version = data.version;
427 this.state.admins = data.admins;
432 UserService.Instance.user = data.my_user;
433 // On the first load, check the unreads
434 if (this.state.isLoggedIn == false) {
435 this.requestNotificationPermission();
437 setTheme(data.my_user.theme, true);
439 this.state.isLoggedIn = true;
444 this.state.siteLoading = false;
445 this.setState(this.state);
450 console.log('Fetching unreads...');
451 let repliesForm: GetRepliesForm = {
452 sort: SortType[SortType.New],
458 let userMentionsForm: GetUserMentionsForm = {
459 sort: SortType[SortType.New],
465 let privateMessagesForm: GetPrivateMessagesForm = {
471 if (this.currentLocation !== '/inbox') {
472 WebSocketService.Instance.getReplies(repliesForm);
473 WebSocketService.Instance.getUserMentions(userMentionsForm);
474 WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
478 get currentLocation() {
479 return this.context.router.history.location.pathname;
483 UserService.Instance.unreadCountSub.next(this.state.unreadCount);
486 calculateUnreadCount(): number {
488 this.state.replies.filter(r => !r.read).length +
489 this.state.mentions.filter(r => !r.read).length +
490 this.state.messages.filter(r => !r.read).length
494 get canAdmin(): boolean {
496 UserService.Instance.user &&
497 this.state.admins.map(a => a.id).includes(UserService.Instance.user.id)
501 requestNotificationPermission() {
502 if (UserService.Instance.user) {
503 document.addEventListener('DOMContentLoaded', function () {
505 toast(i18n.t('notifications_error'), 'danger');
509 if (Notification.permission !== 'granted')
510 Notification.requestPermission();
515 notify(reply: Comment | PrivateMessage) {
516 let creator_name = reply.creator_name;
517 let creator_avatar = reply.creator_avatar
518 ? reply.creator_avatar
519 : `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`;
520 let link = isCommentType(reply)
521 ? `/post/${reply.post_id}/comment/${reply.id}`
523 let htmlBody = md.render(reply.content);
524 let body = reply.content; // Unfortunately the notifications API can't do html
534 if (Notification.permission !== 'granted') Notification.requestPermission();
536 var notification = new Notification(reply.creator_name, {
537 icon: creator_avatar,
541 notification.onclick = () => {
542 event.preventDefault();
543 this.context.router.history.push(link);