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