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