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,
21 PrivateMessageResponse,
22 WebSocketJsonResponse,
24 } from '../interfaces';
27 pictrsAvatarThumbnail,
35 import { version } from '../version';
36 import { i18n } from '../i18next';
38 interface NavbarState {
41 replies: Array<Comment>;
42 mentions: Array<Comment>;
43 messages: Array<PrivateMessage>;
46 admins: Array<UserView>;
48 toggleSearch: boolean;
51 export class Navbar extends Component<any, NavbarState> {
52 private wsSub: Subscription;
53 private userSub: Subscription;
54 private searchTextField: RefObject<HTMLInputElement>;
55 emptyState: NavbarState = {
56 isLoggedIn: UserService.Instance.user !== undefined,
68 constructor(props: any, context: any) {
69 super(props, context);
70 this.state = this.emptyState;
72 // Subscribe to user changes
73 this.userSub = UserService.Instance.sub.subscribe(user => {
74 this.state.isLoggedIn = user.user !== undefined;
75 if (this.state.isLoggedIn) {
76 this.state.unreadCount = user.user.unreadCount;
77 this.requestNotificationPermission();
79 this.setState(this.state);
82 this.wsSub = WebSocketService.Instance.subject
83 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
85 msg => this.parseMessage(msg),
86 err => console.error(err),
87 () => console.log('complete')
90 if (this.state.isLoggedIn) {
91 this.requestNotificationPermission();
92 // TODO couldn't get re-logging in to re-fetch unreads
96 WebSocketService.Instance.getSite();
98 this.searchTextField = createRef();
101 handleSearchParam(i: Navbar, event: any) {
102 i.state.searchParam = event.target.value;
107 const searchParam = this.state.searchParam;
108 this.setState({ searchParam: '' });
109 this.setState({ toggleSearch: false });
110 if (searchParam === '') {
111 this.context.router.history.push(`/search/`);
113 this.context.router.history.push(
114 `/search/q/${searchParam}/type/all/sort/topall/page/1`
119 handleSearchSubmit(i: Navbar, event: any) {
120 event.preventDefault();
124 handleSearchBtn(i: Navbar, event: any) {
125 event.preventDefault();
126 i.setState({ toggleSearch: true });
128 i.searchTextField.current.focus();
129 const offsetWidth = i.searchTextField.current.offsetWidth;
130 if (i.state.searchParam && offsetWidth > 100) {
135 handleSearchBlur(i: Navbar, event: any) {
136 if (!(event.relatedTarget && event.relatedTarget.name !== 'search-btn')) {
137 i.state.toggleSearch = false;
143 return this.navbar();
146 componentWillUnmount() {
147 this.wsSub.unsubscribe();
148 this.userSub.unsubscribe();
151 // TODO class active corresponding to current page
154 <nav class="container-fluid navbar navbar-expand-md navbar-light shadow p-0 px-3">
155 <Link title={version} class="navbar-brand" to="/">
156 {this.state.siteName}
158 {this.state.isLoggedIn && (
160 class="ml-auto p-0 navbar-toggler nav-link"
162 title={i18n.t('inbox')}
165 <use xlinkHref="#icon-bell"></use>
167 {this.state.unreadCount > 0 && (
168 <span class="ml-1 badge badge-light">
169 {this.state.unreadCount}
175 class="navbar-toggler"
178 onClick={linkEvent(this, this.expandNavbar)}
179 data-tippy-content={i18n.t('expand_here')}
181 <span class="navbar-toggler-icon"></span>
184 className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
186 <ul class="navbar-nav mr-auto">
187 <li class="nav-item">
191 title={i18n.t('communities')}
193 {i18n.t('communities')}
196 <li class="nav-item">
200 pathname: '/create_post',
201 state: { prevPath: this.currentLocation },
203 title={i18n.t('create_post')}
205 {i18n.t('create_post')}
208 <li class="nav-item">
211 to="/create_community"
212 title={i18n.t('create_community')}
214 {i18n.t('create_community')}
217 <li className="nav-item">
221 title={i18n.t('donate_to_lemmy')}
224 <use xlinkHref="#icon-coffee"></use>
229 {!this.context.router.history.location.pathname.match(
232 <div class="nav-item my-2">
235 onSubmit={linkEvent(this, this.handleSearchSubmit)}
238 class={`form-control mr-0 search-input ${
239 this.state.toggleSearch ? 'show-input' : 'hide-input'
241 onInput={linkEvent(this, this.handleSearchParam)}
242 value={this.state.searchParam}
243 ref={this.searchTextField}
245 placeholder={i18n.t('search')}
246 onBlur={linkEvent(this, this.handleSearchBlur)}
248 <div class="mx-sm-2">
251 onClick={linkEvent(this, this.handleSearchBtn)}
252 class="btn btn-secondary"
260 <ul class="navbar-nav ml-2">
262 <li className="nav-item mt-1">
266 title={i18n.t('admin_settings')}
269 <use xlinkHref="#icon-settings"></use>
274 {this.state.isLoggedIn ? (
276 <li className="nav-item mt-1">
277 <Link class="nav-link" to="/inbox" title={i18n.t('inbox')}>
279 <use xlinkHref="#icon-bell"></use>
281 {this.state.unreadCount > 0 && (
282 <span class="ml-1 badge badge-light">
283 {this.state.unreadCount}
288 <li className="nav-item">
291 to={`/u/${UserService.Instance.user.username}`}
292 title={i18n.t('settings')}
295 {UserService.Instance.user.avatar && showAvatars() && (
297 src={pictrsAvatarThumbnail(
298 UserService.Instance.user.avatar
302 class="rounded-circle mr-2"
305 {UserService.Instance.user.username}
314 title={i18n.t('login_sign_up')}
316 {i18n.t('login_sign_up')}
325 expandNavbar(i: Navbar) {
326 i.state.expanded = !i.state.expanded;
330 parseMessage(msg: WebSocketJsonResponse) {
331 let res = wsJsonToRes(msg);
333 if (msg.error == 'not_logged_in') {
334 UserService.Instance.logout();
338 } else if (msg.reconnect) {
340 } else if (res.op == UserOperation.GetReplies) {
341 let data = res.data as GetRepliesResponse;
342 let unreadReplies = data.replies.filter(r => !r.read);
344 this.state.replies = unreadReplies;
345 this.state.unreadCount = this.calculateUnreadCount();
346 this.setState(this.state);
347 this.sendUnreadCount();
348 } else if (res.op == UserOperation.GetUserMentions) {
349 let data = res.data as GetUserMentionsResponse;
350 let unreadMentions = data.mentions.filter(r => !r.read);
352 this.state.mentions = unreadMentions;
353 this.state.unreadCount = this.calculateUnreadCount();
354 this.setState(this.state);
355 this.sendUnreadCount();
356 } else if (res.op == UserOperation.GetPrivateMessages) {
357 let data = res.data as PrivateMessagesResponse;
358 let unreadMessages = data.messages.filter(r => !r.read);
360 this.state.messages = unreadMessages;
361 this.state.unreadCount = this.calculateUnreadCount();
362 this.setState(this.state);
363 this.sendUnreadCount();
364 } else if (res.op == UserOperation.CreateComment) {
365 let data = res.data as CommentResponse;
367 if (this.state.isLoggedIn) {
368 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
369 this.state.replies.push(data.comment);
370 this.state.unreadCount++;
371 this.setState(this.state);
372 this.sendUnreadCount();
373 this.notify(data.comment);
376 } else if (res.op == UserOperation.CreatePrivateMessage) {
377 let data = res.data as PrivateMessageResponse;
379 if (this.state.isLoggedIn) {
380 if (data.message.recipient_id == UserService.Instance.user.id) {
381 this.state.messages.push(data.message);
382 this.state.unreadCount++;
383 this.setState(this.state);
384 this.sendUnreadCount();
385 this.notify(data.message);
388 } else if (res.op == UserOperation.GetSite) {
389 let data = res.data as GetSiteResponse;
391 if (data.site && !this.state.siteName) {
392 this.state.siteName = data.site.name;
393 this.state.admins = data.admins;
394 WebSocketService.Instance.site = data.site;
395 WebSocketService.Instance.admins = data.admins;
397 this.setState(this.state);
403 if (this.state.isLoggedIn) {
404 let repliesForm: GetRepliesForm = {
405 sort: SortType[SortType.New],
411 let userMentionsForm: GetUserMentionsForm = {
412 sort: SortType[SortType.New],
418 let privateMessagesForm: GetPrivateMessagesForm = {
424 if (this.currentLocation !== '/inbox') {
425 WebSocketService.Instance.getReplies(repliesForm);
426 WebSocketService.Instance.getUserMentions(userMentionsForm);
427 WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
432 get currentLocation() {
433 return this.context.router.history.location.pathname;
437 UserService.Instance.user.unreadCount = this.state.unreadCount;
438 UserService.Instance.sub.next({
439 user: UserService.Instance.user,
443 calculateUnreadCount(): number {
445 this.state.replies.filter(r => !r.read).length +
446 this.state.mentions.filter(r => !r.read).length +
447 this.state.messages.filter(r => !r.read).length
451 get canAdmin(): boolean {
453 UserService.Instance.user &&
454 this.state.admins.map(a => a.id).includes(UserService.Instance.user.id)
458 requestNotificationPermission() {
459 if (UserService.Instance.user) {
460 document.addEventListener('DOMContentLoaded', function () {
462 toast(i18n.t('notifications_error'), 'danger');
466 if (Notification.permission !== 'granted')
467 Notification.requestPermission();
472 notify(reply: Comment | PrivateMessage) {
473 let creator_name = reply.creator_name;
474 let creator_avatar = reply.creator_avatar
475 ? reply.creator_avatar
476 : `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`;
477 let link = isCommentType(reply)
478 ? `/post/${reply.post_id}/comment/${reply.id}`
480 let htmlBody = md.render(reply.content);
481 let body = reply.content; // Unfortunately the notifications API can't do html
491 if (Notification.permission !== 'granted') Notification.requestPermission();
493 var notification = new Notification(reply.creator_name, {
494 icon: creator_avatar,
498 notification.onclick = () => {
499 event.preventDefault();
500 this.context.router.history.push(link);