1 import { Component, linkEvent } from "inferno";
11 GetPersonMentionsResponse,
16 PersonMentionResponse,
19 PrivateMessageReportResponse,
20 PrivateMessageResponse,
22 PrivateMessagesResponse,
26 } from "lemmy-js-client";
27 import { Subscription } from "rxjs";
28 import { i18n } from "../../i18next";
29 import { CommentViewType, InitialFetchRequest } from "../../interfaces";
30 import { UserService, WebSocketService } from "../../services";
48 import { CommentNodes } from "../comment/comment-nodes";
49 import { CommentSortSelect } from "../common/comment-sort-select";
50 import { HtmlTags } from "../common/html-tags";
51 import { Icon, Spinner } from "../common/icon";
52 import { Paginator } from "../common/paginator";
53 import { PrivateMessage } from "../private_message/private-message";
75 view: CommentView | PrivateMessageView | PersonMentionView | CommentReplyView;
79 interface InboxState {
80 unreadOrAll: UnreadOrAll;
81 messageType: MessageType;
82 replies: CommentReplyView[];
83 mentions: PersonMentionView[];
84 messages: PrivateMessageView[];
85 combined: ReplyType[];
86 sort: CommentSortType;
88 siteRes: GetSiteResponse;
92 export class Inbox extends Component<any, InboxState> {
93 private isoData = setIsoData(this.context);
94 private subscription?: Subscription;
96 unreadOrAll: UnreadOrAll.Unread,
97 messageType: MessageType.All,
102 sort: CommentSortType.New,
104 siteRes: this.isoData.site_res,
108 constructor(props: any, context: any) {
109 super(props, context);
111 this.handleSortChange = this.handleSortChange.bind(this);
112 this.handlePageChange = this.handlePageChange.bind(this);
114 if (!UserService.Instance.myUserInfo && isBrowser()) {
115 toast(i18n.t("not_logged_in"), "danger");
116 this.context.router.history.push(`/login`);
119 this.parseMessage = this.parseMessage.bind(this);
120 this.subscription = wsSubscribe(this.parseMessage);
122 // Only fetch the data if coming from another route
123 if (this.isoData.path == this.context.router.route.match.url) {
127 (this.isoData.routeData[0] as GetRepliesResponse).replies || [],
129 (this.isoData.routeData[1] as GetPersonMentionsResponse).mentions ||
132 (this.isoData.routeData[2] as PrivateMessagesResponse)
133 .private_messages || [],
136 this.state = { ...this.state, combined: this.buildCombined() };
142 componentWillUnmount() {
144 this.subscription?.unsubscribe();
148 get documentTitle(): string {
149 let mui = UserService.Instance.myUserInfo;
151 ? `@${mui.local_user_view.person.name} ${i18n.t("inbox")} - ${
152 this.state.siteRes.site_view.site.name
159 let inboxRss = auth ? `/feeds/inbox/${auth}.xml` : undefined;
161 <div className="container-lg">
162 {this.state.loading ? (
167 <div className="row">
168 <div className="col-12">
170 title={this.documentTitle}
171 path={this.context.router.route.match.url}
173 <h5 className="mb-2">
177 <a href={inboxRss} title="RSS" rel={relTags}>
178 <Icon icon="rss" classes="ml-2 text-muted small" />
182 type="application/atom+xml"
188 {this.state.replies.length +
189 this.state.mentions.length +
190 this.state.messages.length >
192 this.state.unreadOrAll == UnreadOrAll.Unread && (
194 className="btn btn-secondary mb-2"
195 onClick={linkEvent(this, this.markAllAsRead)}
197 {i18n.t("mark_all_as_read")}
201 {this.state.messageType == MessageType.All && this.all()}
202 {this.state.messageType == MessageType.Replies && this.replies()}
203 {this.state.messageType == MessageType.Mentions &&
205 {this.state.messageType == MessageType.Messages &&
208 page={this.state.page}
209 onChange={this.handlePageChange}
218 unreadOrAllRadios() {
220 <div className="btn-group btn-group-toggle flex-wrap mb-2">
222 className={`btn btn-outline-secondary pointer
223 ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
228 value={UnreadOrAll.Unread}
229 checked={this.state.unreadOrAll == UnreadOrAll.Unread}
230 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
235 className={`btn btn-outline-secondary pointer
236 ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
241 value={UnreadOrAll.All}
242 checked={this.state.unreadOrAll == UnreadOrAll.All}
243 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
251 messageTypeRadios() {
253 <div className="btn-group btn-group-toggle flex-wrap mb-2">
255 className={`btn btn-outline-secondary pointer
256 ${this.state.messageType == MessageType.All && "active"}
261 value={MessageType.All}
262 checked={this.state.messageType == MessageType.All}
263 onChange={linkEvent(this, this.handleMessageTypeChange)}
268 className={`btn btn-outline-secondary pointer
269 ${this.state.messageType == MessageType.Replies && "active"}
274 value={MessageType.Replies}
275 checked={this.state.messageType == MessageType.Replies}
276 onChange={linkEvent(this, this.handleMessageTypeChange)}
281 className={`btn btn-outline-secondary pointer
282 ${this.state.messageType == MessageType.Mentions && "active"}
287 value={MessageType.Mentions}
288 checked={this.state.messageType == MessageType.Mentions}
289 onChange={linkEvent(this, this.handleMessageTypeChange)}
294 className={`btn btn-outline-secondary pointer
295 ${this.state.messageType == MessageType.Messages && "active"}
300 value={MessageType.Messages}
301 checked={this.state.messageType == MessageType.Messages}
302 onChange={linkEvent(this, this.handleMessageTypeChange)}
312 <div className="mb-2">
313 <span className="mr-3">{this.unreadOrAllRadios()}</span>
314 <span className="mr-3">{this.messageTypeRadios()}</span>
316 sort={this.state.sort}
317 onChange={this.handleSortChange}
323 replyToReplyType(r: CommentReplyView): ReplyType {
325 id: r.comment_reply.id,
326 type_: ReplyEnum.Reply,
328 published: r.comment.published,
332 mentionToReplyType(r: PersonMentionView): ReplyType {
334 id: r.person_mention.id,
335 type_: ReplyEnum.Mention,
337 published: r.comment.published,
341 messageToReplyType(r: PrivateMessageView): ReplyType {
343 id: r.private_message.id,
344 type_: ReplyEnum.Message,
346 published: r.private_message.published,
350 buildCombined(): ReplyType[] {
351 let replies: ReplyType[] = this.state.replies.map(r =>
352 this.replyToReplyType(r)
354 let mentions: ReplyType[] = this.state.mentions.map(r =>
355 this.mentionToReplyType(r)
357 let messages: ReplyType[] = this.state.messages.map(r =>
358 this.messageToReplyType(r)
361 return [...replies, ...mentions, ...messages].sort((a, b) =>
362 b.published.localeCompare(a.published)
366 renderReplyType(i: ReplyType) {
368 case ReplyEnum.Reply:
373 { comment_view: i.view as CommentView, children: [], depth: 0 },
375 viewType={CommentViewType.Flat}
380 enableDownvotes={enableDownvotes(this.state.siteRes)}
381 allLanguages={this.state.siteRes.all_languages}
382 siteLanguages={this.state.siteRes.discussion_languages}
385 case ReplyEnum.Mention:
391 comment_view: i.view as PersonMentionView,
396 viewType={CommentViewType.Flat}
401 enableDownvotes={enableDownvotes(this.state.siteRes)}
402 allLanguages={this.state.siteRes.all_languages}
403 siteLanguages={this.state.siteRes.discussion_languages}
406 case ReplyEnum.Message:
410 private_message_view={i.view as PrivateMessageView}
419 return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
426 nodes={commentsToFlatNodes(this.state.replies)}
427 viewType={CommentViewType.Flat}
432 enableDownvotes={enableDownvotes(this.state.siteRes)}
433 allLanguages={this.state.siteRes.all_languages}
434 siteLanguages={this.state.siteRes.discussion_languages}
443 {this.state.mentions.map(umv => (
445 key={umv.person_mention.id}
446 nodes={[{ comment_view: umv, children: [], depth: 0 }]}
447 viewType={CommentViewType.Flat}
452 enableDownvotes={enableDownvotes(this.state.siteRes)}
453 allLanguages={this.state.siteRes.all_languages}
454 siteLanguages={this.state.siteRes.discussion_languages}
464 {this.state.messages.map(pmv => (
466 key={pmv.private_message.id}
467 private_message_view={pmv}
474 handlePageChange(page: number) {
475 this.setState({ page });
479 handleUnreadOrAllChange(i: Inbox, event: any) {
480 i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
484 handleMessageTypeChange(i: Inbox, event: any) {
485 i.setState({ messageType: Number(event.target.value), page: 1 });
489 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
490 let promises: Promise<any>[] = [];
492 let sort = CommentSortType.New;
496 // It can be /u/me, or /username/1
497 let repliesForm: GetReplies = {
504 promises.push(req.client.getReplies(repliesForm));
506 let personMentionsForm: GetPersonMentions = {
513 promises.push(req.client.getPersonMentions(personMentionsForm));
515 let privateMessagesForm: GetPrivateMessages = {
521 promises.push(req.client.getPrivateMessages(privateMessagesForm));
528 let sort = this.state.sort;
529 let unread_only = this.state.unreadOrAll == UnreadOrAll.Unread;
530 let page = this.state.page;
531 let limit = fetchLimit;
535 let repliesForm: GetReplies = {
542 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
544 let personMentionsForm: GetPersonMentions = {
551 WebSocketService.Instance.send(
552 wsClient.getPersonMentions(personMentionsForm)
555 let privateMessagesForm: GetPrivateMessages = {
561 WebSocketService.Instance.send(
562 wsClient.getPrivateMessages(privateMessagesForm)
567 handleSortChange(val: CommentSortType) {
568 this.setState({ sort: val, page: 1 });
572 markAllAsRead(i: Inbox) {
575 WebSocketService.Instance.send(
576 wsClient.markAllAsRead({
580 i.setState({ replies: [], mentions: [], messages: [] });
581 i.setState({ combined: i.buildCombined() });
582 UserService.Instance.unreadInboxCountSub.next(0);
583 window.scrollTo(0, 0);
588 sendUnreadCount(read: boolean) {
589 let urcs = UserService.Instance.unreadInboxCountSub;
591 urcs.next(urcs.getValue() - 1);
593 urcs.next(urcs.getValue() + 1);
597 parseMessage(msg: any) {
598 let op = wsUserOp(msg);
601 toast(i18n.t(msg.error), "danger");
603 } else if (msg.reconnect) {
605 } else if (op == UserOperation.GetReplies) {
606 let data = wsJsonToRes<GetRepliesResponse>(msg);
607 this.setState({ replies: data.replies });
608 this.setState({ combined: this.buildCombined(), loading: false });
609 window.scrollTo(0, 0);
611 } else if (op == UserOperation.GetPersonMentions) {
612 let data = wsJsonToRes<GetPersonMentionsResponse>(msg);
613 this.setState({ mentions: data.mentions });
614 this.setState({ combined: this.buildCombined() });
615 window.scrollTo(0, 0);
617 } else if (op == UserOperation.GetPrivateMessages) {
618 let data = wsJsonToRes<PrivateMessagesResponse>(msg);
619 this.setState({ messages: data.private_messages });
620 this.setState({ combined: this.buildCombined() });
621 window.scrollTo(0, 0);
623 } else if (op == UserOperation.EditPrivateMessage) {
624 let data = wsJsonToRes<PrivateMessageResponse>(msg);
625 let found = this.state.messages.find(
627 m.private_message.id === data.private_message_view.private_message.id
630 let combinedView = this.state.combined.find(
631 i => i.id == data.private_message_view.private_message.id
632 )?.view as PrivateMessageView | undefined;
634 found.private_message.content = combinedView.private_message.content =
635 data.private_message_view.private_message.content;
636 found.private_message.updated = combinedView.private_message.updated =
637 data.private_message_view.private_message.updated;
640 this.setState(this.state);
641 } else if (op == UserOperation.DeletePrivateMessage) {
642 let data = wsJsonToRes<PrivateMessageResponse>(msg);
643 let found = this.state.messages.find(
645 m.private_message.id === data.private_message_view.private_message.id
648 let combinedView = this.state.combined.find(
649 i => i.id == data.private_message_view.private_message.id
650 )?.view as PrivateMessageView | undefined;
652 found.private_message.deleted = combinedView.private_message.deleted =
653 data.private_message_view.private_message.deleted;
654 found.private_message.updated = combinedView.private_message.updated =
655 data.private_message_view.private_message.updated;
658 this.setState(this.state);
659 } else if (op == UserOperation.MarkPrivateMessageAsRead) {
660 let data = wsJsonToRes<PrivateMessageResponse>(msg);
661 let found = this.state.messages.find(
663 m.private_message.id === data.private_message_view.private_message.id
667 let combinedView = this.state.combined.find(
669 i.id == data.private_message_view.private_message.id &&
670 i.type_ == ReplyEnum.Message
671 )?.view as PrivateMessageView | undefined;
673 found.private_message.updated = combinedView.private_message.updated =
674 data.private_message_view.private_message.updated;
676 // If youre in the unread view, just remove it from the list
678 this.state.unreadOrAll == UnreadOrAll.Unread &&
679 data.private_message_view.private_message.read
682 messages: this.state.messages.filter(
684 r.private_message.id !==
685 data.private_message_view.private_message.id
689 combined: this.state.combined.filter(
690 r => r.id !== data.private_message_view.private_message.id
694 found.private_message.read = combinedView.private_message.read =
695 data.private_message_view.private_message.read;
699 this.sendUnreadCount(data.private_message_view.private_message.read);
700 this.setState(this.state);
701 } else if (op == UserOperation.MarkAllAsRead) {
702 // Moved to be instant
704 op == UserOperation.EditComment ||
705 op == UserOperation.DeleteComment ||
706 op == UserOperation.RemoveComment
708 let data = wsJsonToRes<CommentResponse>(msg);
709 editCommentRes(data.comment_view, this.state.replies);
710 this.setState(this.state);
711 } else if (op == UserOperation.MarkCommentReplyAsRead) {
712 let data = wsJsonToRes<CommentReplyResponse>(msg);
714 let found = this.state.replies.find(
715 c => c.comment_reply.id == data.comment_reply_view.comment_reply.id
719 let combinedView = this.state.combined.find(
721 i.id == data.comment_reply_view.comment_reply.id &&
722 i.type_ == ReplyEnum.Reply
723 )?.view as CommentReplyView | undefined;
725 found.comment.content = combinedView.comment.content =
726 data.comment_reply_view.comment.content;
727 found.comment.updated = combinedView.comment.updated =
728 data.comment_reply_view.comment.updated;
729 found.comment.removed = combinedView.comment.removed =
730 data.comment_reply_view.comment.removed;
731 found.comment.deleted = combinedView.comment.deleted =
732 data.comment_reply_view.comment.deleted;
733 found.counts.upvotes = combinedView.counts.upvotes =
734 data.comment_reply_view.counts.upvotes;
735 found.counts.downvotes = combinedView.counts.downvotes =
736 data.comment_reply_view.counts.downvotes;
737 found.counts.score = combinedView.counts.score =
738 data.comment_reply_view.counts.score;
740 // If youre in the unread view, just remove it from the list
742 this.state.unreadOrAll == UnreadOrAll.Unread &&
743 data.comment_reply_view.comment_reply.read
746 replies: this.state.replies.filter(
748 r.comment_reply.id !==
749 data.comment_reply_view.comment_reply.id
753 combined: this.state.combined.filter(
754 r => r.id !== data.comment_reply_view.comment_reply.id
758 found.comment_reply.read = combinedView.comment_reply.read =
759 data.comment_reply_view.comment_reply.read;
763 this.sendUnreadCount(data.comment_reply_view.comment_reply.read);
764 this.setState(this.state);
765 } else if (op == UserOperation.MarkPersonMentionAsRead) {
766 let data = wsJsonToRes<PersonMentionResponse>(msg);
768 // TODO this might not be correct, it might need to use the comment id
769 let found = this.state.mentions.find(
770 c => c.person_mention.id == data.person_mention_view.person_mention.id
774 let combinedView = this.state.combined.find(
776 i.id == data.person_mention_view.person_mention.id &&
777 i.type_ == ReplyEnum.Mention
778 )?.view as PersonMentionView | undefined;
780 found.comment.content = combinedView.comment.content =
781 data.person_mention_view.comment.content;
782 found.comment.updated = combinedView.comment.updated =
783 data.person_mention_view.comment.updated;
784 found.comment.removed = combinedView.comment.removed =
785 data.person_mention_view.comment.removed;
786 found.comment.deleted = combinedView.comment.deleted =
787 data.person_mention_view.comment.deleted;
788 found.counts.upvotes = combinedView.counts.upvotes =
789 data.person_mention_view.counts.upvotes;
790 found.counts.downvotes = combinedView.counts.downvotes =
791 data.person_mention_view.counts.downvotes;
792 found.counts.score = combinedView.counts.score =
793 data.person_mention_view.counts.score;
795 // If youre in the unread view, just remove it from the list
797 this.state.unreadOrAll == UnreadOrAll.Unread &&
798 data.person_mention_view.person_mention.read
801 mentions: this.state.mentions.filter(
803 r.person_mention.id !==
804 data.person_mention_view.person_mention.id
808 combined: this.state.combined.filter(
809 r => r.id !== data.person_mention_view.person_mention.id
813 // TODO test to make sure these mentions are getting marked as read
814 found.person_mention.read = combinedView.person_mention.read =
815 data.person_mention_view.person_mention.read;
819 this.sendUnreadCount(data.person_mention_view.person_mention.read);
820 this.setState(this.state);
821 } else if (op == UserOperation.CreatePrivateMessage) {
822 let data = wsJsonToRes<PrivateMessageResponse>(msg);
823 let mui = UserService.Instance.myUserInfo;
825 data.private_message_view.recipient.id == mui?.local_user_view.person.id
827 this.state.messages.unshift(data.private_message_view);
828 this.state.combined.unshift(
829 this.messageToReplyType(data.private_message_view)
831 this.setState(this.state);
833 } else if (op == UserOperation.SaveComment) {
834 let data = wsJsonToRes<CommentResponse>(msg);
835 saveCommentRes(data.comment_view, this.state.replies);
836 this.setState(this.state);
838 } else if (op == UserOperation.CreateCommentLike) {
839 let data = wsJsonToRes<CommentResponse>(msg);
840 createCommentLikeRes(data.comment_view, this.state.replies);
841 this.setState(this.state);
842 } else if (op == UserOperation.BlockPerson) {
843 let data = wsJsonToRes<BlockPersonResponse>(msg);
844 updatePersonBlock(data);
845 } else if (op == UserOperation.CreatePostReport) {
846 let data = wsJsonToRes<PostReportResponse>(msg);
848 toast(i18n.t("report_created"));
850 } else if (op == UserOperation.CreateCommentReport) {
851 let data = wsJsonToRes<CommentReportResponse>(msg);
853 toast(i18n.t("report_created"));
855 } else if (op == UserOperation.CreatePrivateMessageReport) {
856 let data = wsJsonToRes<PrivateMessageReportResponse>(msg);
858 toast(i18n.t("report_created"));
863 isMention(view: any): view is PersonMentionView {
864 return (view as PersonMentionView).person_mention !== undefined;
867 isReply(view: any): view is CommentReplyView {
868 return (view as CommentReplyView).comment_reply !== undefined;