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,
20 PrivateMessageResponse,
21 WebSocketJsonResponse,
22 } from '../interfaces';
25 pictshareAvatarThumbnail,
33 import { version } from '../version';
34 import { i18n } from '../i18next';
36 interface NavbarState {
39 replies: Array<Comment>;
40 mentions: Array<Comment>;
41 messages: Array<PrivateMessage>;
44 admins: Array<UserView>;
47 export class Navbar extends Component<any, NavbarState> {
48 private wsSub: Subscription;
49 private userSub: Subscription;
50 emptyState: NavbarState = {
51 isLoggedIn: UserService.Instance.user !== undefined,
61 constructor(props: any, context: any) {
62 super(props, context);
63 this.state = this.emptyState;
65 // Subscribe to user changes
66 this.userSub = UserService.Instance.sub.subscribe(user => {
67 this.state.isLoggedIn = user.user !== undefined;
68 if (this.state.isLoggedIn) {
69 this.state.unreadCount = user.user.unreadCount;
70 this.requestNotificationPermission();
72 this.setState(this.state);
75 this.wsSub = WebSocketService.Instance.subject
76 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
78 msg => this.parseMessage(msg),
79 err => console.error(err),
80 () => console.log('complete')
83 if (this.state.isLoggedIn) {
84 this.requestNotificationPermission();
85 // TODO couldn't get re-logging in to re-fetch unreads
89 WebSocketService.Instance.getSite();
96 componentWillUnmount() {
97 this.wsSub.unsubscribe();
98 this.userSub.unsubscribe();
101 // TODO class active corresponding to current page
104 <nav class="container-fluid navbar navbar-expand-md navbar-light shadow p-0 px-3">
105 <Link title={version} class="navbar-brand" to="/">
106 {this.state.siteName}
108 {this.state.isLoggedIn && (
110 class="ml-auto p-0 navbar-toggler nav-link"
112 title={i18n.t('inbox')}
115 <use xlinkHref="#icon-bell"></use>
117 {this.state.unreadCount > 0 && (
118 <span class="ml-1 badge badge-light">
119 {this.state.unreadCount}
125 class="navbar-toggler"
128 onClick={linkEvent(this, this.expandNavbar)}
129 data-tippy-content={i18n.t('expand_here')}
131 <span class="navbar-toggler-icon"></span>
134 className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
136 <ul class="navbar-nav mr-auto">
137 <li class="nav-item">
141 title={i18n.t('communities')}
143 {i18n.t('communities')}
146 <li class="nav-item">
147 <Link class="nav-link" to="/search" title={i18n.t('search')}>
151 <li class="nav-item">
155 pathname: '/create_post',
156 state: { prevPath: this.currentLocation },
158 title={i18n.t('create_post')}
160 {i18n.t('create_post')}
163 <li class="nav-item">
166 to="/create_community"
167 title={i18n.t('create_community')}
169 {i18n.t('create_community')}
172 <li className="nav-item">
176 title={i18n.t('donate_to_lemmy')}
179 <use xlinkHref="#icon-coffee"></use>
184 <ul class="navbar-nav ml-auto">
186 <li className="nav-item mt-1">
190 title={i18n.t('admin_settings')}
193 <use xlinkHref="#icon-settings"></use>
198 {this.state.isLoggedIn ? (
200 <li className="nav-item mt-1">
201 <Link class="nav-link" to="/inbox" title={i18n.t('inbox')}>
203 <use xlinkHref="#icon-bell"></use>
205 {this.state.unreadCount > 0 && (
206 <span class="ml-1 badge badge-light">
207 {this.state.unreadCount}
212 <li className="nav-item">
215 to={`/u/${UserService.Instance.user.username}`}
216 title={i18n.t('settings')}
219 {UserService.Instance.user.avatar && showAvatars() && (
221 src={pictshareAvatarThumbnail(
222 UserService.Instance.user.avatar
226 class="rounded-circle mr-2"
229 {UserService.Instance.user.username}
238 title={i18n.t('login_sign_up')}
240 {i18n.t('login_sign_up')}
249 expandNavbar(i: Navbar) {
250 i.state.expanded = !i.state.expanded;
254 parseMessage(msg: WebSocketJsonResponse) {
255 let res = wsJsonToRes(msg);
257 if (msg.error == 'not_logged_in') {
258 UserService.Instance.logout();
262 } else if (msg.reconnect) {
264 } else if (res.op == UserOperation.GetReplies) {
265 let data = res.data as GetRepliesResponse;
266 let unreadReplies = data.replies.filter(r => !r.read);
268 this.state.replies = unreadReplies;
269 this.state.unreadCount = this.calculateUnreadCount();
270 this.setState(this.state);
271 this.sendUnreadCount();
272 } else if (res.op == UserOperation.GetUserMentions) {
273 let data = res.data as GetUserMentionsResponse;
274 let unreadMentions = data.mentions.filter(r => !r.read);
276 this.state.mentions = unreadMentions;
277 this.state.unreadCount = this.calculateUnreadCount();
278 this.setState(this.state);
279 this.sendUnreadCount();
280 } else if (res.op == UserOperation.GetPrivateMessages) {
281 let data = res.data as PrivateMessagesResponse;
282 let unreadMessages = data.messages.filter(r => !r.read);
284 this.state.messages = unreadMessages;
285 this.state.unreadCount = this.calculateUnreadCount();
286 this.setState(this.state);
287 this.sendUnreadCount();
288 } else if (res.op == UserOperation.CreateComment) {
289 let data = res.data as CommentResponse;
291 if (this.state.isLoggedIn) {
292 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
293 this.state.replies.push(data.comment);
294 this.state.unreadCount++;
295 this.setState(this.state);
296 this.sendUnreadCount();
297 this.notify(data.comment);
300 } else if (res.op == UserOperation.CreatePrivateMessage) {
301 let data = res.data as PrivateMessageResponse;
303 if (this.state.isLoggedIn) {
304 if (data.message.recipient_id == UserService.Instance.user.id) {
305 this.state.messages.push(data.message);
306 this.state.unreadCount++;
307 this.setState(this.state);
308 this.sendUnreadCount();
309 this.notify(data.message);
312 } else if (res.op == UserOperation.GetSite) {
313 let data = res.data as GetSiteResponse;
315 if (data.site && !this.state.siteName) {
316 this.state.siteName = data.site.name;
317 this.state.admins = data.admins;
318 WebSocketService.Instance.site = data.site;
319 WebSocketService.Instance.admins = data.admins;
321 this.setState(this.state);
327 if (this.state.isLoggedIn) {
328 let repliesForm: GetRepliesForm = {
329 sort: SortType[SortType.New],
335 let userMentionsForm: GetUserMentionsForm = {
336 sort: SortType[SortType.New],
342 let privateMessagesForm: GetPrivateMessagesForm = {
348 if (this.currentLocation !== '/inbox') {
349 WebSocketService.Instance.getReplies(repliesForm);
350 WebSocketService.Instance.getUserMentions(userMentionsForm);
351 WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
356 get currentLocation() {
357 return this.context.router.history.location.pathname;
361 UserService.Instance.user.unreadCount = this.state.unreadCount;
362 UserService.Instance.sub.next({
363 user: UserService.Instance.user,
367 calculateUnreadCount(): number {
369 this.state.replies.filter(r => !r.read).length +
370 this.state.mentions.filter(r => !r.read).length +
371 this.state.messages.filter(r => !r.read).length
375 get canAdmin(): boolean {
377 UserService.Instance.user &&
378 this.state.admins.map(a => a.id).includes(UserService.Instance.user.id)
382 requestNotificationPermission() {
383 if (UserService.Instance.user) {
384 document.addEventListener('DOMContentLoaded', function() {
386 toast(i18n.t('notifications_error'), 'danger');
390 if (Notification.permission !== 'granted')
391 Notification.requestPermission();
396 notify(reply: Comment | PrivateMessage) {
397 let creator_name = reply.creator_name;
398 let creator_avatar = reply.creator_avatar
399 ? reply.creator_avatar
400 : `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`;
401 let link = isCommentType(reply)
402 ? `/post/${reply.post_id}/comment/${reply.id}`
404 let htmlBody = md.render(reply.content);
405 let body = reply.content; // Unfortunately the notifications API can't do html
415 if (Notification.permission !== 'granted') Notification.requestPermission();
417 var notification = new Notification(reply.creator_name, {
418 icon: creator_avatar,
422 notification.onclick = () => {
423 event.preventDefault();
424 this.context.router.history.push(link);