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