1 import { Component, linkEvent, createRef, RefObject } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { Subscription } from 'rxjs';
4 import { WebSocketService, UserService } from '../services';
10 GetUserMentionsResponse,
11 GetPrivateMessagesForm,
12 PrivateMessagesResponse,
18 PrivateMessageResponse,
19 WebSocketJsonResponse,
20 } from 'lemmy-js-client';
23 pictrsAvatarThumbnail,
34 import { i18n } from '../i18next';
36 interface NavbarProps {
37 site: GetSiteResponse;
40 interface NavbarState {
45 messages: PrivateMessage[];
48 toggleSearch: boolean;
49 onSiteBanner?(url: string): any;
52 export class Navbar extends Component<NavbarProps, NavbarState> {
53 private wsSub: Subscription;
54 private userSub: Subscription;
55 private unreadCountSub: Subscription;
56 private searchTextField: RefObject<HTMLInputElement>;
57 emptyState: NavbarState = {
58 isLoggedIn: !!this.props.site.my_user,
69 constructor(props: any, context: any) {
70 super(props, context);
71 this.state = this.emptyState;
73 this.parseMessage = this.parseMessage.bind(this);
74 this.subscription = wsSubscribe(this.parseMessage);
77 // TODO this needs some work
78 UserService.Instance.user = this.props.site.my_user;
79 i18n.changeLanguage(getLanguage());
80 if (UserService.Instance.user) {
81 setTheme(UserService.Instance.user.theme);
84 // if (!!this.props.site.my_user) {
85 // UserService.Instance.this.props.site.my_user);
86 // // UserService.Instance.user = this.props.site.my_user;
88 // UserService.Instance.setUser(undefined);
93 // Subscribe to jwt changes
95 this.searchTextField = createRef();
96 console.log(`isLoggedIn = ${this.state.isLoggedIn}`);
98 // On the first load, check the unreads
99 if (this.state.isLoggedIn == false) {
100 // setTheme(data.my_user.theme, true);
101 // i18n.changeLanguage(getLanguage());
102 // i18n.changeLanguage('de');
104 this.requestNotificationPermission();
105 WebSocketService.Instance.userJoin();
109 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
111 if (res !== undefined) {
112 this.requestNotificationPermission();
113 WebSocketService.Instance.getSite();
115 this.setState({ isLoggedIn: false });
119 // Subscribe to unread count changes
120 this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(
122 this.setState({ unreadCount: res });
128 handleSearchParam(i: Navbar, event: any) {
129 i.state.searchParam = event.target.value;
134 const searchParam = this.state.searchParam;
135 this.setState({ searchParam: '' });
136 this.setState({ toggleSearch: false });
137 if (searchParam === '') {
138 this.context.router.history.push(`/search/`);
140 const searchParamEncoded = encodeURIComponent(searchParam);
141 this.context.router.history.push(
142 `/search/q/${searchParamEncoded}/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">
186 {this.props.site.site && (
188 title={this.props.site.version}
189 className="d-flex align-items-center navbar-brand mr-md-3"
192 {this.props.site.site.icon && showAvatars() && (
194 src={pictrsAvatarThumbnail(this.props.site.site.icon)}
197 class="rounded-circle mr-2"
200 {this.props.site.site.name}
203 {this.state.isLoggedIn && (
205 className="ml-auto p-0 navbar-toggler nav-link border-0"
207 title={i18n.t('inbox')}
210 <use xlinkHref="#icon-bell"></use>
212 {this.state.unreadCount > 0 && (
213 <span class="mx-1 badge badge-light">
214 {this.state.unreadCount}
220 class="navbar-toggler border-0 p-1"
223 onClick={linkEvent(this, this.expandNavbar)}
224 data-tippy-content={i18n.t('expand_here')}
226 <span class="navbar-toggler-icon"></span>
229 className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
231 <ul class="navbar-nav my-2 mr-auto">
232 <li class="nav-item">
236 title={i18n.t('communities')}
238 {i18n.t('communities')}
241 <li class="nav-item">
245 pathname: '/create_post',
246 state: { prevPath: this.currentLocation },
248 title={i18n.t('create_post')}
250 {i18n.t('create_post')}
253 <li class="nav-item">
256 to="/create_community"
257 title={i18n.t('create_community')}
259 {i18n.t('create_community')}
262 <li className="nav-item">
266 title={i18n.t('donate_to_lemmy')}
269 <use xlinkHref="#icon-coffee"></use>
274 <ul class="navbar-nav my-2">
276 <li className="nav-item">
280 title={i18n.t('admin_settings')}
283 <use xlinkHref="#icon-settings"></use>
289 {!this.context.router.history.location.pathname.match(
294 onSubmit={linkEvent(this, this.handleSearchSubmit)}
297 class={`form-control mr-0 search-input ${
298 this.state.toggleSearch ? 'show-input' : 'hide-input'
300 onInput={linkEvent(this, this.handleSearchParam)}
301 value={this.state.searchParam}
302 ref={this.searchTextField}
304 placeholder={i18n.t('search')}
305 onBlur={linkEvent(this, this.handleSearchBlur)}
309 onClick={linkEvent(this, this.handleSearchBtn)}
310 class="px-1 btn btn-link"
311 style="color: var(--gray)"
314 <use xlinkHref="#icon-search"></use>
319 {this.state.isLoggedIn ? (
321 <ul class="navbar-nav my-2">
322 <li className="nav-item">
326 title={i18n.t('inbox')}
329 <use xlinkHref="#icon-bell"></use>
331 {this.state.unreadCount > 0 && (
332 <span class="ml-1 badge badge-light">
333 {this.state.unreadCount}
339 <ul class="navbar-nav">
340 <li className="nav-item">
343 to={`/u/${user.name}`}
344 title={i18n.t('settings')}
347 {user.avatar && showAvatars() && (
349 src={pictrsAvatarThumbnail(user.avatar)}
352 class="rounded-circle mr-2"
355 {user.preferred_username
356 ? user.preferred_username
364 <ul class="navbar-nav my-2">
365 <li className="ml-2 nav-item">
367 className="btn btn-success"
369 title={i18n.t('login_sign_up')}
371 {i18n.t('login_sign_up')}
382 expandNavbar(i: Navbar) {
383 i.state.expanded = !i.state.expanded;
387 parseMessage(msg: WebSocketJsonResponse) {
388 let res = wsJsonToRes(msg);
390 if (msg.error == 'not_logged_in') {
391 UserService.Instance.logout();
395 } else if (msg.reconnect) {
396 WebSocketService.Instance.userJoin();
398 } else if (res.op == UserOperation.GetReplies) {
399 let data = res.data as GetRepliesResponse;
400 let unreadReplies = data.replies.filter(r => !r.read);
402 this.state.replies = unreadReplies;
403 this.state.unreadCount = this.calculateUnreadCount();
404 this.setState(this.state);
405 this.sendUnreadCount();
406 } else if (res.op == UserOperation.GetUserMentions) {
407 let data = res.data as GetUserMentionsResponse;
408 let unreadMentions = data.mentions.filter(r => !r.read);
410 this.state.mentions = unreadMentions;
411 this.state.unreadCount = this.calculateUnreadCount();
412 this.setState(this.state);
413 this.sendUnreadCount();
414 } else if (res.op == UserOperation.GetPrivateMessages) {
415 let data = res.data as PrivateMessagesResponse;
416 let unreadMessages = data.messages.filter(r => !r.read);
418 this.state.messages = unreadMessages;
419 this.state.unreadCount = this.calculateUnreadCount();
420 this.setState(this.state);
421 this.sendUnreadCount();
422 } else if (res.op == UserOperation.GetSite) {
423 // This is only called on a successful login
424 let data = res.data as GetSiteResponse;
425 UserService.Instance.user = data.my_user;
426 setTheme(UserService.Instance.user.theme);
427 i18n.changeLanguage(getLanguage());
428 this.state.isLoggedIn = true;
429 this.setState(this.state);
430 } else if (res.op == UserOperation.CreateComment) {
431 let data = res.data as CommentResponse;
433 if (this.state.isLoggedIn) {
434 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
435 this.state.replies.push(data.comment);
436 this.state.unreadCount++;
437 this.setState(this.state);
438 this.sendUnreadCount();
439 notifyComment(data.comment, this.context.router);
442 } else if (res.op == UserOperation.CreatePrivateMessage) {
443 let data = res.data as PrivateMessageResponse;
445 if (this.state.isLoggedIn) {
446 if (data.message.recipient_id == UserService.Instance.user.id) {
447 this.state.messages.push(data.message);
448 this.state.unreadCount++;
449 this.setState(this.state);
450 this.sendUnreadCount();
451 notifyPrivateMessage(data.message, this.context.router);
458 console.log('Fetching unreads...');
459 let repliesForm: GetRepliesForm = {
466 let userMentionsForm: GetUserMentionsForm = {
473 let privateMessagesForm: GetPrivateMessagesForm = {
479 if (this.currentLocation !== '/inbox') {
480 WebSocketService.Instance.getReplies(repliesForm);
481 WebSocketService.Instance.getUserMentions(userMentionsForm);
482 WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
486 get currentLocation() {
487 return this.context.router.history.location.pathname;
491 UserService.Instance.unreadCountSub.next(this.state.unreadCount);
494 calculateUnreadCount(): number {
496 this.state.replies.filter(r => !r.read).length +
497 this.state.mentions.filter(r => !r.read).length +
498 this.state.messages.filter(r => !r.read).length
502 get canAdmin(): boolean {
504 UserService.Instance.user &&
505 this.props.site.admins
507 .includes(UserService.Instance.user.id)
511 requestNotificationPermission() {
512 if (UserService.Instance.user) {
513 document.addEventListener('DOMContentLoaded', function () {
515 toast(i18n.t('notifications_error'), 'danger');
519 if (Notification.permission !== 'granted')
520 Notification.requestPermission();