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';
34 import { i18n } from '../i18next';
35 import { PictrsImage } from './pictrs-image';
37 interface NavbarProps {
38 site_res: GetSiteResponse;
41 interface NavbarState {
44 replies: CommentView[];
45 mentions: CommentView[];
46 messages: PrivateMessageView[];
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_res.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.client.userJoin({
92 auth: UserService.Instance.authField(),
97 this.userSub = UserService.Instance.jwtSub.subscribe(res => {
99 if (res !== undefined) {
100 this.requestNotificationPermission();
101 WebSocketService.Instance.client.getSite();
103 this.setState({ isLoggedIn: false });
107 // Subscribe to unread count changes
108 this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(
110 this.setState({ unreadCount: res });
116 handleSearchParam(i: Navbar, event: any) {
117 i.state.searchParam = event.target.value;
122 const searchParam = this.state.searchParam;
123 this.setState({ searchParam: '' });
124 this.setState({ toggleSearch: false });
125 if (searchParam === '') {
126 this.context.router.history.push(`/search/`);
128 const searchParamEncoded = encodeURIComponent(searchParam);
129 this.context.router.history.push(
130 `/search/q/${searchParamEncoded}/type/All/sort/TopAll/page/1`
135 handleSearchSubmit(i: Navbar, event: any) {
136 event.preventDefault();
140 handleSearchBtn(i: Navbar, event: any) {
141 event.preventDefault();
142 i.setState({ toggleSearch: true });
144 i.searchTextField.current.focus();
145 const offsetWidth = i.searchTextField.current.offsetWidth;
146 if (i.state.searchParam && offsetWidth > 100) {
151 handleSearchBlur(i: Navbar, event: any) {
152 if (!(event.relatedTarget && event.relatedTarget.name !== 'search-btn')) {
153 i.state.toggleSearch = false;
159 return this.navbar();
162 componentWillUnmount() {
163 this.wsSub.unsubscribe();
164 this.userSub.unsubscribe();
165 this.unreadCountSub.unsubscribe();
168 // TODO class active corresponding to current page
170 let user = this.props.site_res.my_user;
172 <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
173 <div class="container">
174 {this.props.site_res.site_view && (
176 title={this.props.site_res.version}
177 className="d-flex align-items-center navbar-brand mr-md-3"
180 {this.props.site_res.site_view.site.icon && showAvatars() && (
182 src={this.props.site_res.site_view.site.icon}
186 {this.props.site_res.site_view.site.name}
189 {this.state.isLoggedIn && (
191 className="ml-auto p-1 navbar-toggler nav-link border-0"
193 title={i18n.t('inbox')}
196 <use xlinkHref="#icon-bell"></use>
198 {this.state.unreadCount > 0 && (
199 <span class="mx-1 badge badge-light">
200 {this.state.unreadCount}
206 class="navbar-toggler border-0 p-1"
209 onClick={linkEvent(this, this.expandNavbar)}
210 data-tippy-content={i18n.t('expand_here')}
213 <use xlinkHref="#icon-menu"></use>
217 className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
219 <ul class="navbar-nav my-2 mr-auto">
220 <li class="nav-item">
224 title={i18n.t('communities')}
226 {i18n.t('communities')}
229 <li class="nav-item">
233 pathname: '/create_post',
234 state: { prevPath: this.currentLocation },
236 title={i18n.t('create_post')}
238 {i18n.t('create_post')}
241 <li class="nav-item">
244 to="/create_community"
245 title={i18n.t('create_community')}
247 {i18n.t('create_community')}
250 <li class="nav-item">
253 title={i18n.t('support_lemmy')}
254 href={supportLemmyUrl}
256 <svg class="icon small">
257 <use xlinkHref="#icon-beer"></use>
262 <ul class="navbar-nav my-2">
264 <li className="nav-item">
268 title={i18n.t('admin_settings')}
271 <use xlinkHref="#icon-settings"></use>
277 {!this.context.router.history.location.pathname.match(
282 onSubmit={linkEvent(this, this.handleSearchSubmit)}
285 class={`form-control mr-0 search-input ${
286 this.state.toggleSearch ? 'show-input' : 'hide-input'
288 onInput={linkEvent(this, this.handleSearchParam)}
289 value={this.state.searchParam}
290 ref={this.searchTextField}
292 placeholder={i18n.t('search')}
293 onBlur={linkEvent(this, this.handleSearchBlur)}
297 onClick={linkEvent(this, this.handleSearchBtn)}
298 class="px-1 btn btn-link"
299 style="color: var(--gray)"
302 <use xlinkHref="#icon-search"></use>
307 {this.state.isLoggedIn ? (
309 <ul class="navbar-nav my-2">
310 <li className="nav-item">
314 title={i18n.t('inbox')}
317 <use xlinkHref="#icon-bell"></use>
319 {this.state.unreadCount > 0 && (
320 <span class="ml-1 badge badge-light">
321 {this.state.unreadCount}
327 <ul class="navbar-nav">
328 <li className="nav-item">
331 to={`/u/${user.name}`}
332 title={i18n.t('settings')}
335 {user.avatar && showAvatars() && (
336 <PictrsImage src={user.avatar} icon />
338 {user.preferred_username
339 ? user.preferred_username
347 <ul class="navbar-nav my-2">
348 <li className="ml-2 nav-item">
350 className="btn btn-success"
352 title={i18n.t('login_sign_up')}
354 {i18n.t('login_sign_up')}
365 expandNavbar(i: Navbar) {
366 i.state.expanded = !i.state.expanded;
370 parseMessage(msg: any) {
371 let op = wsUserOp(msg);
373 if (msg.error == 'not_logged_in') {
374 UserService.Instance.logout();
378 } else if (msg.reconnect) {
379 WebSocketService.Instance.client.userJoin({
380 auth: UserService.Instance.authField(),
383 } else if (op == UserOperation.GetReplies) {
384 let data = wsJsonToRes<GetRepliesResponse>(msg).data;
385 let unreadReplies = data.replies.filter(r => !r.comment.read);
387 this.state.replies = unreadReplies;
388 this.state.unreadCount = this.calculateUnreadCount();
389 this.setState(this.state);
390 this.sendUnreadCount();
391 } else if (op == UserOperation.GetUserMentions) {
392 let data = wsJsonToRes<GetUserMentionsResponse>(msg).data;
393 let unreadMentions = data.mentions.filter(r => !r.comment.read);
395 this.state.mentions = unreadMentions;
396 this.state.unreadCount = this.calculateUnreadCount();
397 this.setState(this.state);
398 this.sendUnreadCount();
399 } else if (op == UserOperation.GetPrivateMessages) {
400 let data = wsJsonToRes<PrivateMessagesResponse>(msg).data;
401 let unreadMessages = data.private_messages.filter(
402 r => !r.private_message.read
405 this.state.messages = unreadMessages;
406 this.state.unreadCount = this.calculateUnreadCount();
407 this.setState(this.state);
408 this.sendUnreadCount();
409 } else if (op == UserOperation.GetSite) {
410 // This is only called on a successful login
411 let data = wsJsonToRes<GetSiteResponse>(msg).data;
412 UserService.Instance.user = data.my_user;
413 setTheme(UserService.Instance.user.theme);
414 i18n.changeLanguage(getLanguage());
415 this.state.isLoggedIn = true;
416 this.setState(this.state);
417 } else if (op == UserOperation.CreateComment) {
418 let data = wsJsonToRes<CommentResponse>(msg).data;
420 if (this.state.isLoggedIn) {
421 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
422 this.state.replies.push(data.comment_view);
423 this.state.unreadCount++;
424 this.setState(this.state);
425 this.sendUnreadCount();
426 notifyComment(data.comment_view, this.context.router);
429 } else if (op == UserOperation.CreatePrivateMessage) {
430 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
432 if (this.state.isLoggedIn) {
434 data.private_message_view.recipient.id == UserService.Instance.user.id
436 this.state.messages.push(data.private_message_view);
437 this.state.unreadCount++;
438 this.setState(this.state);
439 this.sendUnreadCount();
440 notifyPrivateMessage(data.private_message_view, this.context.router);
447 console.log('Fetching unreads...');
448 let repliesForm: GetReplies = {
453 auth: UserService.Instance.authField(),
456 let userMentionsForm: GetUserMentions = {
461 auth: UserService.Instance.authField(),
464 let privateMessagesForm: GetPrivateMessages = {
468 auth: UserService.Instance.authField(),
471 if (this.currentLocation !== '/inbox') {
472 WebSocketService.Instance.client.getReplies(repliesForm);
473 WebSocketService.Instance.client.getUserMentions(userMentionsForm);
474 WebSocketService.Instance.client.getPrivateMessages(privateMessagesForm);
478 get currentLocation() {
479 return this.context.router.history.location.pathname;
483 UserService.Instance.unreadCountSub.next(this.state.unreadCount);
486 calculateUnreadCount(): number {
488 this.state.replies.filter(r => !r.comment.read).length +
489 this.state.mentions.filter(r => !r.comment.read).length +
490 this.state.messages.filter(r => !r.private_message.read).length
494 get canAdmin(): boolean {
496 UserService.Instance.user &&
497 this.props.site_res.admins
499 .includes(UserService.Instance.user.id)
503 requestNotificationPermission() {
504 if (UserService.Instance.user) {
505 document.addEventListener('DOMContentLoaded', function () {
507 toast(i18n.t('notifications_error'), 'danger');
511 if (Notification.permission !== 'granted')
512 Notification.requestPermission();