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";
44 import { CommentNodes } from "../comment/comment-nodes";
45 import { HtmlTags } from "../common/html-tags";
46 import { Icon, Spinner } from "../common/icon";
47 import { Paginator } from "../common/paginator";
48 import { SortSelect } from "../common/sort-select";
49 import { PrivateMessage } from "../private_message/private-message";
71 view: CommentView | PrivateMessageView | PersonMentionView;
75 interface InboxState {
76 unreadOrAll: UnreadOrAll;
77 messageType: MessageType;
78 replies: CommentView[];
79 mentions: PersonMentionView[];
80 messages: PrivateMessageView[];
81 combined: ReplyType[];
88 export class Inbox extends Component<any, InboxState> {
89 private isoData = setIsoData(this.context);
90 private subscription: Subscription;
91 private emptyState: InboxState = {
92 unreadOrAll: UnreadOrAll.Unread,
93 messageType: MessageType.All,
100 site_view: this.isoData.site_res.site_view,
104 constructor(props: any, context: any) {
105 super(props, context);
107 this.state = this.emptyState;
108 this.handleSortChange = this.handleSortChange.bind(this);
109 this.handlePageChange = this.handlePageChange.bind(this);
111 if (!UserService.Instance.myUserInfo && isBrowser()) {
112 toast(i18n.t("not_logged_in"), "danger");
113 this.context.router.history.push(`/login`);
116 this.parseMessage = this.parseMessage.bind(this);
117 this.subscription = wsSubscribe(this.parseMessage);
119 // Only fetch the data if coming from another route
120 if (this.isoData.path == this.context.router.route.match.url) {
121 this.state.replies = this.isoData.routeData[0].replies || [];
122 this.state.mentions = this.isoData.routeData[1].mentions || [];
123 this.state.messages = this.isoData.routeData[2].messages || [];
124 this.state.combined = this.buildCombined();
125 this.state.loading = false;
131 componentWillUnmount() {
133 this.subscription.unsubscribe();
137 get documentTitle(): string {
139 UserService.Instance.myUserInfo.local_user_view.person.name
140 } ${i18n.t("inbox")} - ${this.state.site_view.site.name}`;
144 let inboxRss = `/feeds/inbox/${UserService.Instance.auth}.xml`;
146 <div class="container">
147 {this.state.loading ? (
155 title={this.documentTitle}
156 path={this.context.router.route.match.url}
161 <a href={inboxRss} title="RSS" rel={relTags}>
162 <Icon icon="rss" classes="ml-2 text-muted small" />
166 type="application/atom+xml"
171 {this.state.replies.length +
172 this.state.mentions.length +
173 this.state.messages.length >
175 this.state.unreadOrAll == UnreadOrAll.Unread && (
177 class="btn btn-secondary mb-2"
178 onClick={linkEvent(this, this.markAllAsRead)}
180 {i18n.t("mark_all_as_read")}
184 {this.state.messageType == MessageType.All && this.all()}
185 {this.state.messageType == MessageType.Replies && this.replies()}
186 {this.state.messageType == MessageType.Mentions &&
188 {this.state.messageType == MessageType.Messages &&
191 page={this.state.page}
192 onChange={this.handlePageChange}
201 unreadOrAllRadios() {
203 <div class="btn-group btn-group-toggle flex-wrap mb-2">
205 className={`btn btn-outline-secondary pointer
206 ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
211 value={UnreadOrAll.Unread}
212 checked={this.state.unreadOrAll == UnreadOrAll.Unread}
213 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
218 className={`btn btn-outline-secondary pointer
219 ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
224 value={UnreadOrAll.All}
225 checked={this.state.unreadOrAll == UnreadOrAll.All}
226 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
234 messageTypeRadios() {
236 <div class="btn-group btn-group-toggle flex-wrap mb-2">
238 className={`btn btn-outline-secondary pointer
239 ${this.state.messageType == MessageType.All && "active"}
244 value={MessageType.All}
245 checked={this.state.messageType == MessageType.All}
246 onChange={linkEvent(this, this.handleMessageTypeChange)}
251 className={`btn btn-outline-secondary pointer
252 ${this.state.messageType == MessageType.Replies && "active"}
257 value={MessageType.Replies}
258 checked={this.state.messageType == MessageType.Replies}
259 onChange={linkEvent(this, this.handleMessageTypeChange)}
264 className={`btn btn-outline-secondary pointer
265 ${this.state.messageType == MessageType.Mentions && "active"}
270 value={MessageType.Mentions}
271 checked={this.state.messageType == MessageType.Mentions}
272 onChange={linkEvent(this, this.handleMessageTypeChange)}
277 className={`btn btn-outline-secondary pointer
278 ${this.state.messageType == MessageType.Messages && "active"}
283 value={MessageType.Messages}
284 checked={this.state.messageType == MessageType.Messages}
285 onChange={linkEvent(this, this.handleMessageTypeChange)}
295 <div className="mb-2">
296 <span class="mr-3">{this.unreadOrAllRadios()}</span>
297 <span class="mr-3">{this.messageTypeRadios()}</span>
299 sort={this.state.sort}
300 onChange={this.handleSortChange}
308 replyToReplyType(r: CommentView): ReplyType {
311 type_: ReplyEnum.Reply,
313 published: r.comment.published,
317 mentionToReplyType(r: PersonMentionView): ReplyType {
319 id: r.person_mention.id,
320 type_: ReplyEnum.Mention,
322 published: r.comment.published,
326 messageToReplyType(r: PrivateMessageView): ReplyType {
328 id: r.private_message.id,
329 type_: ReplyEnum.Message,
331 published: r.private_message.published,
335 buildCombined(): ReplyType[] {
336 let replies: ReplyType[] = this.state.replies.map(r =>
337 this.replyToReplyType(r)
339 let mentions: ReplyType[] = this.state.mentions.map(r =>
340 this.mentionToReplyType(r)
342 let messages: ReplyType[] = this.state.messages.map(r =>
343 this.messageToReplyType(r)
346 return [...replies, ...mentions, ...messages].sort((a, b) =>
347 b.published.localeCompare(a.published)
351 renderReplyType(i: ReplyType) {
353 case ReplyEnum.Reply:
357 nodes={[{ comment_view: i.view as CommentView }]}
362 enableDownvotes={this.state.site_view.site.enable_downvotes}
365 case ReplyEnum.Mention:
369 nodes={[{ comment_view: i.view as PersonMentionView }]}
374 enableDownvotes={this.state.site_view.site.enable_downvotes}
377 case ReplyEnum.Message:
381 private_message_view={i.view as PrivateMessageView}
390 return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
397 nodes={commentsToFlatNodes(this.state.replies)}
402 enableDownvotes={this.state.site_view.site.enable_downvotes}
411 {this.state.mentions.map(umv => (
413 key={umv.person_mention.id}
414 nodes={[{ comment_view: umv }]}
419 enableDownvotes={this.state.site_view.site.enable_downvotes}
429 {this.state.messages.map(pmv => (
431 key={pmv.private_message.id}
432 private_message_view={pmv}
439 handlePageChange(page: number) {
440 this.setState({ page });
444 handleUnreadOrAllChange(i: Inbox, event: any) {
445 i.state.unreadOrAll = Number(event.target.value);
451 handleMessageTypeChange(i: Inbox, event: any) {
452 i.state.messageType = Number(event.target.value);
458 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
459 let promises: Promise<any>[] = [];
461 // It can be /u/me, or /username/1
462 let repliesForm: GetReplies = {
469 promises.push(req.client.getReplies(repliesForm));
471 let personMentionsForm: GetPersonMentions = {
478 promises.push(req.client.getPersonMentions(personMentionsForm));
480 let privateMessagesForm: GetPrivateMessages = {
486 promises.push(req.client.getPrivateMessages(privateMessagesForm));
492 let repliesForm: GetReplies = {
493 sort: this.state.sort,
494 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
495 page: this.state.page,
499 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
501 let personMentionsForm: GetPersonMentions = {
502 sort: this.state.sort,
503 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
504 page: this.state.page,
508 WebSocketService.Instance.send(
509 wsClient.getPersonMentions(personMentionsForm)
512 let privateMessagesForm: GetPrivateMessages = {
513 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
514 page: this.state.page,
518 WebSocketService.Instance.send(
519 wsClient.getPrivateMessages(privateMessagesForm)
523 handleSortChange(val: SortType) {
524 this.state.sort = val;
526 this.setState(this.state);
530 markAllAsRead(i: Inbox) {
531 WebSocketService.Instance.send(
532 wsClient.markAllAsRead({
536 i.state.replies = [];
537 i.state.mentions = [];
538 i.state.messages = [];
539 UserService.Instance.unreadInboxCountSub.next(0);
540 window.scrollTo(0, 0);
544 sendUnreadCount(read: boolean) {
545 let urcs = UserService.Instance.unreadInboxCountSub;
547 urcs.next(urcs.getValue() - 1);
549 urcs.next(urcs.getValue() + 1);
553 parseMessage(msg: any) {
554 let op = wsUserOp(msg);
557 toast(i18n.t(msg.error), "danger");
559 } else if (msg.reconnect) {
561 } else if (op == UserOperation.GetReplies) {
562 let data = wsJsonToRes<GetRepliesResponse>(msg).data;
563 this.state.replies = data.replies;
564 this.state.combined = this.buildCombined();
565 this.state.loading = false;
566 window.scrollTo(0, 0);
567 this.setState(this.state);
569 } else if (op == UserOperation.GetPersonMentions) {
570 let data = wsJsonToRes<GetPersonMentionsResponse>(msg).data;
571 this.state.mentions = data.mentions;
572 this.state.combined = this.buildCombined();
573 window.scrollTo(0, 0);
574 this.setState(this.state);
576 } else if (op == UserOperation.GetPrivateMessages) {
577 let data = wsJsonToRes<PrivateMessagesResponse>(msg).data;
578 this.state.messages = data.private_messages;
579 this.state.combined = this.buildCombined();
580 window.scrollTo(0, 0);
581 this.setState(this.state);
583 } else if (op == UserOperation.EditPrivateMessage) {
584 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
585 let found: PrivateMessageView = this.state.messages.find(
587 m.private_message.id === data.private_message_view.private_message.id
590 let combinedView = this.state.combined.find(
591 i => i.id == data.private_message_view.private_message.id
592 ).view as PrivateMessageView;
593 found.private_message.content = combinedView.private_message.content =
594 data.private_message_view.private_message.content;
595 found.private_message.updated = combinedView.private_message.updated =
596 data.private_message_view.private_message.updated;
598 this.setState(this.state);
599 } else if (op == UserOperation.DeletePrivateMessage) {
600 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
601 let found: PrivateMessageView = this.state.messages.find(
603 m.private_message.id === data.private_message_view.private_message.id
606 let combinedView = this.state.combined.find(
607 i => i.id == data.private_message_view.private_message.id
608 ).view as PrivateMessageView;
609 found.private_message.deleted = combinedView.private_message.deleted =
610 data.private_message_view.private_message.deleted;
611 found.private_message.updated = combinedView.private_message.updated =
612 data.private_message_view.private_message.updated;
614 this.setState(this.state);
615 } else if (op == UserOperation.MarkPrivateMessageAsRead) {
616 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
617 let found: PrivateMessageView = this.state.messages.find(
619 m.private_message.id === data.private_message_view.private_message.id
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.updated = combinedView.private_message.updated =
627 data.private_message_view.private_message.updated;
629 // If youre in the unread view, just remove it from the list
631 this.state.unreadOrAll == UnreadOrAll.Unread &&
632 data.private_message_view.private_message.read
634 this.state.messages = this.state.messages.filter(
636 r.private_message.id !==
637 data.private_message_view.private_message.id
639 this.state.combined = this.state.combined.filter(
640 r => r.id !== data.private_message_view.private_message.id
643 found.private_message.read = combinedView.private_message.read =
644 data.private_message_view.private_message.read;
647 this.sendUnreadCount(data.private_message_view.private_message.read);
648 this.setState(this.state);
649 } else if (op == UserOperation.MarkAllAsRead) {
650 // Moved to be instant
652 op == UserOperation.EditComment ||
653 op == UserOperation.DeleteComment ||
654 op == UserOperation.RemoveComment
656 let data = wsJsonToRes<CommentResponse>(msg).data;
657 editCommentRes(data.comment_view, this.state.replies);
658 this.setState(this.state);
659 } else if (op == UserOperation.MarkCommentAsRead) {
660 let data = wsJsonToRes<CommentResponse>(msg).data;
662 // If youre in the unread view, just remove it from the list
664 this.state.unreadOrAll == UnreadOrAll.Unread &&
665 data.comment_view.comment.read
667 this.state.replies = this.state.replies.filter(
668 r => r.comment.id !== data.comment_view.comment.id
670 this.state.combined = this.state.combined.filter(
671 r => r.id !== data.comment_view.comment.id
674 let found = this.state.replies.find(
675 c => c.comment.id == data.comment_view.comment.id
677 let combinedView = this.state.combined.find(
678 i => i.id == data.comment_view.comment.id
679 ).view as CommentView;
680 found.comment.read = combinedView.comment.read =
681 data.comment_view.comment.read;
684 this.sendUnreadCount(data.comment_view.comment.read);
685 this.setState(this.state);
687 } else if (op == UserOperation.MarkPersonMentionAsRead) {
688 let data = wsJsonToRes<PersonMentionResponse>(msg).data;
690 // TODO this might not be correct, it might need to use the comment id
691 let found = this.state.mentions.find(
692 c => c.person_mention.id == data.person_mention_view.person_mention.id
696 let combinedView = this.state.combined.find(
697 i => i.id == data.person_mention_view.person_mention.id
698 ).view as PersonMentionView;
699 found.comment.content = combinedView.comment.content =
700 data.person_mention_view.comment.content;
701 found.comment.updated = combinedView.comment.updated =
702 data.person_mention_view.comment.updated;
703 found.comment.removed = combinedView.comment.removed =
704 data.person_mention_view.comment.removed;
705 found.comment.deleted = combinedView.comment.deleted =
706 data.person_mention_view.comment.deleted;
707 found.counts.upvotes = combinedView.counts.upvotes =
708 data.person_mention_view.counts.upvotes;
709 found.counts.downvotes = combinedView.counts.downvotes =
710 data.person_mention_view.counts.downvotes;
711 found.counts.score = combinedView.counts.score =
712 data.person_mention_view.counts.score;
714 // If youre in the unread view, just remove it from the list
716 this.state.unreadOrAll == UnreadOrAll.Unread &&
717 data.person_mention_view.person_mention.read
719 this.state.mentions = this.state.mentions.filter(
721 r.person_mention.id !== data.person_mention_view.person_mention.id
723 this.state.combined = this.state.combined.filter(
724 r => r.id !== data.person_mention_view.person_mention.id
727 // TODO test to make sure these mentions are getting marked as read
728 found.person_mention.read = combinedView.person_mention.read =
729 data.person_mention_view.person_mention.read;
732 this.sendUnreadCount(data.person_mention_view.person_mention.read);
733 this.setState(this.state);
734 } else if (op == UserOperation.CreateComment) {
735 let data = wsJsonToRes<CommentResponse>(msg).data;
738 data.recipient_ids.includes(
739 UserService.Instance.myUserInfo.local_user_view.local_user.id
742 this.state.replies.unshift(data.comment_view);
743 this.state.combined.unshift(this.replyToReplyType(data.comment_view));
744 this.setState(this.state);
746 data.comment_view.creator.id ==
747 UserService.Instance.myUserInfo.local_user_view.person.id
749 // If youre in the unread view, just remove it from the list
750 if (this.state.unreadOrAll == UnreadOrAll.Unread) {
751 this.state.replies = this.state.replies.filter(
752 r => r.comment.id !== data.comment_view.comment.parent_id
754 this.state.mentions = this.state.mentions.filter(
755 m => m.comment.id !== data.comment_view.comment.parent_id
757 this.state.combined = this.state.combined.filter(r => {
758 if (this.isMention(r.view))
759 return r.view.comment.id !== data.comment_view.comment.parent_id;
760 else return r.id !== data.comment_view.comment.parent_id;
763 let mention_found = this.state.mentions.find(
764 i => i.comment.id == data.comment_view.comment.parent_id
767 mention_found.person_mention.read = true;
769 let reply_found = this.state.replies.find(
770 i => i.comment.id == data.comment_view.comment.parent_id
773 reply_found.comment.read = true;
775 this.state.combined = this.buildCombined();
777 this.sendUnreadCount(true);
778 this.setState(this.state);
780 // TODO this seems wrong, you should be using form_id
781 toast(i18n.t("reply_sent"));
783 } else if (op == UserOperation.CreatePrivateMessage) {
784 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
786 data.private_message_view.recipient.id ==
787 UserService.Instance.myUserInfo.local_user_view.person.id
789 this.state.messages.unshift(data.private_message_view);
790 this.state.combined.unshift(
791 this.messageToReplyType(data.private_message_view)
793 this.setState(this.state);
795 } else if (op == UserOperation.SaveComment) {
796 let data = wsJsonToRes<CommentResponse>(msg).data;
797 saveCommentRes(data.comment_view, this.state.replies);
798 this.setState(this.state);
800 } else if (op == UserOperation.CreateCommentLike) {
801 let data = wsJsonToRes<CommentResponse>(msg).data;
802 createCommentLikeRes(data.comment_view, this.state.replies);
803 this.setState(this.state);
804 } else if (op == UserOperation.BlockPerson) {
805 let data = wsJsonToRes<BlockPersonResponse>(msg).data;
806 updatePersonBlock(data);
807 } else if (op == UserOperation.CreatePostReport) {
808 let data = wsJsonToRes<PostReportResponse>(msg).data;
810 toast(i18n.t("report_created"));
812 } else if (op == UserOperation.CreateCommentReport) {
813 let data = wsJsonToRes<CommentReportResponse>(msg).data;
815 toast(i18n.t("report_created"));
820 isMention(view: any): view is PersonMentionView {
821 return (view as PersonMentionView).person_mention !== undefined;