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}`;
143 let inboxRss = `/feeds/inbox/${UserService.Instance.auth}.xml`;
145 <div class="container">
146 {this.state.loading ? (
154 title={this.documentTitle}
155 path={this.context.router.route.match.url}
160 <a href={inboxRss} title="RSS" rel="noopener">
161 <Icon icon="rss" classes="ml-2 text-muted small" />
165 type="application/atom+xml"
170 {this.state.replies.length +
171 this.state.mentions.length +
172 this.state.messages.length >
174 this.state.unreadOrAll == UnreadOrAll.Unread && (
176 class="btn btn-secondary mb-2"
177 onClick={linkEvent(this, this.markAllAsRead)}
179 {i18n.t("mark_all_as_read")}
183 {this.state.messageType == MessageType.All && this.all()}
184 {this.state.messageType == MessageType.Replies && this.replies()}
185 {this.state.messageType == MessageType.Mentions &&
187 {this.state.messageType == MessageType.Messages &&
190 page={this.state.page}
191 onChange={this.handlePageChange}
200 unreadOrAllRadios() {
202 <div class="btn-group btn-group-toggle flex-wrap mb-2">
204 className={`btn btn-outline-secondary pointer
205 ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
210 value={UnreadOrAll.Unread}
211 checked={this.state.unreadOrAll == UnreadOrAll.Unread}
212 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
217 className={`btn btn-outline-secondary pointer
218 ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
223 value={UnreadOrAll.All}
224 checked={this.state.unreadOrAll == UnreadOrAll.All}
225 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
233 messageTypeRadios() {
235 <div class="btn-group btn-group-toggle flex-wrap mb-2">
237 className={`btn btn-outline-secondary pointer
238 ${this.state.messageType == MessageType.All && "active"}
243 value={MessageType.All}
244 checked={this.state.messageType == MessageType.All}
245 onChange={linkEvent(this, this.handleMessageTypeChange)}
250 className={`btn btn-outline-secondary pointer
251 ${this.state.messageType == MessageType.Replies && "active"}
256 value={MessageType.Replies}
257 checked={this.state.messageType == MessageType.Replies}
258 onChange={linkEvent(this, this.handleMessageTypeChange)}
263 className={`btn btn-outline-secondary pointer
264 ${this.state.messageType == MessageType.Mentions && "active"}
269 value={MessageType.Mentions}
270 checked={this.state.messageType == MessageType.Mentions}
271 onChange={linkEvent(this, this.handleMessageTypeChange)}
276 className={`btn btn-outline-secondary pointer
277 ${this.state.messageType == MessageType.Messages && "active"}
282 value={MessageType.Messages}
283 checked={this.state.messageType == MessageType.Messages}
284 onChange={linkEvent(this, this.handleMessageTypeChange)}
294 <div className="mb-2">
295 <span class="mr-3">{this.unreadOrAllRadios()}</span>
296 <span class="mr-3">{this.messageTypeRadios()}</span>
298 sort={this.state.sort}
299 onChange={this.handleSortChange}
307 replyToReplyType(r: CommentView): ReplyType {
310 type_: ReplyEnum.Reply,
312 published: r.comment.published,
316 mentionToReplyType(r: PersonMentionView): ReplyType {
318 id: r.person_mention.id,
319 type_: ReplyEnum.Mention,
321 published: r.comment.published,
325 messageToReplyType(r: PrivateMessageView): ReplyType {
327 id: r.private_message.id,
328 type_: ReplyEnum.Message,
330 published: r.private_message.published,
334 buildCombined(): ReplyType[] {
335 let replies: ReplyType[] = this.state.replies.map(r =>
336 this.replyToReplyType(r)
338 let mentions: ReplyType[] = this.state.mentions.map(r =>
339 this.mentionToReplyType(r)
341 let messages: ReplyType[] = this.state.messages.map(r =>
342 this.messageToReplyType(r)
345 return [...replies, ...mentions, ...messages].sort((a, b) =>
346 b.published.localeCompare(a.published)
350 renderReplyType(i: ReplyType) {
352 case ReplyEnum.Reply:
356 nodes={[{ comment_view: i.view as CommentView }]}
361 enableDownvotes={this.state.site_view.site.enable_downvotes}
364 case ReplyEnum.Mention:
368 nodes={[{ comment_view: i.view as PersonMentionView }]}
373 enableDownvotes={this.state.site_view.site.enable_downvotes}
376 case ReplyEnum.Message:
380 private_message_view={i.view as PrivateMessageView}
389 return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
396 nodes={commentsToFlatNodes(this.state.replies)}
401 enableDownvotes={this.state.site_view.site.enable_downvotes}
410 {this.state.mentions.map(umv => (
412 key={umv.person_mention.id}
413 nodes={[{ comment_view: umv }]}
418 enableDownvotes={this.state.site_view.site.enable_downvotes}
428 {this.state.messages.map(pmv => (
430 key={pmv.private_message.id}
431 private_message_view={pmv}
438 handlePageChange(page: number) {
439 this.setState({ page });
443 handleUnreadOrAllChange(i: Inbox, event: any) {
444 i.state.unreadOrAll = Number(event.target.value);
450 handleMessageTypeChange(i: Inbox, event: any) {
451 i.state.messageType = Number(event.target.value);
457 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
458 let promises: Promise<any>[] = [];
460 // It can be /u/me, or /username/1
461 let repliesForm: GetReplies = {
468 promises.push(req.client.getReplies(repliesForm));
470 let personMentionsForm: GetPersonMentions = {
477 promises.push(req.client.getPersonMentions(personMentionsForm));
479 let privateMessagesForm: GetPrivateMessages = {
485 promises.push(req.client.getPrivateMessages(privateMessagesForm));
491 let repliesForm: GetReplies = {
492 sort: this.state.sort,
493 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
494 page: this.state.page,
498 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
500 let personMentionsForm: GetPersonMentions = {
501 sort: this.state.sort,
502 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
503 page: this.state.page,
507 WebSocketService.Instance.send(
508 wsClient.getPersonMentions(personMentionsForm)
511 let privateMessagesForm: GetPrivateMessages = {
512 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
513 page: this.state.page,
517 WebSocketService.Instance.send(
518 wsClient.getPrivateMessages(privateMessagesForm)
522 handleSortChange(val: SortType) {
523 this.state.sort = val;
525 this.setState(this.state);
529 markAllAsRead(i: Inbox) {
530 WebSocketService.Instance.send(
531 wsClient.markAllAsRead({
535 i.state.replies = [];
536 i.state.mentions = [];
537 i.state.messages = [];
538 UserService.Instance.unreadInboxCountSub.next(0);
539 window.scrollTo(0, 0);
543 sendUnreadCount(read: boolean) {
544 let urcs = UserService.Instance.unreadInboxCountSub;
546 urcs.next(urcs.getValue() - 1);
548 urcs.next(urcs.getValue() + 1);
552 parseMessage(msg: any) {
553 let op = wsUserOp(msg);
556 toast(i18n.t(msg.error), "danger");
558 } else if (msg.reconnect) {
560 } else if (op == UserOperation.GetReplies) {
561 let data = wsJsonToRes<GetRepliesResponse>(msg).data;
562 this.state.replies = data.replies;
563 this.state.combined = this.buildCombined();
564 this.state.loading = false;
565 window.scrollTo(0, 0);
566 this.setState(this.state);
568 } else if (op == UserOperation.GetPersonMentions) {
569 let data = wsJsonToRes<GetPersonMentionsResponse>(msg).data;
570 this.state.mentions = data.mentions;
571 this.state.combined = this.buildCombined();
572 window.scrollTo(0, 0);
573 this.setState(this.state);
575 } else if (op == UserOperation.GetPrivateMessages) {
576 let data = wsJsonToRes<PrivateMessagesResponse>(msg).data;
577 this.state.messages = data.private_messages;
578 this.state.combined = this.buildCombined();
579 window.scrollTo(0, 0);
580 this.setState(this.state);
582 } else if (op == UserOperation.EditPrivateMessage) {
583 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
584 let found: PrivateMessageView = this.state.messages.find(
586 m.private_message.id === data.private_message_view.private_message.id
589 let combinedView = this.state.combined.find(
590 i => i.id == data.private_message_view.private_message.id
591 ).view as PrivateMessageView;
592 found.private_message.content = combinedView.private_message.content =
593 data.private_message_view.private_message.content;
594 found.private_message.updated = combinedView.private_message.updated =
595 data.private_message_view.private_message.updated;
597 this.setState(this.state);
598 } else if (op == UserOperation.DeletePrivateMessage) {
599 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
600 let found: PrivateMessageView = this.state.messages.find(
602 m.private_message.id === data.private_message_view.private_message.id
605 let combinedView = this.state.combined.find(
606 i => i.id == data.private_message_view.private_message.id
607 ).view as PrivateMessageView;
608 found.private_message.deleted = combinedView.private_message.deleted =
609 data.private_message_view.private_message.deleted;
610 found.private_message.updated = combinedView.private_message.updated =
611 data.private_message_view.private_message.updated;
613 this.setState(this.state);
614 } else if (op == UserOperation.MarkPrivateMessageAsRead) {
615 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
616 let found: PrivateMessageView = this.state.messages.find(
618 m.private_message.id === data.private_message_view.private_message.id
622 let combinedView = this.state.combined.find(
623 i => i.id == data.private_message_view.private_message.id
624 ).view as PrivateMessageView;
625 found.private_message.updated = combinedView.private_message.updated =
626 data.private_message_view.private_message.updated;
628 // If youre in the unread view, just remove it from the list
630 this.state.unreadOrAll == UnreadOrAll.Unread &&
631 data.private_message_view.private_message.read
633 this.state.messages = this.state.messages.filter(
635 r.private_message.id !==
636 data.private_message_view.private_message.id
638 this.state.combined = this.state.combined.filter(
639 r => r.id !== data.private_message_view.private_message.id
642 found.private_message.read = combinedView.private_message.read =
643 data.private_message_view.private_message.read;
646 this.sendUnreadCount(data.private_message_view.private_message.read);
647 this.setState(this.state);
648 } else if (op == UserOperation.MarkAllAsRead) {
649 // Moved to be instant
651 op == UserOperation.EditComment ||
652 op == UserOperation.DeleteComment ||
653 op == UserOperation.RemoveComment
655 let data = wsJsonToRes<CommentResponse>(msg).data;
656 editCommentRes(data.comment_view, this.state.replies);
657 this.setState(this.state);
658 } else if (op == UserOperation.MarkCommentAsRead) {
659 let data = wsJsonToRes<CommentResponse>(msg).data;
661 // If youre in the unread view, just remove it from the list
663 this.state.unreadOrAll == UnreadOrAll.Unread &&
664 data.comment_view.comment.read
666 this.state.replies = this.state.replies.filter(
667 r => r.comment.id !== data.comment_view.comment.id
669 this.state.combined = this.state.combined.filter(
670 r => r.id !== data.comment_view.comment.id
673 let found = this.state.replies.find(
674 c => c.comment.id == data.comment_view.comment.id
676 let combinedView = this.state.combined.find(
677 i => i.id == data.comment_view.comment.id
678 ).view as CommentView;
679 found.comment.read = combinedView.comment.read =
680 data.comment_view.comment.read;
683 this.sendUnreadCount(data.comment_view.comment.read);
684 this.setState(this.state);
686 } else if (op == UserOperation.MarkPersonMentionAsRead) {
687 let data = wsJsonToRes<PersonMentionResponse>(msg).data;
689 // TODO this might not be correct, it might need to use the comment id
690 let found = this.state.mentions.find(
691 c => c.person_mention.id == data.person_mention_view.person_mention.id
695 let combinedView = this.state.combined.find(
696 i => i.id == data.person_mention_view.person_mention.id
697 ).view as PersonMentionView;
698 found.comment.content = combinedView.comment.content =
699 data.person_mention_view.comment.content;
700 found.comment.updated = combinedView.comment.updated =
701 data.person_mention_view.comment.updated;
702 found.comment.removed = combinedView.comment.removed =
703 data.person_mention_view.comment.removed;
704 found.comment.deleted = combinedView.comment.deleted =
705 data.person_mention_view.comment.deleted;
706 found.counts.upvotes = combinedView.counts.upvotes =
707 data.person_mention_view.counts.upvotes;
708 found.counts.downvotes = combinedView.counts.downvotes =
709 data.person_mention_view.counts.downvotes;
710 found.counts.score = combinedView.counts.score =
711 data.person_mention_view.counts.score;
713 // If youre in the unread view, just remove it from the list
715 this.state.unreadOrAll == UnreadOrAll.Unread &&
716 data.person_mention_view.person_mention.read
718 this.state.mentions = this.state.mentions.filter(
720 r.person_mention.id !== data.person_mention_view.person_mention.id
722 this.state.combined = this.state.combined.filter(
723 r => r.id !== data.person_mention_view.person_mention.id
726 // TODO test to make sure these mentions are getting marked as read
727 found.person_mention.read = combinedView.person_mention.read =
728 data.person_mention_view.person_mention.read;
731 this.sendUnreadCount(data.person_mention_view.person_mention.read);
732 this.setState(this.state);
733 } else if (op == UserOperation.CreateComment) {
734 let data = wsJsonToRes<CommentResponse>(msg).data;
737 data.recipient_ids.includes(
738 UserService.Instance.myUserInfo.local_user_view.local_user.id
741 this.state.replies.unshift(data.comment_view);
742 this.state.combined.unshift(this.replyToReplyType(data.comment_view));
743 this.setState(this.state);
745 data.comment_view.creator.id ==
746 UserService.Instance.myUserInfo.local_user_view.person.id
748 // If youre in the unread view, just remove it from the list
749 if (this.state.unreadOrAll == UnreadOrAll.Unread) {
750 this.state.replies = this.state.replies.filter(
751 r => r.comment.id !== data.comment_view.comment.parent_id
753 this.state.mentions = this.state.mentions.filter(
754 m => m.comment.id !== data.comment_view.comment.parent_id
756 this.state.combined = this.state.combined.filter(r => {
757 if (this.isMention(r.view))
758 return r.view.comment.id !== data.comment_view.comment.parent_id;
759 else return r.id !== data.comment_view.comment.parent_id;
762 let mention_found = this.state.mentions.find(
763 i => i.comment.id == data.comment_view.comment.parent_id
766 mention_found.person_mention.read = true;
768 let reply_found = this.state.replies.find(
769 i => i.comment.id == data.comment_view.comment.parent_id
772 reply_found.comment.read = true;
774 this.state.combined = this.buildCombined();
776 this.sendUnreadCount(true);
777 this.setState(this.state);
779 // TODO this seems wrong, you should be using form_id
780 toast(i18n.t("reply_sent"));
782 } else if (op == UserOperation.CreatePrivateMessage) {
783 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
785 data.private_message_view.recipient.id ==
786 UserService.Instance.myUserInfo.local_user_view.person.id
788 this.state.messages.unshift(data.private_message_view);
789 this.state.combined.unshift(
790 this.messageToReplyType(data.private_message_view)
792 this.setState(this.state);
794 } else if (op == UserOperation.SaveComment) {
795 let data = wsJsonToRes<CommentResponse>(msg).data;
796 saveCommentRes(data.comment_view, this.state.replies);
797 this.setState(this.state);
799 } else if (op == UserOperation.CreateCommentLike) {
800 let data = wsJsonToRes<CommentResponse>(msg).data;
801 createCommentLikeRes(data.comment_view, this.state.replies);
802 this.setState(this.state);
803 } else if (op == UserOperation.BlockPerson) {
804 let data = wsJsonToRes<BlockPersonResponse>(msg).data;
805 updatePersonBlock(data);
806 } else if (op == UserOperation.CreatePostReport) {
807 let data = wsJsonToRes<PostReportResponse>(msg).data;
809 toast(i18n.t("report_created"));
811 } else if (op == UserOperation.CreateCommentReport) {
812 let data = wsJsonToRes<CommentReportResponse>(msg).data;
814 toast(i18n.t("report_created"));
819 isMention(view: any): view is PersonMentionView {
820 return (view as PersonMentionView).person_mention !== undefined;