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,
19 PrivateMessageResponse,
20 WebSocketJsonResponse,
21 } from 'lemmy-js-client';
24 pictrsAvatarThumbnail,
34 import { i18n } from '../i18next';
36 interface NavbarProps {
37 site: GetSiteResponse;
40 interface NavbarState {
45 messages: PrivateMessage[];
48 toggleSearch: boolean;
49 siteRes: GetSiteResponse;
50 onSiteBanner?(url: string): any;
53 export class Navbar extends Component<NavbarProps, NavbarState> {
54 private wsSub: Subscription;
55 private userSub: Subscription;
56 private unreadCountSub: Subscription;
57 private searchTextField: RefObject<HTMLInputElement>;
58 emptyState: NavbarState = {
59 isLoggedIn: !!this.props.site.my_user,
65 siteRes: this.props.site, // TODO this could probably go away
70 constructor(props: any, context: any) {
71 super(props, context);
72 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 if (this.props.site.my_user) {
90 UserService.Instance.user = this.props.site.my_user;
93 WebSocketService.Instance.userJoin();
94 // On the first load, check the unreads
95 if (this.state.isLoggedIn == false) {
96 this.requestNotificationPermission();
98 // setTheme(data.my_user.theme, true);
99 // i18n.changeLanguage(getLanguage());
105 componentDidMount() {
106 // Subscribe to jwt changes
108 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
110 if (res !== undefined) {
111 this.requestNotificationPermission();
113 this.state.isLoggedIn = false;
115 console.log('a new login');
116 WebSocketService.Instance.getSite();
117 this.setState(this.state);
120 // Subscribe to unread count changes
121 this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(
123 this.setState({ unreadCount: res });
129 handleSearchParam(i: Navbar, event: any) {
130 i.state.searchParam = event.target.value;
135 const searchParam = this.state.searchParam;
136 this.setState({ searchParam: '' });
137 this.setState({ toggleSearch: false });
138 if (searchParam === '') {
139 this.context.router.history.push(`/search/`);
141 this.context.router.history.push(
142 `/search/q/${searchParam}/type/All/sort/TopAll/page/1`
147 handleSearchSubmit(i: Navbar, event: any) {
148 event.preventDefault();
152 handleSearchBtn(i: Navbar, event: any) {
153 event.preventDefault();
154 i.setState({ toggleSearch: true });
156 i.searchTextField.current.focus();
157 const offsetWidth = i.searchTextField.current.offsetWidth;
158 if (i.state.searchParam && offsetWidth > 100) {
163 handleSearchBlur(i: Navbar, event: any) {
164 if (!(event.relatedTarget && event.relatedTarget.name !== 'search-btn')) {
165 i.state.toggleSearch = false;
171 return this.navbar();
174 componentWillUnmount() {
175 this.wsSub.unsubscribe();
176 this.userSub.unsubscribe();
177 this.unreadCountSub.unsubscribe();
180 // TODO class active corresponding to current page
182 let user = UserService.Instance.user;
184 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
185 <div class="container">
187 title={this.state.siteRes.version}
188 className="d-flex align-items-center navbar-brand mr-md-3"
191 {this.state.siteRes.site.icon && showAvatars() && (
193 src={pictrsAvatarThumbnail(this.state.siteRes.site.icon)}
196 class="rounded-circle mr-2"
199 {this.state.siteRes.site.name}
201 {this.state.isLoggedIn && (
203 className="ml-auto p-0 navbar-toggler nav-link border-0"
205 title={i18n.t('inbox')}
208 <use xlinkHref="#icon-bell"></use>
210 {this.state.unreadCount > 0 && (
211 <span class="mx-1 badge badge-light">
212 {this.state.unreadCount}
218 class="navbar-toggler border-0 p-1"
221 onClick={linkEvent(this, this.expandNavbar)}
222 data-tippy-content={i18n.t('expand_here')}
224 <span class="navbar-toggler-icon"></span>
227 className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
229 <ul class="navbar-nav my-2 mr-auto">
230 <li class="nav-item">
234 title={i18n.t('communities')}
236 {i18n.t('communities')}
239 <li class="nav-item">
243 pathname: '/create_post',
244 state: { prevPath: this.currentLocation },
246 title={i18n.t('create_post')}
248 {i18n.t('create_post')}
251 <li class="nav-item">
254 to="/create_community"
255 title={i18n.t('create_community')}
257 {i18n.t('create_community')}
260 <li className="nav-item">
264 title={i18n.t('donate_to_lemmy')}
267 <use xlinkHref="#icon-coffee"></use>
272 <ul class="navbar-nav my-2">
274 <li className="nav-item">
278 title={i18n.t('admin_settings')}
281 <use xlinkHref="#icon-settings"></use>
287 {!this.context.router.history.location.pathname.match(
292 onSubmit={linkEvent(this, this.handleSearchSubmit)}
295 class={`form-control mr-0 search-input ${
296 this.state.toggleSearch ? 'show-input' : 'hide-input'
298 onInput={linkEvent(this, this.handleSearchParam)}
299 value={this.state.searchParam}
300 ref={this.searchTextField}
302 placeholder={i18n.t('search')}
303 onBlur={linkEvent(this, this.handleSearchBlur)}
307 onClick={linkEvent(this, this.handleSearchBtn)}
308 class="px-1 btn btn-link"
309 style="color: var(--gray)"
312 <use xlinkHref="#icon-search"></use>
317 {this.state.isLoggedIn ? (
319 <ul class="navbar-nav my-2">
320 <li className="nav-item">
324 title={i18n.t('inbox')}
327 <use xlinkHref="#icon-bell"></use>
329 {this.state.unreadCount > 0 && (
330 <span class="ml-1 badge badge-light">
331 {this.state.unreadCount}
337 <ul class="navbar-nav">
338 <li className="nav-item">
341 to={`/u/${user.name}`}
342 title={i18n.t('settings')}
345 {user.avatar && showAvatars() && (
347 src={pictrsAvatarThumbnail(user.avatar)}
350 class="rounded-circle mr-2"
353 {user.preferred_username
354 ? user.preferred_username
362 <ul class="navbar-nav my-2">
363 <li className="ml-2 nav-item">
365 className="btn btn-success"
367 title={i18n.t('login_sign_up')}
369 {i18n.t('login_sign_up')}
380 expandNavbar(i: Navbar) {
381 i.state.expanded = !i.state.expanded;
385 parseMessage(msg: WebSocketJsonResponse) {
386 let res = wsJsonToRes(msg);
388 if (msg.error == 'not_logged_in') {
389 UserService.Instance.logout();
393 } else if (msg.reconnect) {
395 } else if (res.op == UserOperation.GetReplies) {
396 let data = res.data as GetRepliesResponse;
397 let unreadReplies = data.replies.filter(r => !r.read);
399 this.state.replies = unreadReplies;
400 this.state.unreadCount = this.calculateUnreadCount();
401 this.setState(this.state);
402 this.sendUnreadCount();
403 } else if (res.op == UserOperation.GetUserMentions) {
404 let data = res.data as GetUserMentionsResponse;
405 let unreadMentions = data.mentions.filter(r => !r.read);
407 this.state.mentions = unreadMentions;
408 this.state.unreadCount = this.calculateUnreadCount();
409 this.setState(this.state);
410 this.sendUnreadCount();
411 } else if (res.op == UserOperation.GetPrivateMessages) {
412 let data = res.data as PrivateMessagesResponse;
413 let unreadMessages = data.messages.filter(r => !r.read);
415 this.state.messages = unreadMessages;
416 this.state.unreadCount = this.calculateUnreadCount();
417 this.setState(this.state);
418 this.sendUnreadCount();
419 } else if (res.op == UserOperation.CreateComment) {
420 let data = res.data as CommentResponse;
422 if (this.state.isLoggedIn) {
423 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
424 this.state.replies.push(data.comment);
425 this.state.unreadCount++;
426 this.setState(this.state);
427 this.sendUnreadCount();
428 notifyComment(data.comment, this.context.router);
431 } else if (res.op == UserOperation.CreatePrivateMessage) {
432 let data = res.data as PrivateMessageResponse;
434 if (this.state.isLoggedIn) {
435 if (data.message.recipient_id == UserService.Instance.user.id) {
436 this.state.messages.push(data.message);
437 this.state.unreadCount++;
438 this.setState(this.state);
439 this.sendUnreadCount();
440 notifyPrivateMessage(data.message, this.context.router);
445 // TODO all this needs to be moved
446 else if (res.op == UserOperation.GetSite) {
447 let data = res.data as GetSiteResponse;
449 this.state.siteRes = data;
453 UserService.Instance.user = data.my_user;
454 WebSocketService.Instance.userJoin();
455 // On the first load, check the unreads
456 if (this.state.isLoggedIn == false) {
457 this.requestNotificationPermission();
459 setTheme(data.my_user.theme, true);
460 i18n.changeLanguage(getLanguage());
462 this.state.isLoggedIn = true;
465 this.setState(this.state);
470 console.log('Fetching unreads...');
471 let repliesForm: GetRepliesForm = {
478 let userMentionsForm: GetUserMentionsForm = {
485 let privateMessagesForm: GetPrivateMessagesForm = {
491 if (this.currentLocation !== '/inbox') {
492 WebSocketService.Instance.getReplies(repliesForm);
493 WebSocketService.Instance.getUserMentions(userMentionsForm);
494 WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
498 get currentLocation() {
499 return this.context.router.history.location.pathname;
503 UserService.Instance.unreadCountSub.next(this.state.unreadCount);
506 calculateUnreadCount(): number {
508 this.state.replies.filter(r => !r.read).length +
509 this.state.mentions.filter(r => !r.read).length +
510 this.state.messages.filter(r => !r.read).length
514 get canAdmin(): boolean {
516 UserService.Instance.user &&
517 this.state.siteRes.admins
519 .includes(UserService.Instance.user.id)
523 requestNotificationPermission() {
524 if (UserService.Instance.user) {
525 document.addEventListener('DOMContentLoaded', function () {
527 toast(i18n.t('notifications_error'), 'danger');
531 if (Notification.permission !== 'granted')
532 Notification.requestPermission();