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";
49 import { CommentNodes } from "../comment/comment-nodes";
50 import { CommentSortSelect } from "../common/comment-sort-select";
51 import { HtmlTags } from "../common/html-tags";
52 import { Icon, Spinner } from "../common/icon";
53 import { Paginator } from "../common/paginator";
54 import { PrivateMessage } from "../private_message/private-message";
75 repliesResponse: GetRepliesResponse;
76 personMentionsResponse: GetPersonMentionsResponse;
77 privateMessagesResponse: PrivateMessagesResponse;
83 view: CommentView | PrivateMessageView | PersonMentionView | CommentReplyView;
87 interface InboxState {
88 unreadOrAll: UnreadOrAll;
89 messageType: MessageType;
90 replies: CommentReplyView[];
91 mentions: PersonMentionView[];
92 messages: PrivateMessageView[];
93 combined: ReplyType[];
94 sort: CommentSortType;
96 siteRes: GetSiteResponse;
100 export class Inbox extends Component<any, InboxState> {
101 private isoData = setIsoData<InboxData>(this.context);
102 private subscription?: Subscription;
103 state: InboxState = {
104 unreadOrAll: UnreadOrAll.Unread,
105 messageType: MessageType.All,
112 siteRes: this.isoData.site_res,
116 constructor(props: any, context: any) {
117 super(props, context);
119 this.handleSortChange = this.handleSortChange.bind(this);
120 this.handlePageChange = this.handlePageChange.bind(this);
122 this.parseMessage = this.parseMessage.bind(this);
123 this.subscription = wsSubscribe(this.parseMessage);
125 // Only fetch the data if coming from another route
126 if (this.isoData.path === this.context.router.route.match.url) {
128 personMentionsResponse,
129 privateMessagesResponse,
131 } = this.isoData.routeData;
135 replies: repliesResponse.replies ?? [],
136 mentions: personMentionsResponse.mentions ?? [],
137 messages: privateMessagesResponse.private_messages ?? [],
140 this.state = { ...this.state, combined: this.buildCombined() };
146 componentWillUnmount() {
148 this.subscription?.unsubscribe();
152 get documentTitle(): string {
153 let mui = UserService.Instance.myUserInfo;
155 ? `@${mui.local_user_view.person.name} ${i18n.t("inbox")} - ${
156 this.state.siteRes.site_view.site.name
163 let inboxRss = auth ? `/feeds/inbox/${auth}.xml` : undefined;
165 <div className="container-lg">
166 {this.state.loading ? (
171 <div className="row">
172 <div className="col-12">
174 title={this.documentTitle}
175 path={this.context.router.route.match.url}
177 <h5 className="mb-2">
181 <a href={inboxRss} title="RSS" rel={relTags}>
182 <Icon icon="rss" classes="ml-2 text-muted small" />
186 type="application/atom+xml"
192 {this.state.replies.length +
193 this.state.mentions.length +
194 this.state.messages.length >
196 this.state.unreadOrAll == UnreadOrAll.Unread && (
198 className="btn btn-secondary mb-2"
199 onClick={linkEvent(this, this.markAllAsRead)}
201 {i18n.t("mark_all_as_read")}
205 {this.state.messageType == MessageType.All && this.all()}
206 {this.state.messageType == MessageType.Replies && this.replies()}
207 {this.state.messageType == MessageType.Mentions &&
209 {this.state.messageType == MessageType.Messages &&
212 page={this.state.page}
213 onChange={this.handlePageChange}
222 unreadOrAllRadios() {
224 <div className="btn-group btn-group-toggle flex-wrap mb-2">
226 className={`btn btn-outline-secondary pointer
227 ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
232 value={UnreadOrAll.Unread}
233 checked={this.state.unreadOrAll == UnreadOrAll.Unread}
234 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
239 className={`btn btn-outline-secondary pointer
240 ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
245 value={UnreadOrAll.All}
246 checked={this.state.unreadOrAll == UnreadOrAll.All}
247 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
255 messageTypeRadios() {
257 <div className="btn-group btn-group-toggle flex-wrap mb-2">
259 className={`btn btn-outline-secondary pointer
260 ${this.state.messageType == MessageType.All && "active"}
265 value={MessageType.All}
266 checked={this.state.messageType == MessageType.All}
267 onChange={linkEvent(this, this.handleMessageTypeChange)}
272 className={`btn btn-outline-secondary pointer
273 ${this.state.messageType == MessageType.Replies && "active"}
278 value={MessageType.Replies}
279 checked={this.state.messageType == MessageType.Replies}
280 onChange={linkEvent(this, this.handleMessageTypeChange)}
285 className={`btn btn-outline-secondary pointer
286 ${this.state.messageType == MessageType.Mentions && "active"}
291 value={MessageType.Mentions}
292 checked={this.state.messageType == MessageType.Mentions}
293 onChange={linkEvent(this, this.handleMessageTypeChange)}
298 className={`btn btn-outline-secondary pointer
299 ${this.state.messageType == MessageType.Messages && "active"}
304 value={MessageType.Messages}
305 checked={this.state.messageType == MessageType.Messages}
306 onChange={linkEvent(this, this.handleMessageTypeChange)}
316 <div className="mb-2">
317 <span className="mr-3">{this.unreadOrAllRadios()}</span>
318 <span className="mr-3">{this.messageTypeRadios()}</span>
320 sort={this.state.sort}
321 onChange={this.handleSortChange}
327 replyToReplyType(r: CommentReplyView): ReplyType {
329 id: r.comment_reply.id,
330 type_: ReplyEnum.Reply,
332 published: r.comment.published,
336 mentionToReplyType(r: PersonMentionView): ReplyType {
338 id: r.person_mention.id,
339 type_: ReplyEnum.Mention,
341 published: r.comment.published,
345 messageToReplyType(r: PrivateMessageView): ReplyType {
347 id: r.private_message.id,
348 type_: ReplyEnum.Message,
350 published: r.private_message.published,
354 buildCombined(): ReplyType[] {
355 let replies: ReplyType[] = this.state.replies.map(r =>
356 this.replyToReplyType(r)
358 let mentions: ReplyType[] = this.state.mentions.map(r =>
359 this.mentionToReplyType(r)
361 let messages: ReplyType[] = this.state.messages.map(r =>
362 this.messageToReplyType(r)
365 return [...replies, ...mentions, ...messages].sort((a, b) =>
366 b.published.localeCompare(a.published)
370 renderReplyType(i: ReplyType) {
372 case ReplyEnum.Reply:
377 { comment_view: i.view as CommentView, children: [], depth: 0 },
379 viewType={CommentViewType.Flat}
384 enableDownvotes={enableDownvotes(this.state.siteRes)}
385 allLanguages={this.state.siteRes.all_languages}
386 siteLanguages={this.state.siteRes.discussion_languages}
389 case ReplyEnum.Mention:
395 comment_view: i.view as PersonMentionView,
400 viewType={CommentViewType.Flat}
405 enableDownvotes={enableDownvotes(this.state.siteRes)}
406 allLanguages={this.state.siteRes.all_languages}
407 siteLanguages={this.state.siteRes.discussion_languages}
410 case ReplyEnum.Message:
414 private_message_view={i.view as PrivateMessageView}
423 return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
430 nodes={commentsToFlatNodes(this.state.replies)}
431 viewType={CommentViewType.Flat}
436 enableDownvotes={enableDownvotes(this.state.siteRes)}
437 allLanguages={this.state.siteRes.all_languages}
438 siteLanguages={this.state.siteRes.discussion_languages}
447 {this.state.mentions.map(umv => (
449 key={umv.person_mention.id}
450 nodes={[{ comment_view: umv, children: [], depth: 0 }]}
451 viewType={CommentViewType.Flat}
456 enableDownvotes={enableDownvotes(this.state.siteRes)}
457 allLanguages={this.state.siteRes.all_languages}
458 siteLanguages={this.state.siteRes.discussion_languages}
468 {this.state.messages.map(pmv => (
470 key={pmv.private_message.id}
471 private_message_view={pmv}
478 handlePageChange(page: number) {
479 this.setState({ page });
483 handleUnreadOrAllChange(i: Inbox, event: any) {
484 i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
488 handleMessageTypeChange(i: Inbox, event: any) {
489 i.setState({ messageType: Number(event.target.value), page: 1 });
493 static fetchInitialData({
496 }: InitialFetchRequest): WithPromiseKeys<InboxData> {
497 const sort: CommentSortType = "New";
499 // It can be /u/me, or /username/1
500 const repliesForm: GetReplies = {
505 auth: auth as string,
508 const personMentionsForm: GetPersonMentions = {
513 auth: auth as string,
516 const privateMessagesForm: GetPrivateMessages = {
520 auth: auth as string,
524 privateMessagesResponse: client.getPrivateMessages(privateMessagesForm),
525 personMentionsResponse: client.getPersonMentions(personMentionsForm),
526 repliesResponse: client.getReplies(repliesForm),
531 const { sort, page, unreadOrAll } = this.state;
532 const unread_only = unreadOrAll === UnreadOrAll.Unread;
533 const limit = fetchLimit;
534 const auth = myAuth();
537 const repliesForm: GetReplies = {
544 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
546 const personMentionsForm: GetPersonMentions = {
553 WebSocketService.Instance.send(
554 wsClient.getPersonMentions(personMentionsForm)
557 const privateMessagesForm: GetPrivateMessages = {
563 WebSocketService.Instance.send(
564 wsClient.getPrivateMessages(privateMessagesForm)
569 handleSortChange(val: CommentSortType) {
570 this.setState({ sort: val, page: 1 });
574 markAllAsRead(i: Inbox) {
577 WebSocketService.Instance.send(
578 wsClient.markAllAsRead({
582 i.setState({ replies: [], mentions: [], messages: [] });
583 i.setState({ combined: i.buildCombined() });
584 UserService.Instance.unreadInboxCountSub.next(0);
585 window.scrollTo(0, 0);
590 sendUnreadCount(read: boolean) {
591 let urcs = UserService.Instance.unreadInboxCountSub;
593 urcs.next(urcs.getValue() - 1);
595 urcs.next(urcs.getValue() + 1);
599 parseMessage(msg: any) {
600 let op = wsUserOp(msg);
603 toast(i18n.t(msg.error), "danger");
605 } else if (msg.reconnect) {
607 } else if (op == UserOperation.GetReplies) {
608 let data = wsJsonToRes<GetRepliesResponse>(msg);
609 this.setState({ replies: data.replies });
610 this.setState({ combined: this.buildCombined(), loading: false });
611 window.scrollTo(0, 0);
613 } else if (op == UserOperation.GetPersonMentions) {
614 let data = wsJsonToRes<GetPersonMentionsResponse>(msg);
615 this.setState({ mentions: data.mentions });
616 this.setState({ combined: this.buildCombined() });
617 window.scrollTo(0, 0);
619 } else if (op == UserOperation.GetPrivateMessages) {
620 let data = wsJsonToRes<PrivateMessagesResponse>(msg);
621 this.setState({ messages: data.private_messages });
622 this.setState({ combined: this.buildCombined() });
623 window.scrollTo(0, 0);
625 } else if (op == UserOperation.EditPrivateMessage) {
626 let data = wsJsonToRes<PrivateMessageResponse>(msg);
627 let found = this.state.messages.find(
629 m.private_message.id === data.private_message_view.private_message.id
632 let combinedView = this.state.combined.find(
633 i => i.id == data.private_message_view.private_message.id
634 )?.view as PrivateMessageView | undefined;
636 found.private_message.content = combinedView.private_message.content =
637 data.private_message_view.private_message.content;
638 found.private_message.updated = combinedView.private_message.updated =
639 data.private_message_view.private_message.updated;
642 this.setState(this.state);
643 } else if (op == UserOperation.DeletePrivateMessage) {
644 let data = wsJsonToRes<PrivateMessageResponse>(msg);
645 let found = this.state.messages.find(
647 m.private_message.id === data.private_message_view.private_message.id
650 let combinedView = this.state.combined.find(
651 i => i.id == data.private_message_view.private_message.id
652 )?.view as PrivateMessageView | undefined;
654 found.private_message.deleted = combinedView.private_message.deleted =
655 data.private_message_view.private_message.deleted;
656 found.private_message.updated = combinedView.private_message.updated =
657 data.private_message_view.private_message.updated;
660 this.setState(this.state);
661 } else if (op == UserOperation.MarkPrivateMessageAsRead) {
662 let data = wsJsonToRes<PrivateMessageResponse>(msg);
663 let found = this.state.messages.find(
665 m.private_message.id === data.private_message_view.private_message.id
669 let combinedView = this.state.combined.find(
671 i.id == data.private_message_view.private_message.id &&
672 i.type_ == ReplyEnum.Message
673 )?.view as PrivateMessageView | undefined;
675 found.private_message.updated = combinedView.private_message.updated =
676 data.private_message_view.private_message.updated;
678 // If youre in the unread view, just remove it from the list
680 this.state.unreadOrAll == UnreadOrAll.Unread &&
681 data.private_message_view.private_message.read
684 messages: this.state.messages.filter(
686 r.private_message.id !==
687 data.private_message_view.private_message.id
691 combined: this.state.combined.filter(
692 r => r.id !== data.private_message_view.private_message.id
696 found.private_message.read = combinedView.private_message.read =
697 data.private_message_view.private_message.read;
701 this.sendUnreadCount(data.private_message_view.private_message.read);
702 this.setState(this.state);
703 } else if (op == UserOperation.MarkAllAsRead) {
704 // Moved to be instant
706 op == UserOperation.EditComment ||
707 op == UserOperation.DeleteComment ||
708 op == UserOperation.RemoveComment
710 let data = wsJsonToRes<CommentResponse>(msg);
711 editCommentRes(data.comment_view, this.state.replies);
712 this.setState(this.state);
713 } else if (op == UserOperation.MarkCommentReplyAsRead) {
714 let data = wsJsonToRes<CommentReplyResponse>(msg);
716 let found = this.state.replies.find(
717 c => c.comment_reply.id == data.comment_reply_view.comment_reply.id
721 let combinedView = this.state.combined.find(
723 i.id == data.comment_reply_view.comment_reply.id &&
724 i.type_ == ReplyEnum.Reply
725 )?.view as CommentReplyView | undefined;
727 found.comment.content = combinedView.comment.content =
728 data.comment_reply_view.comment.content;
729 found.comment.updated = combinedView.comment.updated =
730 data.comment_reply_view.comment.updated;
731 found.comment.removed = combinedView.comment.removed =
732 data.comment_reply_view.comment.removed;
733 found.comment.deleted = combinedView.comment.deleted =
734 data.comment_reply_view.comment.deleted;
735 found.counts.upvotes = combinedView.counts.upvotes =
736 data.comment_reply_view.counts.upvotes;
737 found.counts.downvotes = combinedView.counts.downvotes =
738 data.comment_reply_view.counts.downvotes;
739 found.counts.score = combinedView.counts.score =
740 data.comment_reply_view.counts.score;
742 // If youre in the unread view, just remove it from the list
744 this.state.unreadOrAll == UnreadOrAll.Unread &&
745 data.comment_reply_view.comment_reply.read
748 replies: this.state.replies.filter(
750 r.comment_reply.id !==
751 data.comment_reply_view.comment_reply.id
755 combined: this.state.combined.filter(
756 r => r.id !== data.comment_reply_view.comment_reply.id
760 found.comment_reply.read = combinedView.comment_reply.read =
761 data.comment_reply_view.comment_reply.read;
765 this.sendUnreadCount(data.comment_reply_view.comment_reply.read);
766 this.setState(this.state);
767 } else if (op == UserOperation.MarkPersonMentionAsRead) {
768 let data = wsJsonToRes<PersonMentionResponse>(msg);
770 // TODO this might not be correct, it might need to use the comment id
771 let found = this.state.mentions.find(
772 c => c.person_mention.id == data.person_mention_view.person_mention.id
776 let combinedView = this.state.combined.find(
778 i.id == data.person_mention_view.person_mention.id &&
779 i.type_ == ReplyEnum.Mention
780 )?.view as PersonMentionView | undefined;
782 found.comment.content = combinedView.comment.content =
783 data.person_mention_view.comment.content;
784 found.comment.updated = combinedView.comment.updated =
785 data.person_mention_view.comment.updated;
786 found.comment.removed = combinedView.comment.removed =
787 data.person_mention_view.comment.removed;
788 found.comment.deleted = combinedView.comment.deleted =
789 data.person_mention_view.comment.deleted;
790 found.counts.upvotes = combinedView.counts.upvotes =
791 data.person_mention_view.counts.upvotes;
792 found.counts.downvotes = combinedView.counts.downvotes =
793 data.person_mention_view.counts.downvotes;
794 found.counts.score = combinedView.counts.score =
795 data.person_mention_view.counts.score;
797 // If youre in the unread view, just remove it from the list
799 this.state.unreadOrAll == UnreadOrAll.Unread &&
800 data.person_mention_view.person_mention.read
803 mentions: this.state.mentions.filter(
805 r.person_mention.id !==
806 data.person_mention_view.person_mention.id
810 combined: this.state.combined.filter(
811 r => r.id !== data.person_mention_view.person_mention.id
815 // TODO test to make sure these mentions are getting marked as read
816 found.person_mention.read = combinedView.person_mention.read =
817 data.person_mention_view.person_mention.read;
821 this.sendUnreadCount(data.person_mention_view.person_mention.read);
822 this.setState(this.state);
823 } else if (op == UserOperation.CreatePrivateMessage) {
824 let data = wsJsonToRes<PrivateMessageResponse>(msg);
825 let mui = UserService.Instance.myUserInfo;
827 data.private_message_view.recipient.id == mui?.local_user_view.person.id
829 this.state.messages.unshift(data.private_message_view);
830 this.state.combined.unshift(
831 this.messageToReplyType(data.private_message_view)
833 this.setState(this.state);
835 } else if (op == UserOperation.SaveComment) {
836 let data = wsJsonToRes<CommentResponse>(msg);
837 saveCommentRes(data.comment_view, this.state.replies);
838 this.setState(this.state);
840 } else if (op == UserOperation.CreateCommentLike) {
841 let data = wsJsonToRes<CommentResponse>(msg);
842 createCommentLikeRes(data.comment_view, this.state.replies);
843 this.setState(this.state);
844 } else if (op == UserOperation.BlockPerson) {
845 let data = wsJsonToRes<BlockPersonResponse>(msg);
846 updatePersonBlock(data);
847 } else if (op == UserOperation.CreatePostReport) {
848 let data = wsJsonToRes<PostReportResponse>(msg);
850 toast(i18n.t("report_created"));
852 } else if (op == UserOperation.CreateCommentReport) {
853 let data = wsJsonToRes<CommentReportResponse>(msg);
855 toast(i18n.t("report_created"));
857 } else if (op == UserOperation.CreatePrivateMessageReport) {
858 let data = wsJsonToRes<PrivateMessageReportResponse>(msg);
860 toast(i18n.t("report_created"));
865 isMention(view: any): view is PersonMentionView {
866 return (view as PersonMentionView).person_mention !== undefined;
869 isReply(view: any): view is CommentReplyView {
870 return (view as CommentReplyView).comment_reply !== undefined;