]> Untitled Git - lemmy.git/blob - ui/src/components/navbar.tsx
Merge branch 'dev' into icons
[lemmy.git] / ui / src / components / navbar.tsx
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';
6 import {
7   UserOperation,
8   GetRepliesForm,
9   GetRepliesResponse,
10   GetUserMentionsForm,
11   GetUserMentionsResponse,
12   GetPrivateMessagesForm,
13   PrivateMessagesResponse,
14   SortType,
15   GetSiteResponse,
16   Comment,
17   CommentResponse,
18   PrivateMessage,
19   PrivateMessageResponse,
20   WebSocketJsonResponse,
21 } from '../interfaces';
22 import {
23   wsJsonToRes,
24   pictshareAvatarThumbnail,
25   showAvatars,
26   fetchLimit,
27   isCommentType,
28   toast,
29   setupTippy,
30 } from '../utils';
31 import { version } from '../version';
32 import { i18n } from '../i18next';
33
34 interface NavbarState {
35   isLoggedIn: boolean;
36   expanded: boolean;
37   replies: Array<Comment>;
38   mentions: Array<Comment>;
39   messages: Array<PrivateMessage>;
40   unreadCount: number;
41   siteName: string;
42 }
43
44 export class Navbar extends Component<any, NavbarState> {
45   private wsSub: Subscription;
46   private userSub: Subscription;
47   emptyState: NavbarState = {
48     isLoggedIn: UserService.Instance.user !== undefined,
49     unreadCount: 0,
50     replies: [],
51     mentions: [],
52     messages: [],
53     expanded: false,
54     siteName: undefined,
55   };
56
57   constructor(props: any, context: any) {
58     super(props, context);
59     this.state = this.emptyState;
60
61     // Subscribe to user changes
62     this.userSub = UserService.Instance.sub.subscribe(user => {
63       this.state.isLoggedIn = user.user !== undefined;
64       if (this.state.isLoggedIn) {
65         this.state.unreadCount = user.user.unreadCount;
66         this.requestNotificationPermission();
67       }
68       this.setState(this.state);
69     });
70
71     this.wsSub = WebSocketService.Instance.subject
72       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
73       .subscribe(
74         msg => this.parseMessage(msg),
75         err => console.error(err),
76         () => console.log('complete')
77       );
78
79     if (this.state.isLoggedIn) {
80       this.requestNotificationPermission();
81       // TODO couldn't get re-logging in to re-fetch unreads
82       this.fetchUnreads();
83     }
84
85     WebSocketService.Instance.getSite();
86   }
87
88   componentDidMount() {
89     setupTippy();
90   }
91
92   render() {
93     return this.navbar();
94   }
95
96   componentWillUnmount() {
97     this.wsSub.unsubscribe();
98     this.userSub.unsubscribe();
99   }
100
101   // TODO class active corresponding to current page
102   navbar() {
103     return (
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}
107         </Link>
108         <button
109           class="navbar-toggler"
110           type="button"
111           aria-label="menu"
112           onClick={linkEvent(this, this.expandNavbar)}
113           data-tippy-content={i18n.t('expand_here')}
114         >
115           <span class="navbar-toggler-icon"></span>
116         </button>
117         <div
118           className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
119         >
120           <ul class="navbar-nav mr-auto">
121             <li class="nav-item">
122               <Link
123                 class="nav-link"
124                 to="/communities"
125                 title={i18n.t('communities')}
126               >
127                 {i18n.t('communities')}
128               </Link>
129             </li>
130             <li class="nav-item">
131               <Link class="nav-link" to="/search" title={i18n.t('search')}>
132                 {i18n.t('search')}
133               </Link>
134             </li>
135             <li class="nav-item">
136               <Link
137                 class="nav-link"
138                 to={{
139                   pathname: '/create_post',
140                   state: { prevPath: this.currentLocation },
141                 }}
142                 title={i18n.t('create_post')}
143               >
144                 {i18n.t('create_post')}
145               </Link>
146             </li>
147             <li class="nav-item">
148               <Link
149                 class="nav-link"
150                 to="/create_community"
151                 title={i18n.t('create_community')}
152               >
153                 {i18n.t('create_community')}
154               </Link>
155             </li>
156             <li className="nav-item">
157               <Link
158                 class="nav-link"
159                 to="/sponsors"
160                 title={i18n.t('donate_to_lemmy')}
161               >
162                 <svg class="icon">
163                   <use xlinkHref="#icon-coffee"></use>
164                 </svg>
165               </Link>
166             </li>
167           </ul>
168           <ul class="navbar-nav ml-auto">
169             {this.state.isLoggedIn ? (
170               <>
171                 <li className="nav-item mt-1">
172                   <Link class="nav-link" to="/inbox" title={i18n.t('inbox')}>
173                     <svg class="icon">
174                       <use xlinkHref="#icon-bell"></use>
175                     </svg>
176                     {this.state.unreadCount > 0 && (
177                       <span class="ml-1 badge badge-light">
178                         {this.state.unreadCount}
179                       </span>
180                     )}
181                   </Link>
182                 </li>
183                 <li className="nav-item">
184                   <Link
185                     class="nav-link"
186                     to={`/u/${UserService.Instance.user.username}`}
187                     title={i18n.t('settings')}
188                   >
189                     <span>
190                       {UserService.Instance.user.avatar && showAvatars() && (
191                         <img
192                           src={pictshareAvatarThumbnail(
193                             UserService.Instance.user.avatar
194                           )}
195                           height="32"
196                           width="32"
197                           class="rounded-circle mr-2"
198                         />
199                       )}
200                       {UserService.Instance.user.username}
201                     </span>
202                   </Link>
203                 </li>
204               </>
205             ) : (
206               <Link
207                 class="nav-link"
208                 to="/login"
209                 title={i18n.t('login_sign_up')}
210               >
211                 {i18n.t('login_sign_up')}
212               </Link>
213             )}
214           </ul>
215         </div>
216       </nav>
217     );
218   }
219
220   expandNavbar(i: Navbar) {
221     i.state.expanded = !i.state.expanded;
222     i.setState(i.state);
223   }
224
225   parseMessage(msg: WebSocketJsonResponse) {
226     let res = wsJsonToRes(msg);
227     if (msg.error) {
228       if (msg.error == 'not_logged_in') {
229         UserService.Instance.logout();
230         location.reload();
231       }
232       return;
233     } else if (msg.reconnect) {
234       this.fetchUnreads();
235     } else if (res.op == UserOperation.GetReplies) {
236       let data = res.data as GetRepliesResponse;
237       let unreadReplies = data.replies.filter(r => !r.read);
238
239       this.state.replies = unreadReplies;
240       this.state.unreadCount = this.calculateUnreadCount();
241       this.setState(this.state);
242       this.sendUnreadCount();
243     } else if (res.op == UserOperation.GetUserMentions) {
244       let data = res.data as GetUserMentionsResponse;
245       let unreadMentions = data.mentions.filter(r => !r.read);
246
247       this.state.mentions = unreadMentions;
248       this.state.unreadCount = this.calculateUnreadCount();
249       this.setState(this.state);
250       this.sendUnreadCount();
251     } else if (res.op == UserOperation.GetPrivateMessages) {
252       let data = res.data as PrivateMessagesResponse;
253       let unreadMessages = data.messages.filter(r => !r.read);
254
255       this.state.messages = unreadMessages;
256       this.state.unreadCount = this.calculateUnreadCount();
257       this.setState(this.state);
258       this.sendUnreadCount();
259     } else if (res.op == UserOperation.CreateComment) {
260       let data = res.data as CommentResponse;
261
262       if (this.state.isLoggedIn) {
263         if (data.recipient_ids.includes(UserService.Instance.user.id)) {
264           this.state.replies.push(data.comment);
265           this.state.unreadCount++;
266           this.setState(this.state);
267           this.sendUnreadCount();
268           this.notify(data.comment);
269         }
270       }
271     } else if (res.op == UserOperation.CreatePrivateMessage) {
272       let data = res.data as PrivateMessageResponse;
273
274       if (this.state.isLoggedIn) {
275         if (data.message.recipient_id == UserService.Instance.user.id) {
276           this.state.messages.push(data.message);
277           this.state.unreadCount++;
278           this.setState(this.state);
279           this.sendUnreadCount();
280           this.notify(data.message);
281         }
282       }
283     } else if (res.op == UserOperation.GetSite) {
284       let data = res.data as GetSiteResponse;
285
286       if (data.site) {
287         this.state.siteName = data.site.name;
288         WebSocketService.Instance.site = data.site;
289         this.setState(this.state);
290       }
291     }
292   }
293
294   fetchUnreads() {
295     if (this.state.isLoggedIn) {
296       let repliesForm: GetRepliesForm = {
297         sort: SortType[SortType.New],
298         unread_only: true,
299         page: 1,
300         limit: fetchLimit,
301       };
302
303       let userMentionsForm: GetUserMentionsForm = {
304         sort: SortType[SortType.New],
305         unread_only: true,
306         page: 1,
307         limit: fetchLimit,
308       };
309
310       let privateMessagesForm: GetPrivateMessagesForm = {
311         unread_only: true,
312         page: 1,
313         limit: fetchLimit,
314       };
315
316       if (this.currentLocation !== '/inbox') {
317         WebSocketService.Instance.getReplies(repliesForm);
318         WebSocketService.Instance.getUserMentions(userMentionsForm);
319         WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
320       }
321     }
322   }
323
324   get currentLocation() {
325     return this.context.router.history.location.pathname;
326   }
327
328   sendUnreadCount() {
329     UserService.Instance.user.unreadCount = this.state.unreadCount;
330     UserService.Instance.sub.next({
331       user: UserService.Instance.user,
332     });
333   }
334
335   calculateUnreadCount(): number {
336     return (
337       this.state.replies.filter(r => !r.read).length +
338       this.state.mentions.filter(r => !r.read).length +
339       this.state.messages.filter(r => !r.read).length
340     );
341   }
342
343   requestNotificationPermission() {
344     if (UserService.Instance.user) {
345       document.addEventListener('DOMContentLoaded', function() {
346         if (!Notification) {
347           toast(i18n.t('notifications_error'), 'danger');
348           return;
349         }
350
351         if (Notification.permission !== 'granted')
352           Notification.requestPermission();
353       });
354     }
355   }
356
357   notify(reply: Comment | PrivateMessage) {
358     if (Notification.permission !== 'granted') Notification.requestPermission();
359     else {
360       var notification = new Notification(reply.creator_name, {
361         icon: reply.creator_avatar
362           ? reply.creator_avatar
363           : `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
364         body: `${reply.content}`,
365       });
366
367       notification.onclick = () => {
368         this.context.router.history.push(
369           isCommentType(reply)
370             ? `/post/${reply.post_id}/comment/${reply.id}`
371             : `/inbox`
372         );
373       };
374     }
375   }
376 }