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