]> Untitled Git - lemmy.git/blob - ui/src/components/inbox.tsx
Merge branch 'dev' into federation
[lemmy.git] / ui / src / components / inbox.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 {
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 } from '../interfaces';
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 } from '../utils';
32 import { CommentNodes } from './comment-nodes';
33 import { PrivateMessage } from './private-message';
34 import { SortSelect } from './sort-select';
35 import { i18n } from '../i18next';
36 import { T } from 'inferno-i18next';
37
38 enum UnreadOrAll {
39   Unread,
40   All,
41 }
42
43 enum UnreadType {
44   All,
45   Replies,
46   Mentions,
47   Messages,
48 }
49
50 type ReplyType = Comment | PrivateMessageI;
51
52 interface InboxState {
53   unreadOrAll: UnreadOrAll;
54   unreadType: UnreadType;
55   replies: Array<Comment>;
56   mentions: Array<Comment>;
57   messages: Array<PrivateMessageI>;
58   sort: SortType;
59   page: number;
60 }
61
62 export class Inbox extends Component<any, InboxState> {
63   private subscription: Subscription;
64   private emptyState: InboxState = {
65     unreadOrAll: UnreadOrAll.Unread,
66     unreadType: UnreadType.All,
67     replies: [],
68     mentions: [],
69     messages: [],
70     sort: SortType.New,
71     page: 1,
72   };
73
74   constructor(props: any, context: any) {
75     super(props, context);
76
77     this.state = this.emptyState;
78     this.handleSortChange = this.handleSortChange.bind(this);
79
80     this.subscription = WebSocketService.Instance.subject
81       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
82       .subscribe(
83         msg => this.parseMessage(msg),
84         err => console.error(err),
85         () => console.log('complete')
86       );
87
88     this.refetch();
89   }
90
91   componentWillUnmount() {
92     this.subscription.unsubscribe();
93   }
94
95   componentDidMount() {
96     document.title = `/u/${UserService.Instance.user.username} ${i18n.t(
97       'inbox'
98     )} - ${WebSocketService.Instance.site.name}`;
99   }
100
101   render() {
102     let user = UserService.Instance.user;
103     return (
104       <div class="container">
105         <div class="row">
106           <div class="col-12">
107             <h5 class="mb-0">
108               <T
109                 class="d-inline"
110                 i18nKey="inbox_for"
111                 interpolation={{ user: user.username }}
112               >
113                 #<Link to={`/u/${user.username}`}>#</Link>
114               </T>
115               <small>
116                 <a
117                   href={`/feeds/inbox/${UserService.Instance.auth}.xml`}
118                   target="_blank"
119                 >
120                   <svg class="icon mx-2 text-muted small">
121                     <use xlinkHref="#icon-rss">#</use>
122                   </svg>
123                 </a>
124               </small>
125             </h5>
126             {this.state.replies.length +
127               this.state.mentions.length +
128               this.state.messages.length >
129               0 &&
130               this.state.unreadOrAll == UnreadOrAll.Unread && (
131                 <ul class="list-inline mb-1 text-muted small font-weight-bold">
132                   <li className="list-inline-item">
133                     <span class="pointer" onClick={this.markAllAsRead}>
134                       {i18n.t('mark_all_as_read')}
135                     </span>
136                   </li>
137                 </ul>
138               )}
139             {this.selects()}
140             {this.state.unreadType == UnreadType.All && this.all()}
141             {this.state.unreadType == UnreadType.Replies && this.replies()}
142             {this.state.unreadType == UnreadType.Mentions && this.mentions()}
143             {this.state.unreadType == UnreadType.Messages && this.messages()}
144             {this.paginator()}
145           </div>
146         </div>
147       </div>
148     );
149   }
150
151   selects() {
152     return (
153       <div className="mb-2">
154         <select
155           value={this.state.unreadOrAll}
156           onChange={linkEvent(this, this.handleUnreadOrAllChange)}
157           class="custom-select custom-select-sm w-auto mr-2"
158         >
159           <option disabled>{i18n.t('type')}</option>
160           <option value={UnreadOrAll.Unread}>{i18n.t('unread')}</option>
161           <option value={UnreadOrAll.All}>{i18n.t('all')}</option>
162         </select>
163         <select
164           value={this.state.unreadType}
165           onChange={linkEvent(this, this.handleUnreadTypeChange)}
166           class="custom-select custom-select-sm w-auto mr-2"
167         >
168           <option disabled>{i18n.t('type')}</option>
169           <option value={UnreadType.All}>{i18n.t('all')}</option>
170           <option value={UnreadType.Replies}>{i18n.t('replies')}</option>
171           <option value={UnreadType.Mentions}>{i18n.t('mentions')}</option>
172           <option value={UnreadType.Messages}>{i18n.t('messages')}</option>
173         </select>
174         <SortSelect
175           sort={this.state.sort}
176           onChange={this.handleSortChange}
177           hideHot
178         />
179       </div>
180     );
181   }
182
183   all() {
184     let combined: Array<ReplyType> = [];
185
186     combined.push(...this.state.replies);
187     combined.push(...this.state.mentions);
188     combined.push(...this.state.messages);
189
190     // Sort it
191     combined.sort((a, b) => b.published.localeCompare(a.published));
192
193     return (
194       <div>
195         {combined.map(i =>
196           isCommentType(i) ? (
197             <CommentNodes nodes={[{ comment: i }]} noIndent markable />
198           ) : (
199             <PrivateMessage privateMessage={i} />
200           )
201         )}
202       </div>
203     );
204   }
205
206   replies() {
207     return (
208       <div>
209         <CommentNodes
210           nodes={commentsToFlatNodes(this.state.replies)}
211           noIndent
212           markable
213         />
214       </div>
215     );
216   }
217
218   mentions() {
219     return (
220       <div>
221         {this.state.mentions.map(mention => (
222           <CommentNodes nodes={[{ comment: mention }]} noIndent markable />
223         ))}
224       </div>
225     );
226   }
227
228   messages() {
229     return (
230       <div>
231         {this.state.messages.map(message => (
232           <PrivateMessage privateMessage={message} />
233         ))}
234       </div>
235     );
236   }
237
238   paginator() {
239     return (
240       <div class="mt-2">
241         {this.state.page > 1 && (
242           <button
243             class="btn btn-sm btn-secondary mr-1"
244             onClick={linkEvent(this, this.prevPage)}
245           >
246             {i18n.t('prev')}
247           </button>
248         )}
249         <button
250           class="btn btn-sm btn-secondary"
251           onClick={linkEvent(this, this.nextPage)}
252         >
253           {i18n.t('next')}
254         </button>
255       </div>
256     );
257   }
258
259   nextPage(i: Inbox) {
260     i.state.page++;
261     i.setState(i.state);
262     i.refetch();
263   }
264
265   prevPage(i: Inbox) {
266     i.state.page--;
267     i.setState(i.state);
268     i.refetch();
269   }
270
271   handleUnreadOrAllChange(i: Inbox, event: any) {
272     i.state.unreadOrAll = Number(event.target.value);
273     i.state.page = 1;
274     i.setState(i.state);
275     i.refetch();
276   }
277
278   handleUnreadTypeChange(i: Inbox, event: any) {
279     i.state.unreadType = Number(event.target.value);
280     i.state.page = 1;
281     i.setState(i.state);
282     i.refetch();
283   }
284
285   refetch() {
286     let repliesForm: GetRepliesForm = {
287       sort: SortType[this.state.sort],
288       unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
289       page: this.state.page,
290       limit: fetchLimit,
291     };
292     WebSocketService.Instance.getReplies(repliesForm);
293
294     let userMentionsForm: GetUserMentionsForm = {
295       sort: SortType[this.state.sort],
296       unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
297       page: this.state.page,
298       limit: fetchLimit,
299     };
300     WebSocketService.Instance.getUserMentions(userMentionsForm);
301
302     let privateMessagesForm: GetPrivateMessagesForm = {
303       unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
304       page: this.state.page,
305       limit: fetchLimit,
306     };
307     WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
308   }
309
310   handleSortChange(val: SortType) {
311     this.state.sort = val;
312     this.state.page = 1;
313     this.setState(this.state);
314     this.refetch();
315   }
316
317   markAllAsRead() {
318     WebSocketService.Instance.markAllAsRead();
319   }
320
321   parseMessage(msg: WebSocketJsonResponse) {
322     console.log(msg);
323     let res = wsJsonToRes(msg);
324     if (msg.error) {
325       toast(i18n.t(msg.error), 'danger');
326       return;
327     } else if (msg.reconnect) {
328       this.refetch();
329     } else if (res.op == UserOperation.GetReplies) {
330       let data = res.data as GetRepliesResponse;
331       this.state.replies = data.replies;
332       this.sendUnreadCount();
333       window.scrollTo(0, 0);
334       this.setState(this.state);
335     } else if (res.op == UserOperation.GetUserMentions) {
336       let data = res.data as GetUserMentionsResponse;
337       this.state.mentions = data.mentions;
338       this.sendUnreadCount();
339       window.scrollTo(0, 0);
340       this.setState(this.state);
341     } else if (res.op == UserOperation.GetPrivateMessages) {
342       let data = res.data as PrivateMessagesResponse;
343       this.state.messages = data.messages;
344       this.sendUnreadCount();
345       window.scrollTo(0, 0);
346       this.setState(this.state);
347     } else if (res.op == UserOperation.EditPrivateMessage) {
348       let data = res.data as PrivateMessageResponse;
349       let found: PrivateMessageI = this.state.messages.find(
350         m => m.id === data.message.id
351       );
352       found.content = data.message.content;
353       found.updated = data.message.updated;
354       found.deleted = data.message.deleted;
355       // If youre in the unread view, just remove it from the list
356       if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) {
357         this.state.messages = this.state.messages.filter(
358           r => r.id !== data.message.id
359         );
360       } else {
361         let found = this.state.messages.find(c => c.id == data.message.id);
362         found.read = data.message.read;
363       }
364       this.sendUnreadCount();
365       window.scrollTo(0, 0);
366       this.setState(this.state);
367     } else if (res.op == UserOperation.MarkAllAsRead) {
368       this.state.replies = [];
369       this.state.mentions = [];
370       this.state.messages = [];
371       this.sendUnreadCount();
372       window.scrollTo(0, 0);
373       this.setState(this.state);
374     } else if (res.op == UserOperation.EditComment) {
375       let data = res.data as CommentResponse;
376       editCommentRes(data, this.state.replies);
377
378       // If youre in the unread view, just remove it from the list
379       if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
380         this.state.replies = this.state.replies.filter(
381           r => r.id !== data.comment.id
382         );
383       } else {
384         let found = this.state.replies.find(c => c.id == data.comment.id);
385         found.read = data.comment.read;
386       }
387       this.sendUnreadCount();
388       this.setState(this.state);
389     } else if (res.op == UserOperation.EditUserMention) {
390       let data = res.data as UserMentionResponse;
391
392       let found = this.state.mentions.find(c => c.id == data.mention.id);
393       found.content = data.mention.content;
394       found.updated = data.mention.updated;
395       found.removed = data.mention.removed;
396       found.deleted = data.mention.deleted;
397       found.upvotes = data.mention.upvotes;
398       found.downvotes = data.mention.downvotes;
399       found.score = data.mention.score;
400
401       // If youre in the unread view, just remove it from the list
402       if (this.state.unreadOrAll == UnreadOrAll.Unread && data.mention.read) {
403         this.state.mentions = this.state.mentions.filter(
404           r => r.id !== data.mention.id
405         );
406       } else {
407         let found = this.state.mentions.find(c => c.id == data.mention.id);
408         found.read = data.mention.read;
409       }
410       this.sendUnreadCount();
411       this.setState(this.state);
412     } else if (res.op == UserOperation.CreateComment) {
413       let data = res.data as CommentResponse;
414
415       if (data.recipient_ids.includes(UserService.Instance.user.id)) {
416         this.state.replies.unshift(data.comment);
417         this.setState(this.state);
418       } else if (data.comment.creator_id == UserService.Instance.user.id) {
419         toast(i18n.t('reply_sent'));
420       }
421       this.setState(this.state);
422     } else if (res.op == UserOperation.CreatePrivateMessage) {
423       let data = res.data as PrivateMessageResponse;
424       if (data.message.recipient_id == UserService.Instance.user.id) {
425         this.state.messages.unshift(data.message);
426         this.setState(this.state);
427       }
428     } else if (res.op == UserOperation.SaveComment) {
429       let data = res.data as CommentResponse;
430       saveCommentRes(data, this.state.replies);
431       this.setState(this.state);
432     } else if (res.op == UserOperation.CreateCommentLike) {
433       let data = res.data as CommentResponse;
434       createCommentLikeRes(data, this.state.replies);
435       this.setState(this.state);
436     }
437   }
438
439   sendUnreadCount() {
440     let count =
441       this.state.replies.filter(r => !r.read).length +
442       this.state.mentions.filter(r => !r.read).length +
443       this.state.messages.filter(
444         r => !r.read && r.creator_id !== UserService.Instance.user.id
445       ).length;
446     UserService.Instance.sub.next({
447       user: UserService.Instance.user,
448       unreadCount: count,
449     });
450   }
451 }