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