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,
33 import { version } from '../version';
34 import { i18n } from '../i18next';
36 interface NavbarState {
39 replies: Array<Comment>;
40 mentions: Array<Comment>;
41 messages: Array<PrivateMessage>;
44 admins: Array<UserView>;
46 toggleSearch: boolean;
49 export class Navbar extends Component<any, NavbarState> {
50 private wsSub: Subscription;
51 private userSub: Subscription;
52 private searchTextField: RefObject<HTMLInputElement>;
53 emptyState: NavbarState = {
54 isLoggedIn: UserService.Instance.user !== undefined,
66 constructor(props: any, context: any) {
67 super(props, context);
68 this.state = this.emptyState;
70 // Subscribe to user changes
71 this.userSub = UserService.Instance.sub.subscribe(user => {
72 this.state.isLoggedIn = user.user !== undefined;
73 if (this.state.isLoggedIn) {
74 this.state.unreadCount = user.user.unreadCount;
75 this.requestNotificationPermission();
77 this.setState(this.state);
80 this.wsSub = WebSocketService.Instance.subject
81 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
83 msg => this.parseMessage(msg),
84 err => console.error(err),
85 () => console.log('complete')
88 if (this.state.isLoggedIn) {
89 this.requestNotificationPermission();
90 // TODO couldn't get re-logging in to re-fetch unreads
94 WebSocketService.Instance.getSite();
96 this.searchTextField = createRef();
99 handleSearchParam(i: Navbar, event: any) {
100 i.state.searchParam = event.target.value;
105 const searchParam = this.state.searchParam;
106 this.setState({ searchParam: '' });
107 this.setState({ toggleSearch: false });
108 if (searchParam === '') {
109 this.context.router.history.push(`/search/`);
111 this.context.router.history.push(
112 `/search/q/${searchParam}/type/all/sort/topall/page/1`
117 handleSearchSubmit(i: Navbar, event: any) {
118 event.preventDefault();
122 handleSearchBtn(i: Navbar, event: any) {
123 event.preventDefault();
124 i.setState({ toggleSearch: true });
126 i.searchTextField.current.focus();
127 const offsetWidth = i.searchTextField.current.offsetWidth;
128 if (i.state.searchParam && offsetWidth > 100) {
133 handleSearchBlur(i: Navbar, event: any) {
134 if (!(event.relatedTarget && event.relatedTarget.name !== 'search-btn')) {
135 i.state.toggleSearch = false;
141 return this.navbar();
144 componentWillUnmount() {
145 this.wsSub.unsubscribe();
146 this.userSub.unsubscribe();
149 // TODO class active corresponding to current page
152 <nav class="container-fluid navbar navbar-expand-md navbar-light shadow p-0 px-3">
153 <Link title={version} class="navbar-brand" to="/">
154 {this.state.siteName}
156 {this.state.isLoggedIn && (
158 class="ml-auto p-0 navbar-toggler nav-link"
160 title={i18n.t('inbox')}
163 <use xlinkHref="#icon-bell"></use>
165 {this.state.unreadCount > 0 && (
166 <span class="ml-1 badge badge-light">
167 {this.state.unreadCount}
173 class="navbar-toggler"
176 onClick={linkEvent(this, this.expandNavbar)}
177 data-tippy-content={i18n.t('expand_here')}
179 <span class="navbar-toggler-icon"></span>
182 className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
184 <ul class="navbar-nav my-2 mr-auto">
185 <li class="nav-item">
189 title={i18n.t('communities')}
191 {i18n.t('communities')}
194 <li class="nav-item">
198 pathname: '/create_post',
199 state: { prevPath: this.currentLocation },
201 title={i18n.t('create_post')}
203 {i18n.t('create_post')}
206 <li class="nav-item">
209 to="/create_community"
210 title={i18n.t('create_community')}
212 {i18n.t('create_community')}
215 <li className="nav-item">
219 title={i18n.t('donate_to_lemmy')}
222 <use xlinkHref="#icon-coffee"></use>
227 {!this.context.router.history.location.pathname.match(
232 onSubmit={linkEvent(this, this.handleSearchSubmit)}
235 class={`form-control mr-0 search-input ${
236 this.state.toggleSearch ? 'show-input' : 'hide-input'
238 onInput={linkEvent(this, this.handleSearchParam)}
239 value={this.state.searchParam}
240 ref={this.searchTextField}
242 placeholder={i18n.t('search')}
243 onBlur={linkEvent(this, this.handleSearchBlur)}
247 onClick={linkEvent(this, this.handleSearchBtn)}
249 style="color: var(--gray)"
252 <use xlinkHref="#icon-search"></use>
257 <ul class="navbar-nav my-2">
259 <li className="nav-item">
263 title={i18n.t('admin_settings')}
266 <use xlinkHref="#icon-settings"></use>
272 {this.state.isLoggedIn ? (
274 <ul class="navbar-nav my-2">
275 <li className="nav-item">
276 <Link class="nav-link" to="/inbox" title={i18n.t('inbox')}>
278 <use xlinkHref="#icon-bell"></use>
280 {this.state.unreadCount > 0 && (
281 <span class="ml-1 badge badge-light">
282 {this.state.unreadCount}
288 <ul class="navbar-nav">
289 <li className="nav-item">
292 to={`/u/${UserService.Instance.user.username}`}
293 title={i18n.t('settings')}
296 {UserService.Instance.user.avatar && showAvatars() && (
298 src={pictrsAvatarThumbnail(
299 UserService.Instance.user.avatar
303 class="rounded-circle mr-2"
306 {UserService.Instance.user.username}
313 <ul class="navbar-nav my-2">
314 <li className="nav-item">
318 title={i18n.t('login_sign_up')}
320 {i18n.t('login_sign_up')}
330 expandNavbar(i: Navbar) {
331 i.state.expanded = !i.state.expanded;
335 parseMessage(msg: WebSocketJsonResponse) {
336 let res = wsJsonToRes(msg);
338 if (msg.error == 'not_logged_in') {
339 UserService.Instance.logout();
343 } else if (msg.reconnect) {
345 } else if (res.op == UserOperation.GetReplies) {
346 let data = res.data as GetRepliesResponse;
347 let unreadReplies = data.replies.filter(r => !r.read);
349 this.state.replies = unreadReplies;
350 this.state.unreadCount = this.calculateUnreadCount();
351 this.setState(this.state);
352 this.sendUnreadCount();
353 } else if (res.op == UserOperation.GetUserMentions) {
354 let data = res.data as GetUserMentionsResponse;
355 let unreadMentions = data.mentions.filter(r => !r.read);
357 this.state.mentions = unreadMentions;
358 this.state.unreadCount = this.calculateUnreadCount();
359 this.setState(this.state);
360 this.sendUnreadCount();
361 } else if (res.op == UserOperation.GetPrivateMessages) {
362 let data = res.data as PrivateMessagesResponse;
363 let unreadMessages = data.messages.filter(r => !r.read);
365 this.state.messages = unreadMessages;
366 this.state.unreadCount = this.calculateUnreadCount();
367 this.setState(this.state);
368 this.sendUnreadCount();
369 } else if (res.op == UserOperation.CreateComment) {
370 let data = res.data as CommentResponse;
372 if (this.state.isLoggedIn) {
373 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
374 this.state.replies.push(data.comment);
375 this.state.unreadCount++;
376 this.setState(this.state);
377 this.sendUnreadCount();
378 this.notify(data.comment);
381 } else if (res.op == UserOperation.CreatePrivateMessage) {
382 let data = res.data as PrivateMessageResponse;
384 if (this.state.isLoggedIn) {
385 if (data.message.recipient_id == UserService.Instance.user.id) {
386 this.state.messages.push(data.message);
387 this.state.unreadCount++;
388 this.setState(this.state);
389 this.sendUnreadCount();
390 this.notify(data.message);
393 } else if (res.op == UserOperation.GetSite) {
394 let data = res.data as GetSiteResponse;
396 if (data.site && !this.state.siteName) {
397 this.state.siteName = data.site.name;
398 this.state.admins = data.admins;
399 this.setState(this.state);
405 if (this.state.isLoggedIn) {
406 let repliesForm: GetRepliesForm = {
407 sort: SortType[SortType.New],
413 let userMentionsForm: GetUserMentionsForm = {
414 sort: SortType[SortType.New],
420 let privateMessagesForm: GetPrivateMessagesForm = {
426 if (this.currentLocation !== '/inbox') {
427 WebSocketService.Instance.getReplies(repliesForm);
428 WebSocketService.Instance.getUserMentions(userMentionsForm);
429 WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
434 get currentLocation() {
435 return this.context.router.history.location.pathname;
439 UserService.Instance.user.unreadCount = this.state.unreadCount;
440 UserService.Instance.sub.next({
441 user: UserService.Instance.user,
445 calculateUnreadCount(): number {
447 this.state.replies.filter(r => !r.read).length +
448 this.state.mentions.filter(r => !r.read).length +
449 this.state.messages.filter(r => !r.read).length
453 get canAdmin(): boolean {
455 UserService.Instance.user &&
456 this.state.admins.map(a => a.id).includes(UserService.Instance.user.id)
460 requestNotificationPermission() {
461 if (UserService.Instance.user) {
462 document.addEventListener('DOMContentLoaded', function () {
464 toast(i18n.t('notifications_error'), 'danger');
468 if (Notification.permission !== 'granted')
469 Notification.requestPermission();
474 notify(reply: Comment | PrivateMessage) {
475 let creator_name = reply.creator_name;
476 let creator_avatar = reply.creator_avatar
477 ? reply.creator_avatar
478 : `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`;
479 let link = isCommentType(reply)
480 ? `/post/${reply.post_id}/comment/${reply.id}`
482 let htmlBody = md.render(reply.content);
483 let body = reply.content; // Unfortunately the notifications API can't do html
493 if (Notification.permission !== 'granted') Notification.requestPermission();
495 var notification = new Notification(reply.creator_name, {
496 icon: creator_avatar,
500 notification.onclick = () => {
501 event.preventDefault();
502 this.context.router.history.push(link);