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';
34 import { i18n } from '../i18next';
35 import { PictrsImage } from './pictrs-image';
37 interface NavbarProps {
38 site: GetSiteResponse;
41 interface NavbarState {
46 messages: PrivateMessage[];
49 toggleSearch: boolean;
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,
70 constructor(props: any, context: any) {
71 super(props, context);
72 this.state = this.emptyState;
74 this.parseMessage = this.parseMessage.bind(this);
75 this.subscription = wsSubscribe(this.parseMessage);
79 // Subscribe to jwt changes
81 this.searchTextField = createRef();
82 console.log(`isLoggedIn = ${this.state.isLoggedIn}`);
84 // On the first load, check the unreads
85 if (this.state.isLoggedIn == false) {
86 // setTheme(data.my_user.theme, true);
87 // i18n.changeLanguage(getLanguage());
88 // i18n.changeLanguage('de');
90 this.requestNotificationPermission();
91 WebSocketService.Instance.userJoin();
95 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
97 if (res !== undefined) {
98 this.requestNotificationPermission();
99 WebSocketService.Instance.getSite();
101 this.setState({ isLoggedIn: false });
105 // Subscribe to unread count changes
106 this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(
108 this.setState({ unreadCount: res });
114 handleSearchParam(i: Navbar, event: any) {
115 i.state.searchParam = event.target.value;
120 const searchParam = this.state.searchParam;
121 this.setState({ searchParam: '' });
122 this.setState({ toggleSearch: false });
123 if (searchParam === '') {
124 this.context.router.history.push(`/search/`);
126 const searchParamEncoded = encodeURIComponent(searchParam);
127 this.context.router.history.push(
128 `/search/q/${searchParamEncoded}/type/All/sort/TopAll/page/1`
133 handleSearchSubmit(i: Navbar, event: any) {
134 event.preventDefault();
138 handleSearchBtn(i: Navbar, event: any) {
139 event.preventDefault();
140 i.setState({ toggleSearch: true });
142 i.searchTextField.current.focus();
143 const offsetWidth = i.searchTextField.current.offsetWidth;
144 if (i.state.searchParam && offsetWidth > 100) {
149 handleSearchBlur(i: Navbar, event: any) {
150 if (!(event.relatedTarget && event.relatedTarget.name !== 'search-btn')) {
151 i.state.toggleSearch = false;
157 return this.navbar();
160 componentWillUnmount() {
161 this.wsSub.unsubscribe();
162 this.userSub.unsubscribe();
163 this.unreadCountSub.unsubscribe();
166 // TODO class active corresponding to current page
168 let user = this.props.site.my_user;
170 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
171 <div class="container">
172 {this.props.site.site && (
174 title={this.props.site.version}
175 className="d-flex align-items-center navbar-brand mr-md-3"
178 {this.props.site.site.icon && showAvatars() && (
179 <PictrsImage src={this.props.site.site.icon} icon />
181 {this.props.site.site.name}
184 {this.state.isLoggedIn && (
186 className="ml-auto p-1 navbar-toggler nav-link border-0"
188 title={i18n.t('inbox')}
191 <use xlinkHref="#icon-bell"></use>
193 {this.state.unreadCount > 0 && (
194 <span class="mx-1 badge badge-light">
195 {this.state.unreadCount}
201 class="navbar-toggler border-0 p-1"
204 onClick={linkEvent(this, this.expandNavbar)}
205 data-tippy-content={i18n.t('expand_here')}
208 <use xlinkHref="#icon-menu"></use>
212 className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
214 <ul class="navbar-nav my-2 mr-auto">
215 <li class="nav-item">
219 title={i18n.t('communities')}
221 {i18n.t('communities')}
224 <li class="nav-item">
228 pathname: '/create_post',
229 state: { prevPath: this.currentLocation },
231 title={i18n.t('create_post')}
233 {i18n.t('create_post')}
236 <li class="nav-item">
239 to="/create_community"
240 title={i18n.t('create_community')}
242 {i18n.t('create_community')}
245 <li class="nav-item">
248 title={i18n.t('support_lemmy')}
249 href={supportLemmyUrl}
251 <svg class="icon small">
252 <use xlinkHref="#icon-beer"></use>
257 <ul class="navbar-nav my-2">
259 <li className="nav-item">
263 title={i18n.t('admin_settings')}
266 <use xlinkHref="#icon-settings"></use>
272 {!this.context.router.history.location.pathname.match(
277 onSubmit={linkEvent(this, this.handleSearchSubmit)}
280 class={`form-control mr-0 search-input ${
281 this.state.toggleSearch ? 'show-input' : 'hide-input'
283 onInput={linkEvent(this, this.handleSearchParam)}
284 value={this.state.searchParam}
285 ref={this.searchTextField}
287 placeholder={i18n.t('search')}
288 onBlur={linkEvent(this, this.handleSearchBlur)}
292 onClick={linkEvent(this, this.handleSearchBtn)}
293 class="px-1 btn btn-link"
294 style="color: var(--gray)"
297 <use xlinkHref="#icon-search"></use>
302 {this.state.isLoggedIn ? (
304 <ul class="navbar-nav my-2">
305 <li className="nav-item">
309 title={i18n.t('inbox')}
312 <use xlinkHref="#icon-bell"></use>
314 {this.state.unreadCount > 0 && (
315 <span class="ml-1 badge badge-light">
316 {this.state.unreadCount}
322 <ul class="navbar-nav">
323 <li className="nav-item">
326 to={`/u/${user.name}`}
327 title={i18n.t('settings')}
330 {user.avatar && showAvatars() && (
331 <PictrsImage src={user.avatar} icon />
333 {user.preferred_username
334 ? user.preferred_username
342 <ul class="navbar-nav my-2">
343 <li className="ml-2 nav-item">
345 className="btn btn-success"
347 title={i18n.t('login_sign_up')}
349 {i18n.t('login_sign_up')}
360 expandNavbar(i: Navbar) {
361 i.state.expanded = !i.state.expanded;
365 parseMessage(msg: WebSocketJsonResponse) {
366 let res = wsJsonToRes(msg);
368 if (msg.error == 'not_logged_in') {
369 UserService.Instance.logout();
373 } else if (msg.reconnect) {
374 WebSocketService.Instance.userJoin();
376 } else if (res.op == UserOperation.GetReplies) {
377 let data = res.data as GetRepliesResponse;
378 let unreadReplies = data.replies.filter(r => !r.read);
380 this.state.replies = unreadReplies;
381 this.state.unreadCount = this.calculateUnreadCount();
382 this.setState(this.state);
383 this.sendUnreadCount();
384 } else if (res.op == UserOperation.GetUserMentions) {
385 let data = res.data as GetUserMentionsResponse;
386 let unreadMentions = data.mentions.filter(r => !r.read);
388 this.state.mentions = unreadMentions;
389 this.state.unreadCount = this.calculateUnreadCount();
390 this.setState(this.state);
391 this.sendUnreadCount();
392 } else if (res.op == UserOperation.GetPrivateMessages) {
393 let data = res.data as PrivateMessagesResponse;
394 let unreadMessages = data.messages.filter(r => !r.read);
396 this.state.messages = unreadMessages;
397 this.state.unreadCount = this.calculateUnreadCount();
398 this.setState(this.state);
399 this.sendUnreadCount();
400 } else if (res.op == UserOperation.GetSite) {
401 // This is only called on a successful login
402 let data = res.data as GetSiteResponse;
403 UserService.Instance.user = data.my_user;
404 setTheme(UserService.Instance.user.theme);
405 i18n.changeLanguage(getLanguage());
406 this.state.isLoggedIn = true;
407 this.setState(this.state);
408 } else if (res.op == UserOperation.CreateComment) {
409 let data = res.data as CommentResponse;
411 if (this.state.isLoggedIn) {
412 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
413 this.state.replies.push(data.comment);
414 this.state.unreadCount++;
415 this.setState(this.state);
416 this.sendUnreadCount();
417 notifyComment(data.comment, this.context.router);
420 } else if (res.op == UserOperation.CreatePrivateMessage) {
421 let data = res.data as PrivateMessageResponse;
423 if (this.state.isLoggedIn) {
424 if (data.message.recipient_id == UserService.Instance.user.id) {
425 this.state.messages.push(data.message);
426 this.state.unreadCount++;
427 this.setState(this.state);
428 this.sendUnreadCount();
429 notifyPrivateMessage(data.message, this.context.router);
436 console.log('Fetching unreads...');
437 let repliesForm: GetRepliesForm = {
444 let userMentionsForm: GetUserMentionsForm = {
451 let privateMessagesForm: GetPrivateMessagesForm = {
457 if (this.currentLocation !== '/inbox') {
458 WebSocketService.Instance.getReplies(repliesForm);
459 WebSocketService.Instance.getUserMentions(userMentionsForm);
460 WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
464 get currentLocation() {
465 return this.context.router.history.location.pathname;
469 UserService.Instance.unreadCountSub.next(this.state.unreadCount);
472 calculateUnreadCount(): number {
474 this.state.replies.filter(r => !r.read).length +
475 this.state.mentions.filter(r => !r.read).length +
476 this.state.messages.filter(r => !r.read).length
480 get canAdmin(): boolean {
482 UserService.Instance.user &&
483 this.props.site.admins
485 .includes(UserService.Instance.user.id)
489 requestNotificationPermission() {
490 if (UserService.Instance.user) {
491 document.addEventListener('DOMContentLoaded', function () {
493 toast(i18n.t('notifications_error'), 'danger');
497 if (Notification.permission !== 'granted')
498 Notification.requestPermission();