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