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