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