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