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,
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 this.parseMessage = this.parseMessage.bind(this);
115 this.subscription = wsSubscribe(this.parseMessage);
117 // Only fetch the data if coming from another route
118 if (this.isoData.path == this.context.router.route.match.url) {
122 (this.isoData.routeData[0] as GetRepliesResponse).replies || [],
124 (this.isoData.routeData[1] as GetPersonMentionsResponse).mentions ||
127 (this.isoData.routeData[2] as PrivateMessagesResponse)
128 .private_messages || [],
131 this.state = { ...this.state, combined: this.buildCombined() };
137 componentWillUnmount() {
139 this.subscription?.unsubscribe();
143 get documentTitle(): string {
144 const mui = UserService.Instance.myUserInfo;
146 ? `@${mui.local_user_view.person.name} ${i18n.t("inbox")} - ${
147 this.state.siteRes.site_view.site.name
153 const auth = myAuth();
154 const inboxRss = auth ? `/feeds/inbox/${auth}.xml` : undefined;
156 <div className="container-lg">
157 {this.state.loading ? (
162 <div className="row">
163 <div className="col-12">
165 title={this.documentTitle}
166 path={this.context.router.route.match.url}
168 <h5 className="mb-2">
172 <a href={inboxRss} title="RSS" rel={relTags}>
173 <Icon icon="rss" classes="ml-2 text-muted small" />
177 type="application/atom+xml"
183 {this.state.replies.length +
184 this.state.mentions.length +
185 this.state.messages.length >
187 this.state.unreadOrAll == UnreadOrAll.Unread && (
189 className="btn btn-secondary mb-2"
190 onClick={linkEvent(this, this.markAllAsRead)}
192 {i18n.t("mark_all_as_read")}
196 {this.state.messageType == MessageType.All && this.all()}
197 {this.state.messageType == MessageType.Replies && this.replies()}
198 {this.state.messageType == MessageType.Mentions &&
200 {this.state.messageType == MessageType.Messages &&
203 page={this.state.page}
204 onChange={this.handlePageChange}
213 unreadOrAllRadios() {
215 <div className="btn-group btn-group-toggle flex-wrap mb-2">
217 className={`btn btn-outline-secondary pointer
218 ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
223 value={UnreadOrAll.Unread}
224 checked={this.state.unreadOrAll == UnreadOrAll.Unread}
225 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
230 className={`btn btn-outline-secondary pointer
231 ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
236 value={UnreadOrAll.All}
237 checked={this.state.unreadOrAll == UnreadOrAll.All}
238 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
246 messageTypeRadios() {
248 <div className="btn-group btn-group-toggle flex-wrap mb-2">
250 className={`btn btn-outline-secondary pointer
251 ${this.state.messageType == MessageType.All && "active"}
256 value={MessageType.All}
257 checked={this.state.messageType == MessageType.All}
258 onChange={linkEvent(this, this.handleMessageTypeChange)}
263 className={`btn btn-outline-secondary pointer
264 ${this.state.messageType == MessageType.Replies && "active"}
269 value={MessageType.Replies}
270 checked={this.state.messageType == MessageType.Replies}
271 onChange={linkEvent(this, this.handleMessageTypeChange)}
276 className={`btn btn-outline-secondary pointer
277 ${this.state.messageType == MessageType.Mentions && "active"}
282 value={MessageType.Mentions}
283 checked={this.state.messageType == MessageType.Mentions}
284 onChange={linkEvent(this, this.handleMessageTypeChange)}
289 className={`btn btn-outline-secondary pointer
290 ${this.state.messageType == MessageType.Messages && "active"}
295 value={MessageType.Messages}
296 checked={this.state.messageType == MessageType.Messages}
297 onChange={linkEvent(this, this.handleMessageTypeChange)}
307 <div className="mb-2">
308 <span className="mr-3">{this.unreadOrAllRadios()}</span>
309 <span className="mr-3">{this.messageTypeRadios()}</span>
311 sort={this.state.sort}
312 onChange={this.handleSortChange}
318 replyToReplyType(r: CommentReplyView): ReplyType {
320 id: r.comment_reply.id,
321 type_: ReplyEnum.Reply,
323 published: r.comment.published,
327 mentionToReplyType(r: PersonMentionView): ReplyType {
329 id: r.person_mention.id,
330 type_: ReplyEnum.Mention,
332 published: r.comment.published,
336 messageToReplyType(r: PrivateMessageView): ReplyType {
338 id: r.private_message.id,
339 type_: ReplyEnum.Message,
341 published: r.private_message.published,
345 buildCombined(): ReplyType[] {
346 const replies: ReplyType[] = this.state.replies.map(r =>
347 this.replyToReplyType(r)
349 const mentions: ReplyType[] = this.state.mentions.map(r =>
350 this.mentionToReplyType(r)
352 const messages: ReplyType[] = this.state.messages.map(r =>
353 this.messageToReplyType(r)
356 return [...replies, ...mentions, ...messages].sort((a, b) =>
357 b.published.localeCompare(a.published)
361 renderReplyType(i: ReplyType) {
363 case ReplyEnum.Reply:
368 { comment_view: i.view as CommentView, children: [], depth: 0 },
370 viewType={CommentViewType.Flat}
375 enableDownvotes={enableDownvotes(this.state.siteRes)}
376 allLanguages={this.state.siteRes.all_languages}
377 siteLanguages={this.state.siteRes.discussion_languages}
380 case ReplyEnum.Mention:
386 comment_view: i.view as PersonMentionView,
391 viewType={CommentViewType.Flat}
396 enableDownvotes={enableDownvotes(this.state.siteRes)}
397 allLanguages={this.state.siteRes.all_languages}
398 siteLanguages={this.state.siteRes.discussion_languages}
401 case ReplyEnum.Message:
405 private_message_view={i.view as PrivateMessageView}
414 return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
421 nodes={commentsToFlatNodes(this.state.replies)}
422 viewType={CommentViewType.Flat}
427 enableDownvotes={enableDownvotes(this.state.siteRes)}
428 allLanguages={this.state.siteRes.all_languages}
429 siteLanguages={this.state.siteRes.discussion_languages}
438 {this.state.mentions.map(umv => (
440 key={umv.person_mention.id}
441 nodes={[{ comment_view: umv, children: [], depth: 0 }]}
442 viewType={CommentViewType.Flat}
447 enableDownvotes={enableDownvotes(this.state.siteRes)}
448 allLanguages={this.state.siteRes.all_languages}
449 siteLanguages={this.state.siteRes.discussion_languages}
459 {this.state.messages.map(pmv => (
461 key={pmv.private_message.id}
462 private_message_view={pmv}
469 handlePageChange(page: number) {
470 this.setState({ page });
474 handleUnreadOrAllChange(i: Inbox, event: any) {
475 i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
479 handleMessageTypeChange(i: Inbox, event: any) {
480 i.setState({ messageType: Number(event.target.value), page: 1 });
484 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
485 const promises: Promise<any>[] = [];
487 const sort: CommentSortType = "New";
488 const auth = req.auth;
491 // It can be /u/me, or /username/1
492 const repliesForm: GetReplies = {
499 promises.push(req.client.getReplies(repliesForm));
501 const personMentionsForm: GetPersonMentions = {
508 promises.push(req.client.getPersonMentions(personMentionsForm));
510 const privateMessagesForm: GetPrivateMessages = {
516 promises.push(req.client.getPrivateMessages(privateMessagesForm));
523 const sort = this.state.sort;
524 const unread_only = this.state.unreadOrAll == UnreadOrAll.Unread;
525 const page = this.state.page;
526 const limit = fetchLimit;
527 const auth = myAuth();
530 const repliesForm: GetReplies = {
537 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
539 const personMentionsForm: GetPersonMentions = {
546 WebSocketService.Instance.send(
547 wsClient.getPersonMentions(personMentionsForm)
550 const privateMessagesForm: GetPrivateMessages = {
556 WebSocketService.Instance.send(
557 wsClient.getPrivateMessages(privateMessagesForm)
562 handleSortChange(val: CommentSortType) {
563 this.setState({ sort: val, page: 1 });
567 markAllAsRead(i: Inbox) {
568 const auth = myAuth();
570 WebSocketService.Instance.send(
571 wsClient.markAllAsRead({
575 i.setState({ replies: [], mentions: [], messages: [] });
576 i.setState({ combined: i.buildCombined() });
577 UserService.Instance.unreadInboxCountSub.next(0);
578 window.scrollTo(0, 0);
583 sendUnreadCount(read: boolean) {
584 const urcs = UserService.Instance.unreadInboxCountSub;
586 urcs.next(urcs.getValue() - 1);
588 urcs.next(urcs.getValue() + 1);
592 parseMessage(msg: any) {
593 const op = wsUserOp(msg);
596 toast(i18n.t(msg.error), "danger");
598 } else if (msg.reconnect) {
600 } else if (op == UserOperation.GetReplies) {
601 const data = wsJsonToRes<GetRepliesResponse>(msg);
602 this.setState({ replies: data.replies });
603 this.setState({ combined: this.buildCombined(), loading: false });
604 window.scrollTo(0, 0);
606 } else if (op == UserOperation.GetPersonMentions) {
607 const data = wsJsonToRes<GetPersonMentionsResponse>(msg);
608 this.setState({ mentions: data.mentions });
609 this.setState({ combined: this.buildCombined() });
610 window.scrollTo(0, 0);
612 } else if (op == UserOperation.GetPrivateMessages) {
613 const data = wsJsonToRes<PrivateMessagesResponse>(msg);
614 this.setState({ messages: data.private_messages });
615 this.setState({ combined: this.buildCombined() });
616 window.scrollTo(0, 0);
618 } else if (op == UserOperation.EditPrivateMessage) {
619 const data = wsJsonToRes<PrivateMessageResponse>(msg);
620 const found = this.state.messages.find(
622 m.private_message.id === data.private_message_view.private_message.id
625 const combinedView = this.state.combined.find(
626 i => i.id == data.private_message_view.private_message.id
627 )?.view as PrivateMessageView | undefined;
629 found.private_message.content = combinedView.private_message.content =
630 data.private_message_view.private_message.content;
631 found.private_message.updated = combinedView.private_message.updated =
632 data.private_message_view.private_message.updated;
635 this.setState(this.state);
636 } else if (op == UserOperation.DeletePrivateMessage) {
637 const data = wsJsonToRes<PrivateMessageResponse>(msg);
638 const found = this.state.messages.find(
640 m.private_message.id === data.private_message_view.private_message.id
643 const combinedView = this.state.combined.find(
644 i => i.id == data.private_message_view.private_message.id
645 )?.view as PrivateMessageView | undefined;
647 found.private_message.deleted = combinedView.private_message.deleted =
648 data.private_message_view.private_message.deleted;
649 found.private_message.updated = combinedView.private_message.updated =
650 data.private_message_view.private_message.updated;
653 this.setState(this.state);
654 } else if (op == UserOperation.MarkPrivateMessageAsRead) {
655 const data = wsJsonToRes<PrivateMessageResponse>(msg);
656 const found = this.state.messages.find(
658 m.private_message.id === data.private_message_view.private_message.id
662 const combinedView = this.state.combined.find(
664 i.id == data.private_message_view.private_message.id &&
665 i.type_ == ReplyEnum.Message
666 )?.view as PrivateMessageView | undefined;
668 found.private_message.updated = combinedView.private_message.updated =
669 data.private_message_view.private_message.updated;
671 // If youre in the unread view, just remove it from the list
673 this.state.unreadOrAll == UnreadOrAll.Unread &&
674 data.private_message_view.private_message.read
677 messages: this.state.messages.filter(
679 r.private_message.id !==
680 data.private_message_view.private_message.id
684 combined: this.state.combined.filter(
685 r => r.id !== data.private_message_view.private_message.id
689 found.private_message.read = combinedView.private_message.read =
690 data.private_message_view.private_message.read;
694 this.sendUnreadCount(data.private_message_view.private_message.read);
695 this.setState(this.state);
696 } else if (op == UserOperation.MarkAllAsRead) {
697 // Moved to be instant
699 op == UserOperation.EditComment ||
700 op == UserOperation.DeleteComment ||
701 op == UserOperation.RemoveComment
703 const data = wsJsonToRes<CommentResponse>(msg);
704 editCommentRes(data.comment_view, this.state.replies);
705 this.setState(this.state);
706 } else if (op == UserOperation.MarkCommentReplyAsRead) {
707 const data = wsJsonToRes<CommentReplyResponse>(msg);
709 const found = this.state.replies.find(
710 c => c.comment_reply.id == data.comment_reply_view.comment_reply.id
714 const combinedView = this.state.combined.find(
716 i.id == data.comment_reply_view.comment_reply.id &&
717 i.type_ == ReplyEnum.Reply
718 )?.view as CommentReplyView | undefined;
720 found.comment.content = combinedView.comment.content =
721 data.comment_reply_view.comment.content;
722 found.comment.updated = combinedView.comment.updated =
723 data.comment_reply_view.comment.updated;
724 found.comment.removed = combinedView.comment.removed =
725 data.comment_reply_view.comment.removed;
726 found.comment.deleted = combinedView.comment.deleted =
727 data.comment_reply_view.comment.deleted;
728 found.counts.upvotes = combinedView.counts.upvotes =
729 data.comment_reply_view.counts.upvotes;
730 found.counts.downvotes = combinedView.counts.downvotes =
731 data.comment_reply_view.counts.downvotes;
732 found.counts.score = combinedView.counts.score =
733 data.comment_reply_view.counts.score;
735 // If youre in the unread view, just remove it from the list
737 this.state.unreadOrAll == UnreadOrAll.Unread &&
738 data.comment_reply_view.comment_reply.read
741 replies: this.state.replies.filter(
743 r.comment_reply.id !==
744 data.comment_reply_view.comment_reply.id
748 combined: this.state.combined.filter(
749 r => r.id !== data.comment_reply_view.comment_reply.id
753 found.comment_reply.read = combinedView.comment_reply.read =
754 data.comment_reply_view.comment_reply.read;
758 this.sendUnreadCount(data.comment_reply_view.comment_reply.read);
759 this.setState(this.state);
760 } else if (op == UserOperation.MarkPersonMentionAsRead) {
761 const data = wsJsonToRes<PersonMentionResponse>(msg);
763 // TODO this might not be correct, it might need to use the comment id
764 const found = this.state.mentions.find(
765 c => c.person_mention.id == data.person_mention_view.person_mention.id
769 const combinedView = this.state.combined.find(
771 i.id == data.person_mention_view.person_mention.id &&
772 i.type_ == ReplyEnum.Mention
773 )?.view as PersonMentionView | undefined;
775 found.comment.content = combinedView.comment.content =
776 data.person_mention_view.comment.content;
777 found.comment.updated = combinedView.comment.updated =
778 data.person_mention_view.comment.updated;
779 found.comment.removed = combinedView.comment.removed =
780 data.person_mention_view.comment.removed;
781 found.comment.deleted = combinedView.comment.deleted =
782 data.person_mention_view.comment.deleted;
783 found.counts.upvotes = combinedView.counts.upvotes =
784 data.person_mention_view.counts.upvotes;
785 found.counts.downvotes = combinedView.counts.downvotes =
786 data.person_mention_view.counts.downvotes;
787 found.counts.score = combinedView.counts.score =
788 data.person_mention_view.counts.score;
790 // If youre in the unread view, just remove it from the list
792 this.state.unreadOrAll == UnreadOrAll.Unread &&
793 data.person_mention_view.person_mention.read
796 mentions: this.state.mentions.filter(
798 r.person_mention.id !==
799 data.person_mention_view.person_mention.id
803 combined: this.state.combined.filter(
804 r => r.id !== data.person_mention_view.person_mention.id
808 // TODO test to make sure these mentions are getting marked as read
809 found.person_mention.read = combinedView.person_mention.read =
810 data.person_mention_view.person_mention.read;
814 this.sendUnreadCount(data.person_mention_view.person_mention.read);
815 this.setState(this.state);
816 } else if (op == UserOperation.CreatePrivateMessage) {
817 const data = wsJsonToRes<PrivateMessageResponse>(msg);
818 const mui = UserService.Instance.myUserInfo;
820 data.private_message_view.recipient.id == mui?.local_user_view.person.id
822 this.state.messages.unshift(data.private_message_view);
823 this.state.combined.unshift(
824 this.messageToReplyType(data.private_message_view)
826 this.setState(this.state);
828 } else if (op == UserOperation.SaveComment) {
829 const data = wsJsonToRes<CommentResponse>(msg);
830 saveCommentRes(data.comment_view, this.state.replies);
831 this.setState(this.state);
833 } else if (op == UserOperation.CreateCommentLike) {
834 const data = wsJsonToRes<CommentResponse>(msg);
835 createCommentLikeRes(data.comment_view, this.state.replies);
836 this.setState(this.state);
837 } else if (op == UserOperation.BlockPerson) {
838 const data = wsJsonToRes<BlockPersonResponse>(msg);
839 updatePersonBlock(data);
840 } else if (op == UserOperation.CreatePostReport) {
841 const data = wsJsonToRes<PostReportResponse>(msg);
843 toast(i18n.t("report_created"));
845 } else if (op == UserOperation.CreateCommentReport) {
846 const data = wsJsonToRes<CommentReportResponse>(msg);
848 toast(i18n.t("report_created"));
850 } else if (op == UserOperation.CreatePrivateMessageReport) {
851 const data = wsJsonToRes<PrivateMessageReportResponse>(msg);
853 toast(i18n.t("report_created"));
858 isMention(view: any): view is PersonMentionView {
859 return (view as PersonMentionView).person_mention !== undefined;
862 isReply(view: any): view is CommentReplyView {
863 return (view as CommentReplyView).comment_reply !== undefined;