1 import { Component, linkEvent } from 'inferno';
2 import { Link } 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,
19 PrivateMessageResponse,
20 WebSocketJsonResponse,
21 } from '../interfaces';
24 pictshareAvatarThumbnail,
32 import { version } from '../version';
33 import { i18n } from '../i18next';
35 interface NavbarState {
38 replies: Array<Comment>;
39 mentions: Array<Comment>;
40 messages: Array<PrivateMessage>;
45 export class Navbar extends Component<any, NavbarState> {
46 private wsSub: Subscription;
47 private userSub: Subscription;
48 emptyState: NavbarState = {
49 isLoggedIn: UserService.Instance.user !== undefined,
58 constructor(props: any, context: any) {
59 super(props, context);
60 this.state = this.emptyState;
62 // Subscribe to user changes
63 this.userSub = UserService.Instance.sub.subscribe(user => {
64 this.state.isLoggedIn = user.user !== undefined;
65 if (this.state.isLoggedIn) {
66 this.state.unreadCount = user.user.unreadCount;
67 this.requestNotificationPermission();
69 this.setState(this.state);
72 this.wsSub = WebSocketService.Instance.subject
73 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
75 msg => this.parseMessage(msg),
76 err => console.error(err),
77 () => console.log('complete')
80 if (this.state.isLoggedIn) {
81 this.requestNotificationPermission();
82 // TODO couldn't get re-logging in to re-fetch unreads
86 WebSocketService.Instance.getSite();
93 componentWillUnmount() {
94 this.wsSub.unsubscribe();
95 this.userSub.unsubscribe();
98 // TODO class active corresponding to current page
101 <nav class="container-fluid navbar navbar-expand-md navbar-light shadow p-0 px-3">
102 <Link title={version} class="navbar-brand" to="/">
103 {this.state.siteName}
105 {this.state.isLoggedIn && (
107 class="ml-auto p-0 navbar-toggler nav-link"
109 title={i18n.t('inbox')}
112 <use xlinkHref="#icon-bell"></use>
114 {this.state.unreadCount > 0 && (
115 <span class="ml-1 badge badge-light">
116 {this.state.unreadCount}
122 class="navbar-toggler"
125 onClick={linkEvent(this, this.expandNavbar)}
126 data-tippy-content={i18n.t('expand_here')}
128 <span class="navbar-toggler-icon"></span>
131 className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
133 <ul class="navbar-nav mr-auto">
134 <li class="nav-item">
138 title={i18n.t('communities')}
140 {i18n.t('communities')}
143 <li class="nav-item">
144 <Link class="nav-link" to="/search" title={i18n.t('search')}>
148 <li class="nav-item">
152 pathname: '/create_post',
153 state: { prevPath: this.currentLocation },
155 title={i18n.t('create_post')}
157 {i18n.t('create_post')}
160 <li class="nav-item">
163 to="/create_community"
164 title={i18n.t('create_community')}
166 {i18n.t('create_community')}
169 <li className="nav-item">
173 title={i18n.t('donate_to_lemmy')}
176 <use xlinkHref="#icon-coffee"></use>
181 <ul class="navbar-nav ml-auto">
182 {this.state.isLoggedIn ? (
184 <li className="nav-item mt-1">
185 <Link class="nav-link" to="/inbox" title={i18n.t('inbox')}>
187 <use xlinkHref="#icon-bell"></use>
189 {this.state.unreadCount > 0 && (
190 <span class="ml-1 badge badge-light">
191 {this.state.unreadCount}
196 <li className="nav-item">
199 to={`/u/${UserService.Instance.user.username}`}
200 title={i18n.t('settings')}
203 {UserService.Instance.user.avatar && showAvatars() && (
205 src={pictshareAvatarThumbnail(
206 UserService.Instance.user.avatar
210 class="rounded-circle mr-2"
213 {UserService.Instance.user.username}
222 title={i18n.t('login_sign_up')}
224 {i18n.t('login_sign_up')}
233 expandNavbar(i: Navbar) {
234 i.state.expanded = !i.state.expanded;
238 parseMessage(msg: WebSocketJsonResponse) {
239 let res = wsJsonToRes(msg);
241 if (msg.error == 'not_logged_in') {
242 UserService.Instance.logout();
246 } else if (msg.reconnect) {
248 } else if (res.op == UserOperation.GetReplies) {
249 let data = res.data as GetRepliesResponse;
250 let unreadReplies = data.replies.filter(r => !r.read);
252 this.state.replies = unreadReplies;
253 this.state.unreadCount = this.calculateUnreadCount();
254 this.setState(this.state);
255 this.sendUnreadCount();
256 } else if (res.op == UserOperation.GetUserMentions) {
257 let data = res.data as GetUserMentionsResponse;
258 let unreadMentions = data.mentions.filter(r => !r.read);
260 this.state.mentions = unreadMentions;
261 this.state.unreadCount = this.calculateUnreadCount();
262 this.setState(this.state);
263 this.sendUnreadCount();
264 } else if (res.op == UserOperation.GetPrivateMessages) {
265 let data = res.data as PrivateMessagesResponse;
266 let unreadMessages = data.messages.filter(r => !r.read);
268 this.state.messages = unreadMessages;
269 this.state.unreadCount = this.calculateUnreadCount();
270 this.setState(this.state);
271 this.sendUnreadCount();
272 } else if (res.op == UserOperation.CreateComment) {
273 let data = res.data as CommentResponse;
275 if (this.state.isLoggedIn) {
276 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
277 this.state.replies.push(data.comment);
278 this.state.unreadCount++;
279 this.setState(this.state);
280 this.sendUnreadCount();
281 this.notify(data.comment);
284 } else if (res.op == UserOperation.CreatePrivateMessage) {
285 let data = res.data as PrivateMessageResponse;
287 if (this.state.isLoggedIn) {
288 if (data.message.recipient_id == UserService.Instance.user.id) {
289 this.state.messages.push(data.message);
290 this.state.unreadCount++;
291 this.setState(this.state);
292 this.sendUnreadCount();
293 this.notify(data.message);
296 } else if (res.op == UserOperation.GetSite) {
297 let data = res.data as GetSiteResponse;
299 if (data.site && !this.state.siteName) {
300 this.state.siteName = data.site.name;
301 WebSocketService.Instance.site = data.site;
302 this.setState(this.state);
308 if (this.state.isLoggedIn) {
309 let repliesForm: GetRepliesForm = {
310 sort: SortType[SortType.New],
316 let userMentionsForm: GetUserMentionsForm = {
317 sort: SortType[SortType.New],
323 let privateMessagesForm: GetPrivateMessagesForm = {
329 if (this.currentLocation !== '/inbox') {
330 WebSocketService.Instance.getReplies(repliesForm);
331 WebSocketService.Instance.getUserMentions(userMentionsForm);
332 WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
337 get currentLocation() {
338 return this.context.router.history.location.pathname;
342 UserService.Instance.user.unreadCount = this.state.unreadCount;
343 UserService.Instance.sub.next({
344 user: UserService.Instance.user,
348 calculateUnreadCount(): number {
350 this.state.replies.filter(r => !r.read).length +
351 this.state.mentions.filter(r => !r.read).length +
352 this.state.messages.filter(r => !r.read).length
356 requestNotificationPermission() {
357 if (UserService.Instance.user) {
358 document.addEventListener('DOMContentLoaded', function() {
360 toast(i18n.t('notifications_error'), 'danger');
364 if (Notification.permission !== 'granted')
365 Notification.requestPermission();
370 notify(reply: Comment | PrivateMessage) {
371 let creator_name = reply.creator_name;
372 let creator_avatar = reply.creator_avatar
373 ? reply.creator_avatar
374 : `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`;
375 let link = isCommentType(reply)
376 ? `/post/${reply.post_id}/comment/${reply.id}`
378 let htmlBody = md.render(reply.content);
379 let body = reply.content; // Unfortunately the notifications API can't do html
389 if (Notification.permission !== 'granted') Notification.requestPermission();
391 var notification = new Notification(reply.creator_name, {
392 icon: creator_avatar,
396 notification.onclick = () => {
397 event.preventDefault();
398 this.context.router.history.push(link);