]> Untitled Git - lemmy-ui.git/blob - src/shared/components/inbox.tsx
Running newer prettier.
[lemmy-ui.git] / src / shared / components / inbox.tsx
1 import { Component, linkEvent } from "inferno";
2 import { Subscription } from "rxjs";
3 import {
4   UserOperation,
5   CommentView,
6   SortType,
7   GetReplies,
8   GetRepliesResponse,
9   GetUserMentions,
10   GetUserMentionsResponse,
11   UserMentionResponse,
12   CommentResponse,
13   PrivateMessageView,
14   GetPrivateMessages,
15   PrivateMessagesResponse,
16   PrivateMessageResponse,
17   SiteView,
18   UserMentionView,
19 } from "lemmy-js-client";
20 import { WebSocketService, UserService } from "../services";
21 import {
22   wsJsonToRes,
23   fetchLimit,
24   toast,
25   editCommentRes,
26   saveCommentRes,
27   createCommentLikeRes,
28   commentsToFlatNodes,
29   setupTippy,
30   setIsoData,
31   wsSubscribe,
32   isBrowser,
33   wsUserOp,
34   wsClient,
35   authField,
36 } from "../utils";
37 import { CommentNodes } from "./comment-nodes";
38 import { PrivateMessage } from "./private-message";
39 import { HtmlTags } from "./html-tags";
40 import { SortSelect } from "./sort-select";
41 import { Icon, Spinner } from "./icon";
42 import { i18n } from "../i18next";
43 import { InitialFetchRequest } from "shared/interfaces";
44
45 enum UnreadOrAll {
46   Unread,
47   All,
48 }
49
50 enum MessageType {
51   All,
52   Replies,
53   Mentions,
54   Messages,
55 }
56
57 enum ReplyEnum {
58   Reply,
59   Mention,
60   Message,
61 }
62 type ReplyType = {
63   id: number;
64   type_: ReplyEnum;
65   view: CommentView | PrivateMessageView | UserMentionView;
66   published: string;
67 };
68
69 interface InboxState {
70   unreadOrAll: UnreadOrAll;
71   messageType: MessageType;
72   replies: CommentView[];
73   mentions: UserMentionView[];
74   messages: PrivateMessageView[];
75   combined: ReplyType[];
76   sort: SortType;
77   page: number;
78   site_view: SiteView;
79   loading: boolean;
80 }
81
82 export class Inbox extends Component<any, InboxState> {
83   private isoData = setIsoData(this.context);
84   private subscription: Subscription;
85   private emptyState: InboxState = {
86     unreadOrAll: UnreadOrAll.Unread,
87     messageType: MessageType.All,
88     replies: [],
89     mentions: [],
90     messages: [],
91     combined: [],
92     sort: SortType.New,
93     page: 1,
94     site_view: this.isoData.site_res.site_view,
95     loading: true,
96   };
97
98   constructor(props: any, context: any) {
99     super(props, context);
100
101     this.state = this.emptyState;
102     this.handleSortChange = this.handleSortChange.bind(this);
103
104     if (!UserService.Instance.user && isBrowser()) {
105       toast(i18n.t("not_logged_in"), "danger");
106       this.context.router.history.push(`/login`);
107     }
108
109     this.parseMessage = this.parseMessage.bind(this);
110     this.subscription = wsSubscribe(this.parseMessage);
111
112     // Only fetch the data if coming from another route
113     if (this.isoData.path == this.context.router.route.match.url) {
114       this.state.replies = this.isoData.routeData[0].replies || [];
115       this.state.mentions = this.isoData.routeData[1].mentions || [];
116       this.state.messages = this.isoData.routeData[2].messages || [];
117       this.state.combined = this.buildCombined();
118       this.state.loading = false;
119     } else {
120       this.refetch();
121     }
122   }
123
124   componentWillUnmount() {
125     if (isBrowser()) {
126       this.subscription.unsubscribe();
127     }
128   }
129
130   get documentTitle(): string {
131     return `@${UserService.Instance.user.name} ${i18n.t("inbox")} - ${
132       this.state.site_view.site.name
133     }`;
134   }
135
136   render() {
137     return (
138       <div class="container">
139         {this.state.loading ? (
140           <h5>
141             <Spinner />
142           </h5>
143         ) : (
144           <div class="row">
145             <div class="col-12">
146               <HtmlTags
147                 title={this.documentTitle}
148                 path={this.context.router.route.match.url}
149               />
150               <h5 class="mb-1">
151                 {i18n.t("inbox")}
152                 <small>
153                   <a
154                     href={`/feeds/inbox/${UserService.Instance.auth}.xml`}
155                     title="RSS"
156                     rel="noopener"
157                   >
158                     <Icon icon="rss" classes="ml-2 text-muted small" />
159                   </a>
160                 </small>
161               </h5>
162               {this.state.replies.length +
163                 this.state.mentions.length +
164                 this.state.messages.length >
165                 0 &&
166                 this.state.unreadOrAll == UnreadOrAll.Unread && (
167                   <ul class="list-inline mb-1 text-muted small font-weight-bold">
168                     <li className="list-inline-item">
169                       <span
170                         class="pointer"
171                         role="button"
172                         onClick={linkEvent(this, this.markAllAsRead)}
173                       >
174                         {i18n.t("mark_all_as_read")}
175                       </span>
176                     </li>
177                   </ul>
178                 )}
179               {this.selects()}
180               {this.state.messageType == MessageType.All && this.all()}
181               {this.state.messageType == MessageType.Replies && this.replies()}
182               {this.state.messageType == MessageType.Mentions &&
183                 this.mentions()}
184               {this.state.messageType == MessageType.Messages &&
185                 this.messages()}
186               {this.paginator()}
187             </div>
188           </div>
189         )}
190       </div>
191     );
192   }
193
194   unreadOrAllRadios() {
195     return (
196       <div class="btn-group btn-group-toggle flex-wrap mb-2">
197         <label
198           className={`btn btn-outline-secondary pointer
199             ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
200           `}
201         >
202           <input
203             type="radio"
204             value={UnreadOrAll.Unread}
205             checked={this.state.unreadOrAll == UnreadOrAll.Unread}
206             onChange={linkEvent(this, this.handleUnreadOrAllChange)}
207           />
208           {i18n.t("unread")}
209         </label>
210         <label
211           className={`btn btn-outline-secondary pointer
212             ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
213           `}
214         >
215           <input
216             type="radio"
217             value={UnreadOrAll.All}
218             checked={this.state.unreadOrAll == UnreadOrAll.All}
219             onChange={linkEvent(this, this.handleUnreadOrAllChange)}
220           />
221           {i18n.t("all")}
222         </label>
223       </div>
224     );
225   }
226
227   messageTypeRadios() {
228     return (
229       <div class="btn-group btn-group-toggle flex-wrap mb-2">
230         <label
231           className={`btn btn-outline-secondary pointer
232             ${this.state.messageType == MessageType.All && "active"}
233           `}
234         >
235           <input
236             type="radio"
237             value={MessageType.All}
238             checked={this.state.messageType == MessageType.All}
239             onChange={linkEvent(this, this.handleMessageTypeChange)}
240           />
241           {i18n.t("all")}
242         </label>
243         <label
244           className={`btn btn-outline-secondary pointer
245             ${this.state.messageType == MessageType.Replies && "active"}
246           `}
247         >
248           <input
249             type="radio"
250             value={MessageType.Replies}
251             checked={this.state.messageType == MessageType.Replies}
252             onChange={linkEvent(this, this.handleMessageTypeChange)}
253           />
254           {i18n.t("replies")}
255         </label>
256         <label
257           className={`btn btn-outline-secondary pointer
258             ${this.state.messageType == MessageType.Mentions && "active"}
259           `}
260         >
261           <input
262             type="radio"
263             value={MessageType.Mentions}
264             checked={this.state.messageType == MessageType.Mentions}
265             onChange={linkEvent(this, this.handleMessageTypeChange)}
266           />
267           {i18n.t("mentions")}
268         </label>
269         <label
270           className={`btn btn-outline-secondary pointer
271             ${this.state.messageType == MessageType.Messages && "active"}
272           `}
273         >
274           <input
275             type="radio"
276             value={MessageType.Messages}
277             checked={this.state.messageType == MessageType.Messages}
278             onChange={linkEvent(this, this.handleMessageTypeChange)}
279           />
280           {i18n.t("messages")}
281         </label>
282       </div>
283     );
284   }
285
286   selects() {
287     return (
288       <div className="mb-2">
289         <span class="mr-3">{this.unreadOrAllRadios()}</span>
290         <span class="mr-3">{this.messageTypeRadios()}</span>
291         <SortSelect
292           sort={this.state.sort}
293           onChange={this.handleSortChange}
294           hideHot
295           hideMostComments
296         />
297       </div>
298     );
299   }
300
301   replyToReplyType(r: CommentView): ReplyType {
302     return {
303       id: r.comment.id,
304       type_: ReplyEnum.Reply,
305       view: r,
306       published: r.comment.published,
307     };
308   }
309
310   mentionToReplyType(r: UserMentionView): ReplyType {
311     return {
312       id: r.user_mention.id,
313       type_: ReplyEnum.Mention,
314       view: r,
315       published: r.comment.published,
316     };
317   }
318
319   messageToReplyType(r: PrivateMessageView): ReplyType {
320     return {
321       id: r.private_message.id,
322       type_: ReplyEnum.Message,
323       view: r,
324       published: r.private_message.published,
325     };
326   }
327
328   buildCombined(): ReplyType[] {
329     let replies: ReplyType[] = this.state.replies.map(r =>
330       this.replyToReplyType(r)
331     );
332     let mentions: ReplyType[] = this.state.mentions.map(r =>
333       this.mentionToReplyType(r)
334     );
335     let messages: ReplyType[] = this.state.messages.map(r =>
336       this.messageToReplyType(r)
337     );
338
339     return [...replies, ...mentions, ...messages].sort((a, b) =>
340       b.published.localeCompare(a.published)
341     );
342   }
343
344   renderReplyType(i: ReplyType) {
345     switch (i.type_) {
346       case ReplyEnum.Reply:
347         return (
348           <CommentNodes
349             key={i.id}
350             nodes={[{ comment_view: i.view as CommentView }]}
351             noIndent
352             markable
353             showCommunity
354             showContext
355             enableDownvotes={this.state.site_view.site.enable_downvotes}
356           />
357         );
358       case ReplyEnum.Mention:
359         return (
360           <CommentNodes
361             key={i.id}
362             nodes={[{ comment_view: i.view as UserMentionView }]}
363             noIndent
364             markable
365             showCommunity
366             showContext
367             enableDownvotes={this.state.site_view.site.enable_downvotes}
368           />
369         );
370       case ReplyEnum.Message:
371         return (
372           <PrivateMessage
373             key={i.id}
374             private_message_view={i.view as PrivateMessageView}
375           />
376         );
377       default:
378         return <div />;
379     }
380   }
381
382   all() {
383     return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
384   }
385
386   replies() {
387     return (
388       <div>
389         <CommentNodes
390           nodes={commentsToFlatNodes(this.state.replies)}
391           noIndent
392           markable
393           showCommunity
394           showContext
395           enableDownvotes={this.state.site_view.site.enable_downvotes}
396         />
397       </div>
398     );
399   }
400
401   mentions() {
402     return (
403       <div>
404         {this.state.mentions.map(umv => (
405           <CommentNodes
406             key={umv.user_mention.id}
407             nodes={[{ comment_view: umv }]}
408             noIndent
409             markable
410             showCommunity
411             showContext
412             enableDownvotes={this.state.site_view.site.enable_downvotes}
413           />
414         ))}
415       </div>
416     );
417   }
418
419   messages() {
420     return (
421       <div>
422         {this.state.messages.map(pmv => (
423           <PrivateMessage
424             key={pmv.private_message.id}
425             private_message_view={pmv}
426           />
427         ))}
428       </div>
429     );
430   }
431
432   paginator() {
433     return (
434       <div class="mt-2">
435         {this.state.page > 1 && (
436           <button
437             class="btn btn-secondary mr-1"
438             onClick={linkEvent(this, this.prevPage)}
439           >
440             {i18n.t("prev")}
441           </button>
442         )}
443         {this.unreadCount() > 0 && (
444           <button
445             class="btn btn-secondary"
446             onClick={linkEvent(this, this.nextPage)}
447           >
448             {i18n.t("next")}
449           </button>
450         )}
451       </div>
452     );
453   }
454
455   nextPage(i: Inbox) {
456     i.state.page++;
457     i.setState(i.state);
458     i.refetch();
459   }
460
461   prevPage(i: Inbox) {
462     i.state.page--;
463     i.setState(i.state);
464     i.refetch();
465   }
466
467   handleUnreadOrAllChange(i: Inbox, event: any) {
468     i.state.unreadOrAll = Number(event.target.value);
469     i.state.page = 1;
470     i.setState(i.state);
471     i.refetch();
472   }
473
474   handleMessageTypeChange(i: Inbox, event: any) {
475     i.state.messageType = Number(event.target.value);
476     i.state.page = 1;
477     i.setState(i.state);
478     i.refetch();
479   }
480
481   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
482     let promises: Promise<any>[] = [];
483
484     // It can be /u/me, or /username/1
485     let repliesForm: GetReplies = {
486       sort: SortType.New,
487       unread_only: true,
488       page: 1,
489       limit: fetchLimit,
490       auth: req.auth,
491     };
492     promises.push(req.client.getReplies(repliesForm));
493
494     let userMentionsForm: GetUserMentions = {
495       sort: SortType.New,
496       unread_only: true,
497       page: 1,
498       limit: fetchLimit,
499       auth: req.auth,
500     };
501     promises.push(req.client.getUserMentions(userMentionsForm));
502
503     let privateMessagesForm: GetPrivateMessages = {
504       unread_only: true,
505       page: 1,
506       limit: fetchLimit,
507       auth: req.auth,
508     };
509     promises.push(req.client.getPrivateMessages(privateMessagesForm));
510
511     return promises;
512   }
513
514   refetch() {
515     let repliesForm: GetReplies = {
516       sort: this.state.sort,
517       unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
518       page: this.state.page,
519       limit: fetchLimit,
520       auth: authField(),
521     };
522     WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
523
524     let userMentionsForm: GetUserMentions = {
525       sort: this.state.sort,
526       unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
527       page: this.state.page,
528       limit: fetchLimit,
529       auth: authField(),
530     };
531     WebSocketService.Instance.send(wsClient.getUserMentions(userMentionsForm));
532
533     let privateMessagesForm: GetPrivateMessages = {
534       unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
535       page: this.state.page,
536       limit: fetchLimit,
537       auth: authField(),
538     };
539     WebSocketService.Instance.send(
540       wsClient.getPrivateMessages(privateMessagesForm)
541     );
542   }
543
544   handleSortChange(val: SortType) {
545     this.state.sort = val;
546     this.state.page = 1;
547     this.setState(this.state);
548     this.refetch();
549   }
550
551   markAllAsRead(i: Inbox) {
552     WebSocketService.Instance.send(
553       wsClient.markAllAsRead({
554         auth: authField(),
555       })
556     );
557     i.state.replies = [];
558     i.state.mentions = [];
559     i.state.messages = [];
560     i.sendUnreadCount();
561     window.scrollTo(0, 0);
562     i.setState(i.state);
563   }
564
565   parseMessage(msg: any) {
566     let op = wsUserOp(msg);
567     console.log(msg);
568     if (msg.error) {
569       toast(i18n.t(msg.error), "danger");
570       return;
571     } else if (msg.reconnect) {
572       this.refetch();
573     } else if (op == UserOperation.GetReplies) {
574       let data = wsJsonToRes<GetRepliesResponse>(msg).data;
575       this.state.replies = data.replies;
576       this.state.combined = this.buildCombined();
577       this.state.loading = false;
578       this.sendUnreadCount();
579       window.scrollTo(0, 0);
580       this.setState(this.state);
581       setupTippy();
582     } else if (op == UserOperation.GetUserMentions) {
583       let data = wsJsonToRes<GetUserMentionsResponse>(msg).data;
584       this.state.mentions = data.mentions;
585       this.state.combined = this.buildCombined();
586       this.sendUnreadCount();
587       window.scrollTo(0, 0);
588       this.setState(this.state);
589       setupTippy();
590     } else if (op == UserOperation.GetPrivateMessages) {
591       let data = wsJsonToRes<PrivateMessagesResponse>(msg).data;
592       this.state.messages = data.private_messages;
593       this.state.combined = this.buildCombined();
594       this.sendUnreadCount();
595       window.scrollTo(0, 0);
596       this.setState(this.state);
597       setupTippy();
598     } else if (op == UserOperation.EditPrivateMessage) {
599       let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
600       let found: PrivateMessageView = this.state.messages.find(
601         m =>
602           m.private_message.id === data.private_message_view.private_message.id
603       );
604       if (found) {
605         let combinedView = this.state.combined.find(
606           i => i.id == data.private_message_view.private_message.id
607         ).view as PrivateMessageView;
608         found.private_message.content = combinedView.private_message.content =
609           data.private_message_view.private_message.content;
610         found.private_message.updated = combinedView.private_message.updated =
611           data.private_message_view.private_message.updated;
612       }
613       this.setState(this.state);
614     } else if (op == UserOperation.DeletePrivateMessage) {
615       let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
616       let found: PrivateMessageView = this.state.messages.find(
617         m =>
618           m.private_message.id === data.private_message_view.private_message.id
619       );
620       if (found) {
621         let combinedView = this.state.combined.find(
622           i => i.id == data.private_message_view.private_message.id
623         ).view as PrivateMessageView;
624         found.private_message.deleted = combinedView.private_message.deleted =
625           data.private_message_view.private_message.deleted;
626         found.private_message.updated = combinedView.private_message.updated =
627           data.private_message_view.private_message.updated;
628       }
629       this.setState(this.state);
630     } else if (op == UserOperation.MarkPrivateMessageAsRead) {
631       let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
632       let found: PrivateMessageView = this.state.messages.find(
633         m =>
634           m.private_message.id === data.private_message_view.private_message.id
635       );
636
637       if (found) {
638         let combinedView = this.state.combined.find(
639           i => i.id == data.private_message_view.private_message.id
640         ).view as PrivateMessageView;
641         found.private_message.updated = combinedView.private_message.updated =
642           data.private_message_view.private_message.updated;
643
644         // If youre in the unread view, just remove it from the list
645         if (
646           this.state.unreadOrAll == UnreadOrAll.Unread &&
647           data.private_message_view.private_message.read
648         ) {
649           this.state.messages = this.state.messages.filter(
650             r =>
651               r.private_message.id !==
652               data.private_message_view.private_message.id
653           );
654           this.state.combined = this.state.combined.filter(
655             r => r.id !== data.private_message_view.private_message.id
656           );
657         } else {
658           found.private_message.read = combinedView.private_message.read =
659             data.private_message_view.private_message.read;
660         }
661       }
662       this.sendUnreadCount();
663       this.setState(this.state);
664     } else if (op == UserOperation.MarkAllAsRead) {
665       // Moved to be instant
666     } else if (
667       op == UserOperation.EditComment ||
668       op == UserOperation.DeleteComment ||
669       op == UserOperation.RemoveComment
670     ) {
671       let data = wsJsonToRes<CommentResponse>(msg).data;
672       editCommentRes(data.comment_view, this.state.replies);
673       this.setState(this.state);
674     } else if (op == UserOperation.MarkCommentAsRead) {
675       let data = wsJsonToRes<CommentResponse>(msg).data;
676
677       // If youre in the unread view, just remove it from the list
678       if (
679         this.state.unreadOrAll == UnreadOrAll.Unread &&
680         data.comment_view.comment.read
681       ) {
682         this.state.replies = this.state.replies.filter(
683           r => r.comment.id !== data.comment_view.comment.id
684         );
685         this.state.combined = this.state.combined.filter(
686           r => r.id !== data.comment_view.comment.id
687         );
688       } else {
689         let found = this.state.replies.find(
690           c => c.comment.id == data.comment_view.comment.id
691         );
692         let combinedView = this.state.combined.find(
693           i => i.id == data.comment_view.comment.id
694         ).view as CommentView;
695         found.comment.read = combinedView.comment.read =
696           data.comment_view.comment.read;
697       }
698       this.sendUnreadCount();
699       this.setState(this.state);
700       setupTippy();
701     } else if (op == UserOperation.MarkUserMentionAsRead) {
702       let data = wsJsonToRes<UserMentionResponse>(msg).data;
703
704       // TODO this might not be correct, it might need to use the comment id
705       let found = this.state.mentions.find(
706         c => c.user_mention.id == data.user_mention_view.user_mention.id
707       );
708
709       if (found) {
710         let combinedView = this.state.combined.find(
711           i => i.id == data.user_mention_view.user_mention.id
712         ).view as UserMentionView;
713         found.comment.content = combinedView.comment.content =
714           data.user_mention_view.comment.content;
715         found.comment.updated = combinedView.comment.updated =
716           data.user_mention_view.comment.updated;
717         found.comment.removed = combinedView.comment.removed =
718           data.user_mention_view.comment.removed;
719         found.comment.deleted = combinedView.comment.deleted =
720           data.user_mention_view.comment.deleted;
721         found.counts.upvotes = combinedView.counts.upvotes =
722           data.user_mention_view.counts.upvotes;
723         found.counts.downvotes = combinedView.counts.downvotes =
724           data.user_mention_view.counts.downvotes;
725         found.counts.score = combinedView.counts.score =
726           data.user_mention_view.counts.score;
727
728         // If youre in the unread view, just remove it from the list
729         if (
730           this.state.unreadOrAll == UnreadOrAll.Unread &&
731           data.user_mention_view.user_mention.read
732         ) {
733           this.state.mentions = this.state.mentions.filter(
734             r => r.user_mention.id !== data.user_mention_view.user_mention.id
735           );
736           this.state.combined = this.state.combined.filter(
737             r => r.id !== data.user_mention_view.user_mention.id
738           );
739         } else {
740           // TODO test to make sure these mentions are getting marked as read
741           found.user_mention.read = combinedView.user_mention.read =
742             data.user_mention_view.user_mention.read;
743         }
744       }
745       this.sendUnreadCount();
746       this.setState(this.state);
747     } else if (op == UserOperation.CreateComment) {
748       let data = wsJsonToRes<CommentResponse>(msg).data;
749
750       if (data.recipient_ids.includes(UserService.Instance.user.id)) {
751         this.state.replies.unshift(data.comment_view);
752         this.state.combined.unshift(this.replyToReplyType(data.comment_view));
753         this.setState(this.state);
754       } else if (data.comment_view.creator.id == UserService.Instance.user.id) {
755         // TODO this seems wrong, you should be using form_id
756         toast(i18n.t("reply_sent"));
757       }
758     } else if (op == UserOperation.CreatePrivateMessage) {
759       let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
760       if (
761         data.private_message_view.recipient.id == UserService.Instance.user.id
762       ) {
763         this.state.messages.unshift(data.private_message_view);
764         this.state.combined.unshift(
765           this.messageToReplyType(data.private_message_view)
766         );
767         this.setState(this.state);
768       }
769     } else if (op == UserOperation.SaveComment) {
770       let data = wsJsonToRes<CommentResponse>(msg).data;
771       saveCommentRes(data.comment_view, this.state.replies);
772       this.setState(this.state);
773       setupTippy();
774     } else if (op == UserOperation.CreateCommentLike) {
775       let data = wsJsonToRes<CommentResponse>(msg).data;
776       createCommentLikeRes(data.comment_view, this.state.replies);
777       this.setState(this.state);
778     }
779   }
780
781   sendUnreadCount() {
782     UserService.Instance.unreadCountSub.next(this.unreadCount());
783   }
784
785   unreadCount(): number {
786     return (
787       this.state.replies.filter(r => !r.comment.read).length +
788       this.state.mentions.filter(r => !r.user_mention.read).length +
789       this.state.messages.filter(
790         r =>
791           UserService.Instance.user &&
792           !r.private_message.read &&
793           // TODO also seems very strang and wrong
794           r.creator.id !== UserService.Instance.user.id
795       ).length
796     );
797   }
798 }