1 import { Component, linkEvent } from "inferno";
8 GetPersonMentionsResponse,
12 PersonMentionResponse,
15 PrivateMessageResponse,
16 PrivateMessagesResponse,
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";
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";
70 view: CommentView | PrivateMessageView | PersonMentionView;
74 interface InboxState {
75 unreadOrAll: UnreadOrAll;
76 messageType: MessageType;
77 replies: CommentView[];
78 mentions: PersonMentionView[];
79 messages: PrivateMessageView[];
80 combined: ReplyType[];
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,
99 site_view: this.isoData.site_res.site_view,
103 constructor(props: any, context: any) {
104 super(props, context);
106 this.state = this.emptyState;
107 this.handleSortChange = this.handleSortChange.bind(this);
108 this.handlePageChange = this.handlePageChange.bind(this);
110 if (!UserService.Instance.myUserInfo && isBrowser()) {
111 toast(i18n.t("not_logged_in"), "danger");
112 this.context.router.history.push(`/login`);
115 this.parseMessage = this.parseMessage.bind(this);
116 this.subscription = wsSubscribe(this.parseMessage);
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;
130 componentWillUnmount() {
132 this.subscription.unsubscribe();
136 get documentTitle(): string {
138 UserService.Instance.myUserInfo.local_user_view.person.name
139 } ${i18n.t("inbox")} - ${this.state.site_view.site.name}`;
144 <div class="container">
145 {this.state.loading ? (
153 title={this.documentTitle}
154 path={this.context.router.route.match.url}
160 href={`/feeds/inbox/${UserService.Instance.auth}.xml`}
164 <Icon icon="rss" classes="ml-2 text-muted small" />
168 {this.state.replies.length +
169 this.state.mentions.length +
170 this.state.messages.length >
172 this.state.unreadOrAll == UnreadOrAll.Unread && (
174 class="btn btn-secondary mb-2"
175 onClick={linkEvent(this, this.markAllAsRead)}
177 {i18n.t("mark_all_as_read")}
181 {this.state.messageType == MessageType.All && this.all()}
182 {this.state.messageType == MessageType.Replies && this.replies()}
183 {this.state.messageType == MessageType.Mentions &&
185 {this.state.messageType == MessageType.Messages &&
188 page={this.state.page}
189 onChange={this.handlePageChange}
198 unreadOrAllRadios() {
200 <div class="btn-group btn-group-toggle flex-wrap mb-2">
202 className={`btn btn-outline-secondary pointer
203 ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
208 value={UnreadOrAll.Unread}
209 checked={this.state.unreadOrAll == UnreadOrAll.Unread}
210 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
215 className={`btn btn-outline-secondary pointer
216 ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
221 value={UnreadOrAll.All}
222 checked={this.state.unreadOrAll == UnreadOrAll.All}
223 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
231 messageTypeRadios() {
233 <div class="btn-group btn-group-toggle flex-wrap mb-2">
235 className={`btn btn-outline-secondary pointer
236 ${this.state.messageType == MessageType.All && "active"}
241 value={MessageType.All}
242 checked={this.state.messageType == MessageType.All}
243 onChange={linkEvent(this, this.handleMessageTypeChange)}
248 className={`btn btn-outline-secondary pointer
249 ${this.state.messageType == MessageType.Replies && "active"}
254 value={MessageType.Replies}
255 checked={this.state.messageType == MessageType.Replies}
256 onChange={linkEvent(this, this.handleMessageTypeChange)}
261 className={`btn btn-outline-secondary pointer
262 ${this.state.messageType == MessageType.Mentions && "active"}
267 value={MessageType.Mentions}
268 checked={this.state.messageType == MessageType.Mentions}
269 onChange={linkEvent(this, this.handleMessageTypeChange)}
274 className={`btn btn-outline-secondary pointer
275 ${this.state.messageType == MessageType.Messages && "active"}
280 value={MessageType.Messages}
281 checked={this.state.messageType == MessageType.Messages}
282 onChange={linkEvent(this, this.handleMessageTypeChange)}
292 <div className="mb-2">
293 <span class="mr-3">{this.unreadOrAllRadios()}</span>
294 <span class="mr-3">{this.messageTypeRadios()}</span>
296 sort={this.state.sort}
297 onChange={this.handleSortChange}
305 replyToReplyType(r: CommentView): ReplyType {
308 type_: ReplyEnum.Reply,
310 published: r.comment.published,
314 mentionToReplyType(r: PersonMentionView): ReplyType {
316 id: r.person_mention.id,
317 type_: ReplyEnum.Mention,
319 published: r.comment.published,
323 messageToReplyType(r: PrivateMessageView): ReplyType {
325 id: r.private_message.id,
326 type_: ReplyEnum.Message,
328 published: r.private_message.published,
332 buildCombined(): ReplyType[] {
333 let replies: ReplyType[] = this.state.replies.map(r =>
334 this.replyToReplyType(r)
336 let mentions: ReplyType[] = this.state.mentions.map(r =>
337 this.mentionToReplyType(r)
339 let messages: ReplyType[] = this.state.messages.map(r =>
340 this.messageToReplyType(r)
343 return [...replies, ...mentions, ...messages].sort((a, b) =>
344 b.published.localeCompare(a.published)
348 renderReplyType(i: ReplyType) {
350 case ReplyEnum.Reply:
354 nodes={[{ comment_view: i.view as CommentView }]}
359 enableDownvotes={this.state.site_view.site.enable_downvotes}
362 case ReplyEnum.Mention:
366 nodes={[{ comment_view: i.view as PersonMentionView }]}
371 enableDownvotes={this.state.site_view.site.enable_downvotes}
374 case ReplyEnum.Message:
378 private_message_view={i.view as PrivateMessageView}
387 return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
394 nodes={commentsToFlatNodes(this.state.replies)}
399 enableDownvotes={this.state.site_view.site.enable_downvotes}
408 {this.state.mentions.map(umv => (
410 key={umv.person_mention.id}
411 nodes={[{ comment_view: umv }]}
416 enableDownvotes={this.state.site_view.site.enable_downvotes}
426 {this.state.messages.map(pmv => (
428 key={pmv.private_message.id}
429 private_message_view={pmv}
436 handlePageChange(page: number) {
437 this.setState({ page });
441 handleUnreadOrAllChange(i: Inbox, event: any) {
442 i.state.unreadOrAll = Number(event.target.value);
448 handleMessageTypeChange(i: Inbox, event: any) {
449 i.state.messageType = Number(event.target.value);
455 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
456 let promises: Promise<any>[] = [];
458 // It can be /u/me, or /username/1
459 let repliesForm: GetReplies = {
466 promises.push(req.client.getReplies(repliesForm));
468 let personMentionsForm: GetPersonMentions = {
475 promises.push(req.client.getPersonMentions(personMentionsForm));
477 let privateMessagesForm: GetPrivateMessages = {
483 promises.push(req.client.getPrivateMessages(privateMessagesForm));
489 let repliesForm: GetReplies = {
490 sort: this.state.sort,
491 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
492 page: this.state.page,
496 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
498 let personMentionsForm: GetPersonMentions = {
499 sort: this.state.sort,
500 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
501 page: this.state.page,
505 WebSocketService.Instance.send(
506 wsClient.getPersonMentions(personMentionsForm)
509 let privateMessagesForm: GetPrivateMessages = {
510 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
511 page: this.state.page,
515 WebSocketService.Instance.send(
516 wsClient.getPrivateMessages(privateMessagesForm)
520 handleSortChange(val: SortType) {
521 this.state.sort = val;
523 this.setState(this.state);
527 markAllAsRead(i: Inbox) {
528 WebSocketService.Instance.send(
529 wsClient.markAllAsRead({
533 i.state.replies = [];
534 i.state.mentions = [];
535 i.state.messages = [];
537 window.scrollTo(0, 0);
541 parseMessage(msg: any) {
542 let op = wsUserOp(msg);
545 toast(i18n.t(msg.error), "danger");
547 } else if (msg.reconnect) {
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);
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);
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);
574 } else if (op == UserOperation.EditPrivateMessage) {
575 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
576 let found: PrivateMessageView = this.state.messages.find(
578 m.private_message.id === data.private_message_view.private_message.id
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;
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(
594 m.private_message.id === data.private_message_view.private_message.id
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;
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(
610 m.private_message.id === data.private_message_view.private_message.id
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;
620 // If youre in the unread view, just remove it from the list
622 this.state.unreadOrAll == UnreadOrAll.Unread &&
623 data.private_message_view.private_message.read
625 this.state.messages = this.state.messages.filter(
627 r.private_message.id !==
628 data.private_message_view.private_message.id
630 this.state.combined = this.state.combined.filter(
631 r => r.id !== data.private_message_view.private_message.id
634 found.private_message.read = combinedView.private_message.read =
635 data.private_message_view.private_message.read;
638 this.sendUnreadCount();
639 this.setState(this.state);
640 } else if (op == UserOperation.MarkAllAsRead) {
641 // Moved to be instant
643 op == UserOperation.EditComment ||
644 op == UserOperation.DeleteComment ||
645 op == UserOperation.RemoveComment
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;
653 // If youre in the unread view, just remove it from the list
655 this.state.unreadOrAll == UnreadOrAll.Unread &&
656 data.comment_view.comment.read
658 this.state.replies = this.state.replies.filter(
659 r => r.comment.id !== data.comment_view.comment.id
661 this.state.combined = this.state.combined.filter(
662 r => r.id !== data.comment_view.comment.id
665 let found = this.state.replies.find(
666 c => c.comment.id == data.comment_view.comment.id
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;
674 this.sendUnreadCount();
675 this.setState(this.state);
677 } else if (op == UserOperation.MarkPersonMentionAsRead) {
678 let data = wsJsonToRes<PersonMentionResponse>(msg).data;
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
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;
704 // If youre in the unread view, just remove it from the list
706 this.state.unreadOrAll == UnreadOrAll.Unread &&
707 data.person_mention_view.person_mention.read
709 this.state.mentions = this.state.mentions.filter(
711 r.person_mention.id !== data.person_mention_view.person_mention.id
713 this.state.combined = this.state.combined.filter(
714 r => r.id !== data.person_mention_view.person_mention.id
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;
722 this.sendUnreadCount();
723 this.setState(this.state);
724 } else if (op == UserOperation.CreateComment) {
725 let data = wsJsonToRes<CommentResponse>(msg).data;
728 data.recipient_ids.includes(
729 UserService.Instance.myUserInfo.local_user_view.local_user.id
732 this.state.replies.unshift(data.comment_view);
733 this.state.combined.unshift(this.replyToReplyType(data.comment_view));
734 this.setState(this.state);
736 data.comment_view.creator.id ==
737 UserService.Instance.myUserInfo.local_user_view.person.id
739 // If youre in the unread view, just remove it from the list
740 if (this.state.unreadOrAll == UnreadOrAll.Unread) {
741 this.state.replies = this.state.replies.filter(
742 r => r.comment.id !== data.comment_view.comment.parent_id
744 this.state.mentions = this.state.mentions.filter(
745 m => m.comment.id !== data.comment_view.comment.parent_id
747 this.state.combined = this.state.combined.filter(r => {
748 if (this.isMention(r.view))
749 return r.view.comment.id !== data.comment_view.comment.parent_id;
750 else return r.id !== data.comment_view.comment.parent_id;
753 let mention_found = this.state.mentions.find(
754 i => i.comment.id == data.comment_view.comment.parent_id
757 mention_found.person_mention.read = true;
759 let reply_found = this.state.replies.find(
760 i => i.comment.id == data.comment_view.comment.parent_id
763 reply_found.comment.read = true;
765 this.state.combined = this.buildCombined();
767 this.sendUnreadCount();
768 this.setState(this.state);
770 // TODO this seems wrong, you should be using form_id
771 toast(i18n.t("reply_sent"));
773 } else if (op == UserOperation.CreatePrivateMessage) {
774 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
776 data.private_message_view.recipient.id ==
777 UserService.Instance.myUserInfo.local_user_view.person.id
779 this.state.messages.unshift(data.private_message_view);
780 this.state.combined.unshift(
781 this.messageToReplyType(data.private_message_view)
783 this.setState(this.state);
785 } else if (op == UserOperation.SaveComment) {
786 let data = wsJsonToRes<CommentResponse>(msg).data;
787 saveCommentRes(data.comment_view, this.state.replies);
788 this.setState(this.state);
790 } else if (op == UserOperation.CreateCommentLike) {
791 let data = wsJsonToRes<CommentResponse>(msg).data;
792 createCommentLikeRes(data.comment_view, this.state.replies);
793 this.setState(this.state);
794 } else if (op == UserOperation.BlockPerson) {
795 let data = wsJsonToRes<BlockPersonResponse>(msg).data;
796 updatePersonBlock(data);
797 } else if (op == UserOperation.CreatePostReport) {
798 let data = wsJsonToRes<PostReportResponse>(msg).data;
800 toast(i18n.t("report_created"));
802 } else if (op == UserOperation.CreateCommentReport) {
803 let data = wsJsonToRes<CommentReportResponse>(msg).data;
805 toast(i18n.t("report_created"));
811 UserService.Instance.unreadInboxCountSub.next(this.unreadCount());
814 unreadCount(): number {
816 this.state.replies.filter(r => !r.comment.read).length +
817 this.state.mentions.filter(r => !r.person_mention.read).length +
818 this.state.messages.filter(
820 UserService.Instance.myUserInfo &&
821 !r.private_message.read &&
822 // TODO also seems very strange and wrong
824 UserService.Instance.myUserInfo.local_user_view.person.id
829 isMention(view: any): view is PersonMentionView {
830 return (view as PersonMentionView).person_mention !== undefined;