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,
12 PrivateMessagesResponse,
17 PrivateMessageResponse,
19 } from 'lemmy-js-client';
36 import { i18n } from '../i18next';
37 import { PictrsImage } from './pictrs-image';
39 interface NavbarProps {
40 site_res: GetSiteResponse;
43 interface NavbarState {
46 replies: CommentView[];
47 mentions: CommentView[];
48 messages: PrivateMessageView[];
51 toggleSearch: boolean;
52 onSiteBanner?(url: string): any;
55 export class Navbar extends Component<NavbarProps, NavbarState> {
56 private wsSub: Subscription;
57 private userSub: Subscription;
58 private unreadCountSub: Subscription;
59 private searchTextField: RefObject<HTMLInputElement>;
60 emptyState: NavbarState = {
61 isLoggedIn: !!this.props.site_res.my_user,
72 constructor(props: any, context: any) {
73 super(props, context);
74 this.state = this.emptyState;
76 this.parseMessage = this.parseMessage.bind(this);
77 this.subscription = wsSubscribe(this.parseMessage);
81 // Subscribe to jwt changes
83 this.searchTextField = createRef();
84 console.log(`isLoggedIn = ${this.state.isLoggedIn}`);
86 // On the first load, check the unreads
87 if (this.state.isLoggedIn == false) {
88 // setTheme(data.my_user.theme, true);
89 // i18n.changeLanguage(getLanguage());
90 // i18n.changeLanguage('de');
92 this.requestNotificationPermission();
93 WebSocketService.Instance.send(
101 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
103 if (res !== undefined) {
104 this.requestNotificationPermission();
105 WebSocketService.Instance.send(
106 wsClient.getSite({ auth: authField() })
109 this.setState({ isLoggedIn: false });
113 // Subscribe to unread count changes
114 this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(
116 this.setState({ unreadCount: res });
122 handleSearchParam(i: Navbar, event: any) {
123 i.state.searchParam = event.target.value;
128 const searchParam = this.state.searchParam;
129 this.setState({ searchParam: '' });
130 this.setState({ toggleSearch: false });
131 if (searchParam === '') {
132 this.context.router.history.push(`/search/`);
134 const searchParamEncoded = encodeURIComponent(searchParam);
135 this.context.router.history.push(
136 `/search/q/${searchParamEncoded}/type/All/sort/TopAll/page/1`
141 handleSearchSubmit(i: Navbar, event: any) {
142 event.preventDefault();
146 handleSearchBtn(i: Navbar, event: any) {
147 event.preventDefault();
148 i.setState({ toggleSearch: true });
150 i.searchTextField.current.focus();
151 const offsetWidth = i.searchTextField.current.offsetWidth;
152 if (i.state.searchParam && offsetWidth > 100) {
157 handleSearchBlur(i: Navbar, event: any) {
158 if (!(event.relatedTarget && event.relatedTarget.name !== 'search-btn')) {
159 i.state.toggleSearch = false;
165 return this.navbar();
168 componentWillUnmount() {
169 this.wsSub.unsubscribe();
170 this.userSub.unsubscribe();
171 this.unreadCountSub.unsubscribe();
174 // TODO class active corresponding to current page
176 let user = this.props.site_res.my_user || UserService.Instance.user;
178 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
179 <div class="container">
180 {this.props.site_res.site_view && (
182 title={this.props.site_res.version}
183 className="d-flex align-items-center navbar-brand mr-md-3"
186 {this.props.site_res.site_view.site.icon && showAvatars() && (
188 src={this.props.site_res.site_view.site.icon}
192 {this.props.site_res.site_view.site.name}
195 {this.state.isLoggedIn && (
197 className="ml-auto p-1 navbar-toggler nav-link border-0"
199 title={i18n.t('inbox')}
202 <use xlinkHref="#icon-bell"></use>
204 {this.state.unreadCount > 0 && (
205 <span class="mx-1 badge badge-light">
206 {this.state.unreadCount}
212 class="navbar-toggler border-0 p-1"
215 onClick={linkEvent(this, this.expandNavbar)}
216 data-tippy-content={i18n.t('expand_here')}
219 <use xlinkHref="#icon-menu"></use>
223 className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
225 <ul class="navbar-nav my-2 mr-auto">
226 <li class="nav-item">
230 title={i18n.t('communities')}
232 {i18n.t('communities')}
235 <li class="nav-item">
239 pathname: '/create_post',
240 state: { prevPath: this.currentLocation },
242 title={i18n.t('create_post')}
244 {i18n.t('create_post')}
247 <li class="nav-item">
250 to="/create_community"
251 title={i18n.t('create_community')}
253 {i18n.t('create_community')}
256 <li class="nav-item">
259 title={i18n.t('support_lemmy')}
260 href={supportLemmyUrl}
262 <svg class="icon small">
263 <use xlinkHref="#icon-beer"></use>
268 <ul class="navbar-nav my-2">
270 <li className="nav-item">
274 title={i18n.t('admin_settings')}
277 <use xlinkHref="#icon-settings"></use>
283 {!this.context.router.history.location.pathname.match(
288 onSubmit={linkEvent(this, this.handleSearchSubmit)}
291 class={`form-control mr-0 search-input ${
292 this.state.toggleSearch ? 'show-input' : 'hide-input'
294 onInput={linkEvent(this, this.handleSearchParam)}
295 value={this.state.searchParam}
296 ref={this.searchTextField}
298 placeholder={i18n.t('search')}
299 onBlur={linkEvent(this, this.handleSearchBlur)}
303 onClick={linkEvent(this, this.handleSearchBtn)}
304 class="px-1 btn btn-link"
305 style="color: var(--gray)"
308 <use xlinkHref="#icon-search"></use>
313 {this.state.isLoggedIn ? (
315 <ul class="navbar-nav my-2">
316 <li className="nav-item">
320 title={i18n.t('inbox')}
323 <use xlinkHref="#icon-bell"></use>
325 {this.state.unreadCount > 0 && (
326 <span class="ml-1 badge badge-light">
327 {this.state.unreadCount}
333 <ul class="navbar-nav">
334 <li className="nav-item">
337 to={`/u/${user.name}`}
338 title={i18n.t('settings')}
341 {user.avatar && showAvatars() && (
342 <PictrsImage src={user.avatar} icon />
344 {user.preferred_username
345 ? user.preferred_username
353 <ul class="navbar-nav my-2">
354 <li className="ml-2 nav-item">
356 className="btn btn-success"
358 title={i18n.t('login_sign_up')}
360 {i18n.t('login_sign_up')}
371 expandNavbar(i: Navbar) {
372 i.state.expanded = !i.state.expanded;
376 parseMessage(msg: any) {
377 let op = wsUserOp(msg);
379 if (msg.error == 'not_logged_in') {
380 UserService.Instance.logout();
384 } else if (msg.reconnect) {
385 WebSocketService.Instance.send(
391 } else if (op == UserOperation.GetReplies) {
392 let data = wsJsonToRes<GetRepliesResponse>(msg).data;
393 let unreadReplies = data.replies.filter(r => !r.comment.read);
395 this.state.replies = unreadReplies;
396 this.state.unreadCount = this.calculateUnreadCount();
397 this.setState(this.state);
398 this.sendUnreadCount();
399 } else if (op == UserOperation.GetUserMentions) {
400 let data = wsJsonToRes<GetUserMentionsResponse>(msg).data;
401 let unreadMentions = data.mentions.filter(r => !r.comment.read);
403 this.state.mentions = unreadMentions;
404 this.state.unreadCount = this.calculateUnreadCount();
405 this.setState(this.state);
406 this.sendUnreadCount();
407 } else if (op == UserOperation.GetPrivateMessages) {
408 let data = wsJsonToRes<PrivateMessagesResponse>(msg).data;
409 let unreadMessages = data.private_messages.filter(
410 r => !r.private_message.read
413 this.state.messages = unreadMessages;
414 this.state.unreadCount = this.calculateUnreadCount();
415 this.setState(this.state);
416 this.sendUnreadCount();
417 } else if (op == UserOperation.GetSite) {
418 // This is only called on a successful login
419 let data = wsJsonToRes<GetSiteResponse>(msg).data;
420 console.log(data.my_user);
421 UserService.Instance.user = data.my_user;
422 setTheme(UserService.Instance.user.theme);
423 i18n.changeLanguage(getLanguage());
424 this.state.isLoggedIn = true;
425 this.setState(this.state);
426 } else if (op == UserOperation.CreateComment) {
427 let data = wsJsonToRes<CommentResponse>(msg).data;
429 if (this.state.isLoggedIn) {
430 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
431 this.state.replies.push(data.comment_view);
432 this.state.unreadCount++;
433 this.setState(this.state);
434 this.sendUnreadCount();
435 notifyComment(data.comment_view, this.context.router);
438 } else if (op == UserOperation.CreatePrivateMessage) {
439 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
441 if (this.state.isLoggedIn) {
443 data.private_message_view.recipient.id == UserService.Instance.user.id
445 this.state.messages.push(data.private_message_view);
446 this.state.unreadCount++;
447 this.setState(this.state);
448 this.sendUnreadCount();
449 notifyPrivateMessage(data.private_message_view, this.context.router);
456 console.log('Fetching unreads...');
457 let repliesForm: GetReplies = {
465 let userMentionsForm: GetUserMentions = {
473 let privateMessagesForm: GetPrivateMessages = {
480 if (this.currentLocation !== '/inbox') {
481 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
482 WebSocketService.Instance.send(
483 wsClient.getUserMentions(userMentionsForm)
485 WebSocketService.Instance.send(
486 wsClient.getPrivateMessages(privateMessagesForm)
491 get currentLocation() {
492 return this.context.router.history.location.pathname;
496 UserService.Instance.unreadCountSub.next(this.state.unreadCount);
499 calculateUnreadCount(): number {
501 this.state.replies.filter(r => !r.comment.read).length +
502 this.state.mentions.filter(r => !r.comment.read).length +
503 this.state.messages.filter(r => !r.private_message.read).length
507 get canAdmin(): boolean {
509 UserService.Instance.user &&
510 this.props.site_res.admins
512 .includes(UserService.Instance.user.id)
516 requestNotificationPermission() {
517 if (UserService.Instance.user) {
518 document.addEventListener('DOMContentLoaded', function () {
520 toast(i18n.t('notifications_error'), 'danger');
524 if (Notification.permission !== 'granted')
525 Notification.requestPermission();