]> Untitled Git - lemmy-ui.git/blob - src/shared/components/inbox.tsx
Add post, inbox, and user routes.
[lemmy-ui.git] / src / shared / components / inbox.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Helmet } from 'inferno-helmet';
3 import { Subscription } from 'rxjs';
4 import {
5   UserOperation,
6   Comment,
7   SortType,
8   GetRepliesForm,
9   GetRepliesResponse,
10   GetUserMentionsForm,
11   GetUserMentionsResponse,
12   UserMentionResponse,
13   CommentResponse,
14   WebSocketJsonResponse,
15   PrivateMessage as PrivateMessageI,
16   GetPrivateMessagesForm,
17   PrivateMessagesResponse,
18   PrivateMessageResponse,
19   Site,
20 } from 'lemmy-js-client';
21 import { WebSocketService, UserService } from '../services';
22 import {
23   wsJsonToRes,
24   fetchLimit,
25   isCommentType,
26   toast,
27   editCommentRes,
28   saveCommentRes,
29   createCommentLikeRes,
30   commentsToFlatNodes,
31   setupTippy,
32   setIsoData,
33   wsSubscribe,
34   lemmyHttp,
35   setAuth,
36   isBrowser,
37 } from '../utils';
38 import { CommentNodes } from './comment-nodes';
39 import { PrivateMessage } from './private-message';
40 import { SortSelect } from './sort-select';
41 import { i18n } from '../i18next';
42
43 enum UnreadOrAll {
44   Unread,
45   All,
46 }
47
48 enum MessageType {
49   All,
50   Replies,
51   Mentions,
52   Messages,
53 }
54
55 type ReplyType = Comment | PrivateMessageI;
56
57 interface InboxState {
58   unreadOrAll: UnreadOrAll;
59   messageType: MessageType;
60   replies: Comment[];
61   mentions: Comment[];
62   messages: PrivateMessageI[];
63   sort: SortType;
64   page: number;
65   site: Site;
66   loading: boolean;
67 }
68
69 export class Inbox extends Component<any, InboxState> {
70   private isoData = setIsoData(this.context);
71   private subscription: Subscription;
72   private emptyState: InboxState = {
73     unreadOrAll: UnreadOrAll.Unread,
74     messageType: MessageType.All,
75     replies: [],
76     mentions: [],
77     messages: [],
78     sort: SortType.New,
79     page: 1,
80     site: this.isoData.site.site,
81     loading: true,
82   };
83
84   constructor(props: any, context: any) {
85     super(props, context);
86
87     this.state = this.emptyState;
88     this.handleSortChange = this.handleSortChange.bind(this);
89
90     this.parseMessage = this.parseMessage.bind(this);
91     this.subscription = wsSubscribe(this.parseMessage);
92
93     // Only fetch the data if coming from another route
94     if (this.isoData.path == this.context.router.route.match.url) {
95       this.state.replies = this.isoData.routeData[0].replies;
96       this.state.mentions = this.isoData.routeData[1].mentions;
97       this.state.messages = this.isoData.routeData[2].messages;
98       this.sendUnreadCount();
99       this.state.loading = false;
100     } else {
101       this.refetch();
102     }
103   }
104
105   componentWillUnmount() {
106     if (isBrowser()) {
107       this.subscription.unsubscribe();
108     }
109   }
110
111   get documentTitle(): string {
112     return `@${UserService.Instance.user.name} ${i18n.t('inbox')} - ${
113       this.state.site.name
114     }`;
115   }
116
117   render() {
118     return (
119       <div class="container">
120         <Helmet title={this.documentTitle} />
121         {this.state.loading ? (
122           <h5>
123             <svg class="icon icon-spinner spin">
124               <use xlinkHref="#icon-spinner"></use>
125             </svg>
126           </h5>
127         ) : (
128           <div class="row">
129             <div class="col-12">
130               <h5 class="mb-1">
131                 {i18n.t('inbox')}
132                 <small>
133                   <a
134                     href={`/feeds/inbox/${UserService.Instance.auth}.xml`}
135                     target="_blank"
136                     title="RSS"
137                     rel="noopener"
138                   >
139                     <svg class="icon ml-2 text-muted small">
140                       <use xlinkHref="#icon-rss">#</use>
141                     </svg>
142                   </a>
143                 </small>
144               </h5>
145               {this.state.replies.length +
146                 this.state.mentions.length +
147                 this.state.messages.length >
148                 0 &&
149                 this.state.unreadOrAll == UnreadOrAll.Unread && (
150                   <ul class="list-inline mb-1 text-muted small font-weight-bold">
151                     <li className="list-inline-item">
152                       <span
153                         class="pointer"
154                         onClick={linkEvent(this, this.markAllAsRead)}
155                       >
156                         {i18n.t('mark_all_as_read')}
157                       </span>
158                     </li>
159                   </ul>
160                 )}
161               {this.selects()}
162               {this.state.messageType == MessageType.All && this.all()}
163               {this.state.messageType == MessageType.Replies && this.replies()}
164               {this.state.messageType == MessageType.Mentions &&
165                 this.mentions()}
166               {this.state.messageType == MessageType.Messages &&
167                 this.messages()}
168               {this.paginator()}
169             </div>
170           </div>
171         )}
172       </div>
173     );
174   }
175
176   unreadOrAllRadios() {
177     return (
178       <div class="btn-group btn-group-toggle flex-wrap mb-2">
179         <label
180           className={`btn btn-outline-secondary pointer
181             ${this.state.unreadOrAll == UnreadOrAll.Unread && 'active'}
182           `}
183         >
184           <input
185             type="radio"
186             value={UnreadOrAll.Unread}
187             checked={this.state.unreadOrAll == UnreadOrAll.Unread}
188             onChange={linkEvent(this, this.handleUnreadOrAllChange)}
189           />
190           {i18n.t('unread')}
191         </label>
192         <label
193           className={`btn btn-outline-secondary pointer
194             ${this.state.unreadOrAll == UnreadOrAll.All && 'active'}
195           `}
196         >
197           <input
198             type="radio"
199             value={UnreadOrAll.All}
200             checked={this.state.unreadOrAll == UnreadOrAll.All}
201             onChange={linkEvent(this, this.handleUnreadOrAllChange)}
202           />
203           {i18n.t('all')}
204         </label>
205       </div>
206     );
207   }
208
209   messageTypeRadios() {
210     return (
211       <div class="btn-group btn-group-toggle flex-wrap mb-2">
212         <label
213           className={`btn btn-outline-secondary pointer
214             ${this.state.messageType == MessageType.All && 'active'}
215           `}
216         >
217           <input
218             type="radio"
219             value={MessageType.All}
220             checked={this.state.messageType == MessageType.All}
221             onChange={linkEvent(this, this.handleMessageTypeChange)}
222           />
223           {i18n.t('all')}
224         </label>
225         <label
226           className={`btn btn-outline-secondary pointer
227             ${this.state.messageType == MessageType.Replies && 'active'}
228           `}
229         >
230           <input
231             type="radio"
232             value={MessageType.Replies}
233             checked={this.state.messageType == MessageType.Replies}
234             onChange={linkEvent(this, this.handleMessageTypeChange)}
235           />
236           {i18n.t('replies')}
237         </label>
238         <label
239           className={`btn btn-outline-secondary pointer
240             ${this.state.messageType == MessageType.Mentions && 'active'}
241           `}
242         >
243           <input
244             type="radio"
245             value={MessageType.Mentions}
246             checked={this.state.messageType == MessageType.Mentions}
247             onChange={linkEvent(this, this.handleMessageTypeChange)}
248           />
249           {i18n.t('mentions')}
250         </label>
251         <label
252           className={`btn btn-outline-secondary pointer
253             ${this.state.messageType == MessageType.Messages && 'active'}
254           `}
255         >
256           <input
257             type="radio"
258             value={MessageType.Messages}
259             checked={this.state.messageType == MessageType.Messages}
260             onChange={linkEvent(this, this.handleMessageTypeChange)}
261           />
262           {i18n.t('messages')}
263         </label>
264       </div>
265     );
266   }
267
268   selects() {
269     return (
270       <div className="mb-2">
271         <span class="mr-3">{this.unreadOrAllRadios()}</span>
272         <span class="mr-3">{this.messageTypeRadios()}</span>
273         <SortSelect
274           sort={this.state.sort}
275           onChange={this.handleSortChange}
276           hideHot
277         />
278       </div>
279     );
280   }
281
282   combined(): ReplyType[] {
283     return [
284       ...this.state.replies,
285       ...this.state.mentions,
286       ...this.state.messages,
287     ].sort((a, b) => b.published.localeCompare(a.published));
288   }
289
290   all() {
291     return (
292       <div>
293         {this.combined().map(i =>
294           isCommentType(i) ? (
295             <CommentNodes
296               key={i.id}
297               nodes={[{ comment: i }]}
298               noIndent
299               markable
300               showCommunity
301               showContext
302               enableDownvotes={this.state.site.enable_downvotes}
303             />
304           ) : (
305             <PrivateMessage key={i.id} privateMessage={i} />
306           )
307         )}
308       </div>
309     );
310   }
311
312   replies() {
313     return (
314       <div>
315         <CommentNodes
316           nodes={commentsToFlatNodes(this.state.replies)}
317           noIndent
318           markable
319           showCommunity
320           showContext
321           enableDownvotes={this.state.site.enable_downvotes}
322         />
323       </div>
324     );
325   }
326
327   mentions() {
328     return (
329       <div>
330         {this.state.mentions.map(mention => (
331           <CommentNodes
332             key={mention.id}
333             nodes={[{ comment: mention }]}
334             noIndent
335             markable
336             showCommunity
337             showContext
338             enableDownvotes={this.state.site.enable_downvotes}
339           />
340         ))}
341       </div>
342     );
343   }
344
345   messages() {
346     return (
347       <div>
348         {this.state.messages.map(message => (
349           <PrivateMessage key={message.id} privateMessage={message} />
350         ))}
351       </div>
352     );
353   }
354
355   paginator() {
356     return (
357       <div class="mt-2">
358         {this.state.page > 1 && (
359           <button
360             class="btn btn-secondary mr-1"
361             onClick={linkEvent(this, this.prevPage)}
362           >
363             {i18n.t('prev')}
364           </button>
365         )}
366         {this.unreadCount() > 0 && (
367           <button
368             class="btn btn-secondary"
369             onClick={linkEvent(this, this.nextPage)}
370           >
371             {i18n.t('next')}
372           </button>
373         )}
374       </div>
375     );
376   }
377
378   nextPage(i: Inbox) {
379     i.state.page++;
380     i.setState(i.state);
381     i.refetch();
382   }
383
384   prevPage(i: Inbox) {
385     i.state.page--;
386     i.setState(i.state);
387     i.refetch();
388   }
389
390   handleUnreadOrAllChange(i: Inbox, event: any) {
391     i.state.unreadOrAll = Number(event.target.value);
392     i.state.page = 1;
393     i.setState(i.state);
394     i.refetch();
395   }
396
397   handleMessageTypeChange(i: Inbox, event: any) {
398     i.state.messageType = Number(event.target.value);
399     i.state.page = 1;
400     i.setState(i.state);
401     i.refetch();
402   }
403
404   static fetchInitialData(auth: string, _path: string): Promise<any>[] {
405     let promises: Promise<any>[] = [];
406
407     // It can be /u/me, or /username/1
408     let repliesForm: GetRepliesForm = {
409       sort: SortType.New,
410       unread_only: true,
411       page: 1,
412       limit: fetchLimit,
413     };
414     setAuth(repliesForm, auth);
415     promises.push(lemmyHttp.getReplies(repliesForm));
416
417     let userMentionsForm: GetUserMentionsForm = {
418       sort: SortType.New,
419       unread_only: true,
420       page: 1,
421       limit: fetchLimit,
422     };
423     setAuth(userMentionsForm, auth);
424     promises.push(lemmyHttp.getUserMentions(userMentionsForm));
425
426     let privateMessagesForm: GetPrivateMessagesForm = {
427       unread_only: true,
428       page: 1,
429       limit: fetchLimit,
430     };
431     setAuth(privateMessagesForm, auth);
432     promises.push(lemmyHttp.getPrivateMessages(privateMessagesForm));
433
434     return promises;
435   }
436
437   refetch() {
438     let repliesForm: GetRepliesForm = {
439       sort: this.state.sort,
440       unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
441       page: this.state.page,
442       limit: fetchLimit,
443     };
444     WebSocketService.Instance.getReplies(repliesForm);
445
446     let userMentionsForm: GetUserMentionsForm = {
447       sort: this.state.sort,
448       unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
449       page: this.state.page,
450       limit: fetchLimit,
451     };
452     WebSocketService.Instance.getUserMentions(userMentionsForm);
453
454     let privateMessagesForm: GetPrivateMessagesForm = {
455       unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
456       page: this.state.page,
457       limit: fetchLimit,
458     };
459     WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
460   }
461
462   handleSortChange(val: SortType) {
463     this.state.sort = val;
464     this.state.page = 1;
465     this.setState(this.state);
466     this.refetch();
467   }
468
469   markAllAsRead(i: Inbox) {
470     WebSocketService.Instance.markAllAsRead();
471     i.state.replies = [];
472     i.state.mentions = [];
473     i.state.messages = [];
474     i.sendUnreadCount();
475     window.scrollTo(0, 0);
476     i.setState(i.state);
477   }
478
479   parseMessage(msg: WebSocketJsonResponse) {
480     console.log(msg);
481     let res = wsJsonToRes(msg);
482     if (msg.error) {
483       toast(i18n.t(msg.error), 'danger');
484       return;
485     } else if (msg.reconnect) {
486       this.refetch();
487     } else if (res.op == UserOperation.GetReplies) {
488       let data = res.data as GetRepliesResponse;
489       this.state.replies = data.replies;
490       this.state.loading = false;
491       this.sendUnreadCount();
492       window.scrollTo(0, 0);
493       this.setState(this.state);
494       setupTippy();
495     } else if (res.op == UserOperation.GetUserMentions) {
496       let data = res.data as GetUserMentionsResponse;
497       this.state.mentions = data.mentions;
498       this.sendUnreadCount();
499       window.scrollTo(0, 0);
500       this.setState(this.state);
501       setupTippy();
502     } else if (res.op == UserOperation.GetPrivateMessages) {
503       let data = res.data as PrivateMessagesResponse;
504       this.state.messages = data.messages;
505       this.sendUnreadCount();
506       window.scrollTo(0, 0);
507       this.setState(this.state);
508       setupTippy();
509     } else if (res.op == UserOperation.EditPrivateMessage) {
510       let data = res.data as PrivateMessageResponse;
511       let found: PrivateMessageI = this.state.messages.find(
512         m => m.id === data.message.id
513       );
514       if (found) {
515         found.content = data.message.content;
516         found.updated = data.message.updated;
517       }
518       this.setState(this.state);
519     } else if (res.op == UserOperation.DeletePrivateMessage) {
520       let data = res.data as PrivateMessageResponse;
521       let found: PrivateMessageI = this.state.messages.find(
522         m => m.id === data.message.id
523       );
524       if (found) {
525         found.deleted = data.message.deleted;
526         found.updated = data.message.updated;
527       }
528       this.setState(this.state);
529     } else if (res.op == UserOperation.MarkPrivateMessageAsRead) {
530       let data = res.data as PrivateMessageResponse;
531       let found: PrivateMessageI = this.state.messages.find(
532         m => m.id === data.message.id
533       );
534
535       if (found) {
536         found.updated = data.message.updated;
537
538         // If youre in the unread view, just remove it from the list
539         if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) {
540           this.state.messages = this.state.messages.filter(
541             r => r.id !== data.message.id
542           );
543         } else {
544           let found = this.state.messages.find(c => c.id == data.message.id);
545           found.read = data.message.read;
546         }
547       }
548       this.sendUnreadCount();
549       this.setState(this.state);
550     } else if (res.op == UserOperation.MarkAllAsRead) {
551       // Moved to be instant
552     } else if (
553       res.op == UserOperation.EditComment ||
554       res.op == UserOperation.DeleteComment ||
555       res.op == UserOperation.RemoveComment
556     ) {
557       let data = res.data as CommentResponse;
558       editCommentRes(data, this.state.replies);
559       this.setState(this.state);
560     } else if (res.op == UserOperation.MarkCommentAsRead) {
561       let data = res.data as CommentResponse;
562
563       // If youre in the unread view, just remove it from the list
564       if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
565         this.state.replies = this.state.replies.filter(
566           r => r.id !== data.comment.id
567         );
568       } else {
569         let found = this.state.replies.find(c => c.id == data.comment.id);
570         found.read = data.comment.read;
571       }
572       this.sendUnreadCount();
573       this.setState(this.state);
574       setupTippy();
575     } else if (res.op == UserOperation.MarkUserMentionAsRead) {
576       let data = res.data as UserMentionResponse;
577
578       let found = this.state.mentions.find(c => c.id == data.mention.id);
579       found.content = data.mention.content;
580       found.updated = data.mention.updated;
581       found.removed = data.mention.removed;
582       found.deleted = data.mention.deleted;
583       found.upvotes = data.mention.upvotes;
584       found.downvotes = data.mention.downvotes;
585       found.score = data.mention.score;
586
587       // If youre in the unread view, just remove it from the list
588       if (this.state.unreadOrAll == UnreadOrAll.Unread && data.mention.read) {
589         this.state.mentions = this.state.mentions.filter(
590           r => r.id !== data.mention.id
591         );
592       } else {
593         let found = this.state.mentions.find(c => c.id == data.mention.id);
594         found.read = data.mention.read;
595       }
596       this.sendUnreadCount();
597       this.setState(this.state);
598     } else if (res.op == UserOperation.CreateComment) {
599       let data = res.data as CommentResponse;
600
601       if (data.recipient_ids.includes(UserService.Instance.user.id)) {
602         this.state.replies.unshift(data.comment);
603         this.setState(this.state);
604       } else if (data.comment.creator_id == UserService.Instance.user.id) {
605         toast(i18n.t('reply_sent'));
606       }
607     } else if (res.op == UserOperation.CreatePrivateMessage) {
608       let data = res.data as PrivateMessageResponse;
609       if (data.message.recipient_id == UserService.Instance.user.id) {
610         this.state.messages.unshift(data.message);
611         this.setState(this.state);
612       }
613     } else if (res.op == UserOperation.SaveComment) {
614       let data = res.data as CommentResponse;
615       saveCommentRes(data, this.state.replies);
616       this.setState(this.state);
617       setupTippy();
618     } else if (res.op == UserOperation.CreateCommentLike) {
619       let data = res.data as CommentResponse;
620       createCommentLikeRes(data, this.state.replies);
621       this.setState(this.state);
622     }
623   }
624
625   sendUnreadCount() {
626     UserService.Instance.unreadCountSub.next(this.unreadCount());
627   }
628
629   unreadCount(): number {
630     return (
631       this.state.replies.filter(r => !r.read).length +
632       this.state.mentions.filter(r => !r.read).length +
633       this.state.messages.filter(
634         r =>
635           UserService.Instance.user &&
636           !r.read &&
637           r.creator_id !== UserService.Instance.user.id
638       ).length
639     );
640   }
641 }