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