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,
35 import { i18n } from '../i18next';
37 interface NavbarState {
40 replies: Array<Comment>;
41 mentions: Array<Comment>;
42 messages: Array<PrivateMessage>;
46 admins: Array<UserView>;
48 toggleSearch: boolean;
52 export class Navbar extends Component<any, NavbarState> {
53 private wsSub: Subscription;
54 private userSub: Subscription;
55 private unreadCountSub: Subscription;
56 private searchTextField: RefObject<HTMLInputElement>;
57 emptyState: NavbarState = {
72 constructor(props: any, context: any) {
73 super(props, context);
74 this.state = this.emptyState;
76 this.wsSub = WebSocketService.Instance.subject
77 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
79 msg => this.parseMessage(msg),
80 err => console.error(err),
81 () => console.log('complete')
84 WebSocketService.Instance.getSite();
86 this.searchTextField = createRef();
90 // Subscribe to jwt changes
91 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
93 if (res !== undefined) {
94 this.requestNotificationPermission();
96 this.state.isLoggedIn = false;
98 WebSocketService.Instance.getSite();
99 this.setState(this.state);
102 // Subscribe to unread count changes
103 this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(res => {
104 this.setState({ unreadCount: res });
108 handleSearchParam(i: Navbar, event: any) {
109 i.state.searchParam = event.target.value;
114 const searchParam = this.state.searchParam;
115 this.setState({ searchParam: '' });
116 this.setState({ toggleSearch: false });
117 if (searchParam === '') {
118 this.context.router.history.push(`/search/`);
120 this.context.router.history.push(
121 `/search/q/${searchParam}/type/all/sort/topall/page/1`
126 handleSearchSubmit(i: Navbar, event: any) {
127 event.preventDefault();
131 handleSearchBtn(i: Navbar, event: any) {
132 event.preventDefault();
133 i.setState({ toggleSearch: true });
135 i.searchTextField.current.focus();
136 const offsetWidth = i.searchTextField.current.offsetWidth;
137 if (i.state.searchParam && offsetWidth > 100) {
142 handleSearchBlur(i: Navbar, event: any) {
143 if (!(event.relatedTarget && event.relatedTarget.name !== 'search-btn')) {
144 i.state.toggleSearch = false;
150 return this.navbar();
153 componentWillUnmount() {
154 this.wsSub.unsubscribe();
155 this.userSub.unsubscribe();
156 this.unreadCountSub.unsubscribe();
159 // TODO class active corresponding to current page
162 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
163 <div class="container">
164 {!this.state.siteLoading ? (
165 <Link title={this.state.version} class="navbar-brand" to="/">
166 {this.state.siteName}
169 <div class="navbar-item">
170 <svg class="icon icon-spinner spin">
171 <use xlinkHref="#icon-spinner"></use>
175 {this.state.isLoggedIn && (
177 class="ml-auto p-0 navbar-toggler nav-link border-0"
179 title={i18n.t('inbox')}
182 <use xlinkHref="#icon-bell"></use>
184 {this.state.unreadCount > 0 && (
185 <span class="ml-1 badge badge-light">
186 {this.state.unreadCount}
192 class="navbar-toggler border-0"
195 onClick={linkEvent(this, this.expandNavbar)}
196 data-tippy-content={i18n.t('expand_here')}
198 <span class="navbar-toggler-icon"></span>
200 {!this.state.siteLoading && (
203 !this.state.expanded && 'collapse'
206 <ul class="navbar-nav my-2 mr-auto">
207 <li class="nav-item">
211 title={i18n.t('communities')}
213 {i18n.t('communities')}
216 <li class="nav-item">
220 pathname: '/create_post',
221 state: { prevPath: this.currentLocation },
223 title={i18n.t('create_post')}
225 {i18n.t('create_post')}
228 <li class="nav-item">
231 to="/create_community"
232 title={i18n.t('create_community')}
234 {i18n.t('create_community')}
237 <li className="nav-item">
241 title={i18n.t('donate_to_lemmy')}
244 <use xlinkHref="#icon-coffee"></use>
249 {!this.context.router.history.location.pathname.match(
254 onSubmit={linkEvent(this, this.handleSearchSubmit)}
257 class={`form-control mr-0 search-input ${
258 this.state.toggleSearch ? 'show-input' : 'hide-input'
260 onInput={linkEvent(this, this.handleSearchParam)}
261 value={this.state.searchParam}
262 ref={this.searchTextField}
264 placeholder={i18n.t('search')}
265 onBlur={linkEvent(this, this.handleSearchBlur)}
269 onClick={linkEvent(this, this.handleSearchBtn)}
271 style="color: var(--gray)"
274 <use xlinkHref="#icon-search"></use>
279 <ul class="navbar-nav my-2">
281 <li className="nav-item">
285 title={i18n.t('admin_settings')}
288 <use xlinkHref="#icon-settings"></use>
294 {this.state.isLoggedIn ? (
296 <ul class="navbar-nav my-2">
297 <li className="nav-item">
301 title={i18n.t('inbox')}
304 <use xlinkHref="#icon-bell"></use>
306 {this.state.unreadCount > 0 && (
307 <span class="ml-1 badge badge-light">
308 {this.state.unreadCount}
314 <ul class="navbar-nav">
315 <li className="nav-item">
318 to={`/u/${UserService.Instance.user.name}`}
319 title={i18n.t('settings')}
322 {UserService.Instance.user.avatar &&
325 src={pictrsAvatarThumbnail(
326 UserService.Instance.user.avatar
330 class="rounded-circle mr-2"
333 {UserService.Instance.user.name}
340 <ul class="navbar-nav my-2">
341 <li className="nav-item">
343 class="btn btn-success"
345 title={i18n.t('login_sign_up')}
347 {i18n.t('login_sign_up')}
359 expandNavbar(i: Navbar) {
360 i.state.expanded = !i.state.expanded;
364 parseMessage(msg: WebSocketJsonResponse) {
365 let res = wsJsonToRes(msg);
367 if (msg.error == 'not_logged_in') {
368 UserService.Instance.logout();
372 } else if (msg.reconnect) {
374 } else if (res.op == UserOperation.GetReplies) {
375 let data = res.data as GetRepliesResponse;
376 let unreadReplies = data.replies.filter(r => !r.read);
378 this.state.replies = unreadReplies;
379 this.state.unreadCount = this.calculateUnreadCount();
380 this.setState(this.state);
381 this.sendUnreadCount();
382 } else if (res.op == UserOperation.GetUserMentions) {
383 let data = res.data as GetUserMentionsResponse;
384 let unreadMentions = data.mentions.filter(r => !r.read);
386 this.state.mentions = unreadMentions;
387 this.state.unreadCount = this.calculateUnreadCount();
388 this.setState(this.state);
389 this.sendUnreadCount();
390 } else if (res.op == UserOperation.GetPrivateMessages) {
391 let data = res.data as PrivateMessagesResponse;
392 let unreadMessages = data.messages.filter(r => !r.read);
394 this.state.messages = unreadMessages;
395 this.state.unreadCount = this.calculateUnreadCount();
396 this.setState(this.state);
397 this.sendUnreadCount();
398 } else if (res.op == UserOperation.CreateComment) {
399 let data = res.data as CommentResponse;
401 if (this.state.isLoggedIn) {
402 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
403 this.state.replies.push(data.comment);
404 this.state.unreadCount++;
405 this.setState(this.state);
406 this.sendUnreadCount();
407 this.notify(data.comment);
410 } else if (res.op == UserOperation.CreatePrivateMessage) {
411 let data = res.data as PrivateMessageResponse;
413 if (this.state.isLoggedIn) {
414 if (data.message.recipient_id == UserService.Instance.user.id) {
415 this.state.messages.push(data.message);
416 this.state.unreadCount++;
417 this.setState(this.state);
418 this.sendUnreadCount();
419 this.notify(data.message);
422 } else if (res.op == UserOperation.GetSite) {
423 let data = res.data as GetSiteResponse;
425 if (data.site && !this.state.siteName) {
426 this.state.siteName = data.site.name;
427 this.state.version = data.version;
428 this.state.admins = data.admins;
433 UserService.Instance.user = data.my_user;
434 WebSocketService.Instance.userJoin();
435 // On the first load, check the unreads
436 if (this.state.isLoggedIn == false) {
437 this.requestNotificationPermission();
439 setTheme(data.my_user.theme, true);
440 i18n.changeLanguage(getLanguage());
442 this.state.isLoggedIn = true;
445 this.state.siteLoading = false;
446 this.setState(this.state);
451 console.log('Fetching unreads...');
452 let repliesForm: GetRepliesForm = {
453 sort: SortType[SortType.New],
459 let userMentionsForm: GetUserMentionsForm = {
460 sort: SortType[SortType.New],
466 let privateMessagesForm: GetPrivateMessagesForm = {
472 if (this.currentLocation !== '/inbox') {
473 WebSocketService.Instance.getReplies(repliesForm);
474 WebSocketService.Instance.getUserMentions(userMentionsForm);
475 WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
479 get currentLocation() {
480 return this.context.router.history.location.pathname;
484 UserService.Instance.unreadCountSub.next(this.state.unreadCount);
487 calculateUnreadCount(): number {
489 this.state.replies.filter(r => !r.read).length +
490 this.state.mentions.filter(r => !r.read).length +
491 this.state.messages.filter(r => !r.read).length
495 get canAdmin(): boolean {
497 UserService.Instance.user &&
498 this.state.admins.map(a => a.id).includes(UserService.Instance.user.id)
502 requestNotificationPermission() {
503 if (UserService.Instance.user) {
504 document.addEventListener('DOMContentLoaded', function () {
506 toast(i18n.t('notifications_error'), 'danger');
510 if (Notification.permission !== 'granted')
511 Notification.requestPermission();
516 notify(reply: Comment | PrivateMessage) {
517 let creator_name = reply.creator_name;
518 let creator_avatar = reply.creator_avatar
519 ? reply.creator_avatar
520 : `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`;
521 let link = isCommentType(reply)
522 ? `/post/${reply.post_id}/comment/${reply.id}`
524 let htmlBody = md.render(reply.content);
525 let body = reply.content; // Unfortunately the notifications API can't do html
535 if (Notification.permission !== 'granted') Notification.requestPermission();
537 var notification = new Notification(reply.creator_name, {
538 icon: creator_avatar,
542 notification.onclick = () => {
543 event.preventDefault();
544 this.context.router.history.push(link);