]> Untitled Git - lemmy.git/blob - ui/src/components/navbar.tsx
Merge branch 'websocket_reconnect_reload' 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   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 } from '../utils';
30 import { version } from '../version';
31 import { i18n } from '../i18next';
32
33 interface NavbarState {
34   isLoggedIn: boolean;
35   expanded: boolean;
36   replies: Array<Comment>;
37   mentions: Array<Comment>;
38   messages: Array<PrivateMessage>;
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     replies: [],
50     mentions: [],
51     messages: [],
52     expanded: false,
53     siteName: undefined,
54   };
55
56   constructor(props: any, context: any) {
57     super(props, context);
58     this.state = this.emptyState;
59
60     // Subscribe to user changes
61     this.userSub = UserService.Instance.sub.subscribe(user => {
62       this.state.isLoggedIn = user.user !== undefined;
63       this.state.unreadCount = user.unreadCount;
64       this.requestNotificationPermission();
65       this.setState(this.state);
66     });
67
68     this.wsSub = WebSocketService.Instance.subject
69       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
70       .subscribe(
71         msg => this.parseMessage(msg),
72         err => console.error(err),
73         () => console.log('complete')
74       );
75
76     if (this.state.isLoggedIn) {
77       this.requestNotificationPermission();
78       // TODO couldn't get re-logging in to re-fetch unreads
79       this.fetchUnreads();
80     }
81
82     WebSocketService.Instance.getSite();
83   }
84
85   render() {
86     return this.navbar();
87   }
88
89   componentWillUnmount() {
90     this.wsSub.unsubscribe();
91     this.userSub.unsubscribe();
92   }
93
94   // TODO class active corresponding to current page
95   navbar() {
96     return (
97       <nav class="container-fluid navbar navbar-expand-md navbar-light shadow p-0 px-3">
98         <Link title={version} class="navbar-brand" to="/">
99           {this.state.siteName}
100         </Link>
101         <button
102           class="navbar-toggler"
103           type="button"
104           aria-label="menu"
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                 {i18n.t('communities')}
116               </Link>
117             </li>
118             <li class="nav-item">
119               <Link class="nav-link" to="/search">
120                 {i18n.t('search')}
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                 {i18n.t('create_post')}
132               </Link>
133             </li>
134             <li class="nav-item">
135               <Link class="nav-link" to="/create_community">
136                 {i18n.t('create_community')}
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                 {i18n.t('login_sign_up')}
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 (msg.reconnect) {
212       this.fetchUnreads();
213     } else if (res.op == UserOperation.GetReplies) {
214       let data = res.data as GetRepliesResponse;
215       let unreadReplies = data.replies.filter(r => !r.read);
216
217       this.state.replies = unreadReplies;
218       this.state.unreadCount = this.calculateUnreadCount();
219       this.setState(this.state);
220       this.sendUnreadCount();
221     } else if (res.op == UserOperation.GetUserMentions) {
222       let data = res.data as GetUserMentionsResponse;
223       let unreadMentions = data.mentions.filter(r => !r.read);
224
225       this.state.mentions = unreadMentions;
226       this.state.unreadCount = this.calculateUnreadCount();
227       this.setState(this.state);
228       this.sendUnreadCount();
229     } else if (res.op == UserOperation.GetPrivateMessages) {
230       let data = res.data as PrivateMessagesResponse;
231       let unreadMessages = data.messages.filter(r => !r.read);
232
233       this.state.messages = unreadMessages;
234       this.state.unreadCount = this.calculateUnreadCount();
235       this.setState(this.state);
236       this.sendUnreadCount();
237     } else if (res.op == UserOperation.CreateComment) {
238       let data = res.data as CommentResponse;
239
240       if (this.state.isLoggedIn) {
241         if (data.recipient_ids.includes(UserService.Instance.user.id)) {
242           this.state.replies.push(data.comment);
243           this.state.unreadCount++;
244           this.setState(this.state);
245           this.sendUnreadCount();
246           this.notify(data.comment);
247         }
248       }
249     } else if (res.op == UserOperation.CreatePrivateMessage) {
250       let data = res.data as PrivateMessageResponse;
251
252       if (this.state.isLoggedIn) {
253         if (data.message.recipient_id == UserService.Instance.user.id) {
254           this.state.messages.push(data.message);
255           this.state.unreadCount++;
256           this.setState(this.state);
257           this.sendUnreadCount();
258           this.notify(data.message);
259         }
260       }
261     } else if (res.op == UserOperation.GetSite) {
262       let data = res.data as GetSiteResponse;
263
264       if (data.site) {
265         this.state.siteName = data.site.name;
266         WebSocketService.Instance.site = data.site;
267         this.setState(this.state);
268       }
269     }
270   }
271
272   fetchUnreads() {
273     if (this.state.isLoggedIn) {
274       let repliesForm: GetRepliesForm = {
275         sort: SortType[SortType.New],
276         unread_only: true,
277         page: 1,
278         limit: fetchLimit,
279       };
280
281       let userMentionsForm: GetUserMentionsForm = {
282         sort: SortType[SortType.New],
283         unread_only: true,
284         page: 1,
285         limit: fetchLimit,
286       };
287
288       let privateMessagesForm: GetPrivateMessagesForm = {
289         unread_only: true,
290         page: 1,
291         limit: fetchLimit,
292       };
293
294       if (this.currentLocation !== '/inbox') {
295         WebSocketService.Instance.getReplies(repliesForm);
296         WebSocketService.Instance.getUserMentions(userMentionsForm);
297         WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
298       }
299     }
300   }
301
302   get currentLocation() {
303     return this.context.router.history.location.pathname;
304   }
305
306   sendUnreadCount() {
307     UserService.Instance.sub.next({
308       user: UserService.Instance.user,
309       unreadCount: this.state.unreadCount,
310     });
311   }
312
313   calculateUnreadCount(): number {
314     return (
315       this.state.replies.filter(r => !r.read).length +
316       this.state.mentions.filter(r => !r.read).length +
317       this.state.messages.filter(r => !r.read).length
318     );
319   }
320
321   requestNotificationPermission() {
322     if (UserService.Instance.user) {
323       document.addEventListener('DOMContentLoaded', function() {
324         if (!Notification) {
325           toast(i18n.t('notifications_error'), 'danger');
326           return;
327         }
328
329         if (Notification.permission !== 'granted')
330           Notification.requestPermission();
331       });
332     }
333   }
334
335   notify(reply: Comment | PrivateMessage) {
336     if (Notification.permission !== 'granted') Notification.requestPermission();
337     else {
338       var notification = new Notification(reply.creator_name, {
339         icon: reply.creator_avatar
340           ? reply.creator_avatar
341           : `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
342         body: `${reply.content}`,
343       });
344
345       notification.onclick = () => {
346         this.context.router.history.push(
347           isCommentType(reply)
348             ? `/post/${reply.post_id}/comment/${reply.id}`
349             : `/inbox`
350         );
351       };
352     }
353   }
354 }