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