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 { i18n } from '../i18next';
35 interface NavbarState {
38 replies: Array<Comment>;
39 mentions: Array<Comment>;
40 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,
67 constructor(props: any, context: any) {
68 super(props, context);
69 this.state = this.emptyState;
71 // Subscribe to user changes
72 this.userSub = UserService.Instance.sub.subscribe(user => {
73 this.state.isLoggedIn = user.user !== undefined;
74 if (this.state.isLoggedIn) {
75 this.state.unreadCount = user.user.unreadCount;
76 this.requestNotificationPermission();
78 this.setState(this.state);
81 this.wsSub = WebSocketService.Instance.subject
82 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
84 msg => this.parseMessage(msg),
85 err => console.error(err),
86 () => console.log('complete')
89 if (this.state.isLoggedIn) {
90 this.requestNotificationPermission();
91 // TODO couldn't get re-logging in to re-fetch unreads
95 WebSocketService.Instance.getSite();
97 this.searchTextField = createRef();
100 handleSearchParam(i: Navbar, event: any) {
101 i.state.searchParam = event.target.value;
106 const searchParam = this.state.searchParam;
107 this.setState({ searchParam: '' });
108 this.setState({ toggleSearch: false });
109 if (searchParam === '') {
110 this.context.router.history.push(`/search/`);
112 this.context.router.history.push(
113 `/search/q/${searchParam}/type/all/sort/topall/page/1`
118 handleSearchSubmit(i: Navbar, event: any) {
119 event.preventDefault();
123 handleSearchBtn(i: Navbar, event: any) {
124 event.preventDefault();
125 i.setState({ toggleSearch: true });
127 i.searchTextField.current.focus();
128 const offsetWidth = i.searchTextField.current.offsetWidth;
129 if (i.state.searchParam && offsetWidth > 100) {
134 handleSearchBlur(i: Navbar, event: any) {
135 if (!(event.relatedTarget && event.relatedTarget.name !== 'search-btn')) {
136 i.state.toggleSearch = false;
142 return this.navbar();
145 componentWillUnmount() {
146 this.wsSub.unsubscribe();
147 this.userSub.unsubscribe();
150 // TODO class active corresponding to current page
153 <nav class="container-fluid navbar navbar-expand-md navbar-light shadow p-0 px-3">
154 <Link title={this.state.version} class="navbar-brand" to="/">
155 {this.state.siteName}
157 {this.state.isLoggedIn && (
159 class="ml-auto p-0 navbar-toggler nav-link"
161 title={i18n.t('inbox')}
164 <use xlinkHref="#icon-bell"></use>
166 {this.state.unreadCount > 0 && (
167 <span class="ml-1 badge badge-light">
168 {this.state.unreadCount}
174 class="navbar-toggler"
177 onClick={linkEvent(this, this.expandNavbar)}
178 data-tippy-content={i18n.t('expand_here')}
180 <span class="navbar-toggler-icon"></span>
183 className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
185 <ul class="navbar-nav my-2 mr-auto">
186 <li class="nav-item">
190 title={i18n.t('communities')}
192 {i18n.t('communities')}
195 <li class="nav-item">
199 pathname: '/create_post',
200 state: { prevPath: this.currentLocation },
202 title={i18n.t('create_post')}
204 {i18n.t('create_post')}
207 <li class="nav-item">
210 to="/create_community"
211 title={i18n.t('create_community')}
213 {i18n.t('create_community')}
216 <li className="nav-item">
220 title={i18n.t('donate_to_lemmy')}
223 <use xlinkHref="#icon-coffee"></use>
228 {!this.context.router.history.location.pathname.match(
233 onSubmit={linkEvent(this, this.handleSearchSubmit)}
236 class={`form-control mr-0 search-input ${
237 this.state.toggleSearch ? 'show-input' : 'hide-input'
239 onInput={linkEvent(this, this.handleSearchParam)}
240 value={this.state.searchParam}
241 ref={this.searchTextField}
243 placeholder={i18n.t('search')}
244 onBlur={linkEvent(this, this.handleSearchBlur)}
248 onClick={linkEvent(this, this.handleSearchBtn)}
250 style="color: var(--gray)"
253 <use xlinkHref="#icon-search"></use>
258 <ul class="navbar-nav my-2">
260 <li className="nav-item">
264 title={i18n.t('admin_settings')}
267 <use xlinkHref="#icon-settings"></use>
273 {this.state.isLoggedIn ? (
275 <ul class="navbar-nav my-2">
276 <li className="nav-item">
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}
289 <ul class="navbar-nav">
290 <li className="nav-item">
293 to={`/u/${UserService.Instance.user.username}`}
294 title={i18n.t('settings')}
297 {UserService.Instance.user.avatar && showAvatars() && (
299 src={pictrsAvatarThumbnail(
300 UserService.Instance.user.avatar
304 class="rounded-circle mr-2"
307 {UserService.Instance.user.username}
314 <ul class="navbar-nav my-2">
315 <li className="nav-item">
319 title={i18n.t('login_sign_up')}
321 {i18n.t('login_sign_up')}
331 expandNavbar(i: Navbar) {
332 i.state.expanded = !i.state.expanded;
336 parseMessage(msg: WebSocketJsonResponse) {
337 let res = wsJsonToRes(msg);
339 if (msg.error == 'not_logged_in') {
340 UserService.Instance.logout();
344 } else if (msg.reconnect) {
346 } else if (res.op == UserOperation.GetReplies) {
347 let data = res.data as GetRepliesResponse;
348 let unreadReplies = data.replies.filter(r => !r.read);
350 this.state.replies = unreadReplies;
351 this.state.unreadCount = this.calculateUnreadCount();
352 this.setState(this.state);
353 this.sendUnreadCount();
354 } else if (res.op == UserOperation.GetUserMentions) {
355 let data = res.data as GetUserMentionsResponse;
356 let unreadMentions = data.mentions.filter(r => !r.read);
358 this.state.mentions = unreadMentions;
359 this.state.unreadCount = this.calculateUnreadCount();
360 this.setState(this.state);
361 this.sendUnreadCount();
362 } else if (res.op == UserOperation.GetPrivateMessages) {
363 let data = res.data as PrivateMessagesResponse;
364 let unreadMessages = data.messages.filter(r => !r.read);
366 this.state.messages = unreadMessages;
367 this.state.unreadCount = this.calculateUnreadCount();
368 this.setState(this.state);
369 this.sendUnreadCount();
370 } else if (res.op == UserOperation.CreateComment) {
371 let data = res.data as CommentResponse;
373 if (this.state.isLoggedIn) {
374 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
375 this.state.replies.push(data.comment);
376 this.state.unreadCount++;
377 this.setState(this.state);
378 this.sendUnreadCount();
379 this.notify(data.comment);
382 } else if (res.op == UserOperation.CreatePrivateMessage) {
383 let data = res.data as PrivateMessageResponse;
385 if (this.state.isLoggedIn) {
386 if (data.message.recipient_id == UserService.Instance.user.id) {
387 this.state.messages.push(data.message);
388 this.state.unreadCount++;
389 this.setState(this.state);
390 this.sendUnreadCount();
391 this.notify(data.message);
394 } else if (res.op == UserOperation.GetSite) {
395 let data = res.data as GetSiteResponse;
397 if (data.site && !this.state.siteName) {
398 this.state.siteName = data.site.name;
399 this.state.version = data.version;
400 this.state.admins = data.admins;
401 this.setState(this.state);
407 if (this.state.isLoggedIn) {
408 let repliesForm: GetRepliesForm = {
409 sort: SortType[SortType.New],
415 let userMentionsForm: GetUserMentionsForm = {
416 sort: SortType[SortType.New],
422 let privateMessagesForm: GetPrivateMessagesForm = {
428 if (this.currentLocation !== '/inbox') {
429 WebSocketService.Instance.getReplies(repliesForm);
430 WebSocketService.Instance.getUserMentions(userMentionsForm);
431 WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
436 get currentLocation() {
437 return this.context.router.history.location.pathname;
441 UserService.Instance.user.unreadCount = this.state.unreadCount;
442 UserService.Instance.sub.next({
443 user: UserService.Instance.user,
447 calculateUnreadCount(): number {
449 this.state.replies.filter(r => !r.read).length +
450 this.state.mentions.filter(r => !r.read).length +
451 this.state.messages.filter(r => !r.read).length
455 get canAdmin(): boolean {
457 UserService.Instance.user &&
458 this.state.admins.map(a => a.id).includes(UserService.Instance.user.id)
462 requestNotificationPermission() {
463 if (UserService.Instance.user) {
464 document.addEventListener('DOMContentLoaded', function () {
466 toast(i18n.t('notifications_error'), 'danger');
470 if (Notification.permission !== 'granted')
471 Notification.requestPermission();
476 notify(reply: Comment | PrivateMessage) {
477 let creator_name = reply.creator_name;
478 let creator_avatar = reply.creator_avatar
479 ? reply.creator_avatar
480 : `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`;
481 let link = isCommentType(reply)
482 ? `/post/${reply.post_id}/comment/${reply.id}`
484 let htmlBody = md.render(reply.content);
485 let body = reply.content; // Unfortunately the notifications API can't do html
495 if (Notification.permission !== 'granted') Notification.requestPermission();
497 var notification = new Notification(reply.creator_name, {
498 icon: creator_avatar,
502 notification.onclick = () => {
503 event.preventDefault();
504 this.context.router.history.push(link);