1 import { Component, linkEvent } from 'inferno';
2 import { Link, withRouter } 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,
21 PrivateMessageResponse,
22 WebSocketJsonResponse,
24 } from '../interfaces';
27 pictrsAvatarThumbnail,
35 import { version } from '../version';
36 import { i18n } from '../i18next';
38 interface NavbarState {
41 replies: Array<Comment>;
42 mentions: Array<Comment>;
43 messages: Array<PrivateMessage>;
46 admins: Array<UserView>;
50 class Navbar extends Component<any, NavbarState> {
51 private wsSub: Subscription;
52 private userSub: Subscription;
53 emptyState: NavbarState = {
54 isLoggedIn: UserService.Instance.user !== undefined,
65 constructor(props: any, context: any) {
66 super(props, context);
67 this.state = this.emptyState;
69 // Subscribe to user changes
70 this.userSub = UserService.Instance.sub.subscribe(user => {
71 this.state.isLoggedIn = user.user !== undefined;
72 if (this.state.isLoggedIn) {
73 this.state.unreadCount = user.user.unreadCount;
74 this.requestNotificationPermission();
76 this.setState(this.state);
79 this.wsSub = WebSocketService.Instance.subject
80 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
82 msg => this.parseMessage(msg),
83 err => console.error(err),
84 () => console.log('complete')
87 if (this.state.isLoggedIn) {
88 this.requestNotificationPermission();
89 // TODO couldn't get re-logging in to re-fetch unreads
93 WebSocketService.Instance.getSite();
95 this.handleSearchParam = this.handleSearchParam.bind(this);
98 handleSearchParam(i: Navbar, event: any) {
99 i.state.searchParam = event.target.value;
104 this.props.history.push(
105 `/search/q/${this.state.searchParam}/type/all/sort/topall/page/1`
107 this.setState({ searchParam: '' });
110 handleSearchSubmit(i: Navbar, event: any) {
111 event.preventDefault();
116 return this.navbar();
119 componentWillUnmount() {
120 this.wsSub.unsubscribe();
121 this.userSub.unsubscribe();
124 // TODO class active corresponding to current page
127 <nav class="container-fluid navbar navbar-expand-md navbar-light shadow p-0 px-3">
128 <Link title={version} class="navbar-brand" to="/">
129 {this.state.siteName}
131 {this.state.isLoggedIn && (
133 class="ml-auto p-0 navbar-toggler nav-link"
135 title={i18n.t('inbox')}
138 <use xlinkHref="#icon-bell"></use>
140 {this.state.unreadCount > 0 && (
141 <span class="ml-1 badge badge-light">
142 {this.state.unreadCount}
148 class="navbar-toggler"
151 onClick={linkEvent(this, this.expandNavbar)}
152 data-tippy-content={i18n.t('expand_here')}
154 <span class="navbar-toggler-icon"></span>
157 className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
159 <ul class="navbar-nav mr-auto">
160 <li class="nav-item">
164 title={i18n.t('communities')}
166 {i18n.t('communities')}
169 <li class="nav-item">
173 pathname: '/create_post',
174 state: { prevPath: this.currentLocation },
176 title={i18n.t('create_post')}
178 {i18n.t('create_post')}
181 <li class="nav-item">
184 to="/create_community"
185 title={i18n.t('create_community')}
187 {i18n.t('create_community')}
190 <li className="nav-item">
194 title={i18n.t('donate_to_lemmy')}
197 <use xlinkHref="#icon-coffee"></use>
202 {!this.props.history.location.pathname.match(/^\/search/) && (
203 <div class="nav-item search-bar">
204 <form onSubmit={linkEvent(this, this.handleSearchSubmit)}>
206 class="form-control mr-sm-2"
207 onInput={linkEvent(this, this.handleSearchParam)}
208 value={this.state.searchParam}
210 placeholder={i18n.t('search')}
215 <ul class="navbar-nav ml-2">
217 <li className="nav-item mt-1">
221 title={i18n.t('admin_settings')}
224 <use xlinkHref="#icon-settings"></use>
229 {this.state.isLoggedIn ? (
231 <li className="nav-item mt-1">
232 <Link class="nav-link" to="/inbox" title={i18n.t('inbox')}>
234 <use xlinkHref="#icon-bell"></use>
236 {this.state.unreadCount > 0 && (
237 <span class="ml-1 badge badge-light">
238 {this.state.unreadCount}
243 <li className="nav-item">
246 to={`/u/${UserService.Instance.user.username}`}
247 title={i18n.t('settings')}
250 {UserService.Instance.user.avatar && showAvatars() && (
252 src={pictrsAvatarThumbnail(
253 UserService.Instance.user.avatar
257 class="rounded-circle mr-2"
260 {UserService.Instance.user.username}
269 title={i18n.t('login_sign_up')}
271 {i18n.t('login_sign_up')}
280 expandNavbar(i: Navbar) {
281 i.state.expanded = !i.state.expanded;
285 parseMessage(msg: WebSocketJsonResponse) {
286 let res = wsJsonToRes(msg);
288 if (msg.error == 'not_logged_in') {
289 UserService.Instance.logout();
293 } else if (msg.reconnect) {
295 } else if (res.op == UserOperation.GetReplies) {
296 let data = res.data as GetRepliesResponse;
297 let unreadReplies = data.replies.filter(r => !r.read);
299 this.state.replies = unreadReplies;
300 this.state.unreadCount = this.calculateUnreadCount();
301 this.setState(this.state);
302 this.sendUnreadCount();
303 } else if (res.op == UserOperation.GetUserMentions) {
304 let data = res.data as GetUserMentionsResponse;
305 let unreadMentions = data.mentions.filter(r => !r.read);
307 this.state.mentions = unreadMentions;
308 this.state.unreadCount = this.calculateUnreadCount();
309 this.setState(this.state);
310 this.sendUnreadCount();
311 } else if (res.op == UserOperation.GetPrivateMessages) {
312 let data = res.data as PrivateMessagesResponse;
313 let unreadMessages = data.messages.filter(r => !r.read);
315 this.state.messages = unreadMessages;
316 this.state.unreadCount = this.calculateUnreadCount();
317 this.setState(this.state);
318 this.sendUnreadCount();
319 } else if (res.op == UserOperation.CreateComment) {
320 let data = res.data as CommentResponse;
322 if (this.state.isLoggedIn) {
323 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
324 this.state.replies.push(data.comment);
325 this.state.unreadCount++;
326 this.setState(this.state);
327 this.sendUnreadCount();
328 this.notify(data.comment);
331 } else if (res.op == UserOperation.CreatePrivateMessage) {
332 let data = res.data as PrivateMessageResponse;
334 if (this.state.isLoggedIn) {
335 if (data.message.recipient_id == UserService.Instance.user.id) {
336 this.state.messages.push(data.message);
337 this.state.unreadCount++;
338 this.setState(this.state);
339 this.sendUnreadCount();
340 this.notify(data.message);
343 } else if (res.op == UserOperation.GetSite) {
344 let data = res.data as GetSiteResponse;
346 if (data.site && !this.state.siteName) {
347 this.state.siteName = data.site.name;
348 this.state.admins = data.admins;
349 WebSocketService.Instance.site = data.site;
350 WebSocketService.Instance.admins = data.admins;
352 this.setState(this.state);
358 if (this.state.isLoggedIn) {
359 let repliesForm: GetRepliesForm = {
360 sort: SortType[SortType.New],
366 let userMentionsForm: GetUserMentionsForm = {
367 sort: SortType[SortType.New],
373 let privateMessagesForm: GetPrivateMessagesForm = {
379 if (this.currentLocation !== '/inbox') {
380 WebSocketService.Instance.getReplies(repliesForm);
381 WebSocketService.Instance.getUserMentions(userMentionsForm);
382 WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
387 get currentLocation() {
388 return this.context.router.history.location.pathname;
392 UserService.Instance.user.unreadCount = this.state.unreadCount;
393 UserService.Instance.sub.next({
394 user: UserService.Instance.user,
398 calculateUnreadCount(): number {
400 this.state.replies.filter(r => !r.read).length +
401 this.state.mentions.filter(r => !r.read).length +
402 this.state.messages.filter(r => !r.read).length
406 get canAdmin(): boolean {
408 UserService.Instance.user &&
409 this.state.admins.map(a => a.id).includes(UserService.Instance.user.id)
413 requestNotificationPermission() {
414 if (UserService.Instance.user) {
415 document.addEventListener('DOMContentLoaded', function () {
417 toast(i18n.t('notifications_error'), 'danger');
421 if (Notification.permission !== 'granted')
422 Notification.requestPermission();
427 notify(reply: Comment | PrivateMessage) {
428 let creator_name = reply.creator_name;
429 let creator_avatar = reply.creator_avatar
430 ? reply.creator_avatar
431 : `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`;
432 let link = isCommentType(reply)
433 ? `/post/${reply.post_id}/comment/${reply.id}`
435 let htmlBody = md.render(reply.content);
436 let body = reply.content; // Unfortunately the notifications API can't do html
446 if (Notification.permission !== 'granted') Notification.requestPermission();
448 var notification = new Notification(reply.creator_name, {
449 icon: creator_avatar,
453 notification.onclick = () => {
454 event.preventDefault();
455 this.context.router.history.push(link);
461 export default withRouter(Navbar);