1 import { None, Some } from "@sniptt/monads";
2 import { Component, linkEvent } from "inferno";
9 GetPersonMentionsResponse,
14 PersonMentionResponse,
17 PrivateMessageResponse,
18 PrivateMessagesResponse,
24 } from "lemmy-js-client";
25 import { Subscription } from "rxjs";
26 import { i18n } from "../../i18next";
27 import { InitialFetchRequest } from "../../interfaces";
28 import { UserService, WebSocketService } from "../../services";
46 import { CommentNodes } from "../comment/comment-nodes";
47 import { HtmlTags } from "../common/html-tags";
48 import { Icon, Spinner } from "../common/icon";
49 import { Paginator } from "../common/paginator";
50 import { SortSelect } from "../common/sort-select";
51 import { PrivateMessage } from "../private_message/private-message";
73 view: CommentView | PrivateMessageView | PersonMentionView;
77 interface InboxState {
78 unreadOrAll: UnreadOrAll;
79 messageType: MessageType;
80 replies: CommentView[];
81 mentions: PersonMentionView[];
82 messages: PrivateMessageView[];
83 combined: ReplyType[];
86 siteRes: GetSiteResponse;
90 export class Inbox extends Component<any, InboxState> {
91 private isoData = setIsoData(
94 GetPersonMentionsResponse,
95 PrivateMessagesResponse
97 private subscription: Subscription;
98 private emptyState: InboxState = {
99 unreadOrAll: UnreadOrAll.Unread,
100 messageType: MessageType.All,
107 siteRes: this.isoData.site_res,
111 constructor(props: any, context: any) {
112 super(props, context);
114 this.state = this.emptyState;
115 this.handleSortChange = this.handleSortChange.bind(this);
116 this.handlePageChange = this.handlePageChange.bind(this);
118 if (UserService.Instance.myUserInfo.isNone() && isBrowser()) {
119 toast(i18n.t("not_logged_in"), "danger");
120 this.context.router.history.push(`/login`);
123 this.parseMessage = this.parseMessage.bind(this);
124 this.subscription = wsSubscribe(this.parseMessage);
126 // Only fetch the data if coming from another route
127 if (this.isoData.path == this.context.router.route.match.url) {
129 (this.isoData.routeData[0] as GetRepliesResponse).replies || [];
130 this.state.mentions =
131 (this.isoData.routeData[1] as GetPersonMentionsResponse).mentions || [];
132 this.state.messages =
133 (this.isoData.routeData[2] as PrivateMessagesResponse)
134 .private_messages || [];
135 this.state.combined = this.buildCombined();
136 this.state.loading = false;
142 componentWillUnmount() {
144 this.subscription.unsubscribe();
148 get documentTitle(): string {
149 return this.state.siteRes.site_view.match({
151 UserService.Instance.myUserInfo.match({
153 `@${mui.local_user_view.person.name} ${i18n.t("inbox")} - ${
163 let inboxRss = auth()
165 .map(a => `/feeds/inbox/${a}.xml`);
167 <div class="container">
168 {this.state.loading ? (
176 title={this.documentTitle}
177 path={this.context.router.route.match.url}
186 <a href={rss} title="RSS" rel={relTags}>
187 <Icon icon="rss" classes="ml-2 text-muted small" />
191 type="application/atom+xml"
199 {this.state.replies.length +
200 this.state.mentions.length +
201 this.state.messages.length >
203 this.state.unreadOrAll == UnreadOrAll.Unread && (
205 class="btn btn-secondary mb-2"
206 onClick={linkEvent(this, this.markAllAsRead)}
208 {i18n.t("mark_all_as_read")}
212 {this.state.messageType == MessageType.All && this.all()}
213 {this.state.messageType == MessageType.Replies && this.replies()}
214 {this.state.messageType == MessageType.Mentions &&
216 {this.state.messageType == MessageType.Messages &&
219 page={this.state.page}
220 onChange={this.handlePageChange}
229 unreadOrAllRadios() {
231 <div class="btn-group btn-group-toggle flex-wrap mb-2">
233 className={`btn btn-outline-secondary pointer
234 ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
239 value={UnreadOrAll.Unread}
240 checked={this.state.unreadOrAll == UnreadOrAll.Unread}
241 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
246 className={`btn btn-outline-secondary pointer
247 ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
252 value={UnreadOrAll.All}
253 checked={this.state.unreadOrAll == UnreadOrAll.All}
254 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
262 messageTypeRadios() {
264 <div class="btn-group btn-group-toggle flex-wrap mb-2">
266 className={`btn btn-outline-secondary pointer
267 ${this.state.messageType == MessageType.All && "active"}
272 value={MessageType.All}
273 checked={this.state.messageType == MessageType.All}
274 onChange={linkEvent(this, this.handleMessageTypeChange)}
279 className={`btn btn-outline-secondary pointer
280 ${this.state.messageType == MessageType.Replies && "active"}
285 value={MessageType.Replies}
286 checked={this.state.messageType == MessageType.Replies}
287 onChange={linkEvent(this, this.handleMessageTypeChange)}
292 className={`btn btn-outline-secondary pointer
293 ${this.state.messageType == MessageType.Mentions && "active"}
298 value={MessageType.Mentions}
299 checked={this.state.messageType == MessageType.Mentions}
300 onChange={linkEvent(this, this.handleMessageTypeChange)}
305 className={`btn btn-outline-secondary pointer
306 ${this.state.messageType == MessageType.Messages && "active"}
311 value={MessageType.Messages}
312 checked={this.state.messageType == MessageType.Messages}
313 onChange={linkEvent(this, this.handleMessageTypeChange)}
323 <div className="mb-2">
324 <span class="mr-3">{this.unreadOrAllRadios()}</span>
325 <span class="mr-3">{this.messageTypeRadios()}</span>
327 sort={this.state.sort}
328 onChange={this.handleSortChange}
336 replyToReplyType(r: CommentView): ReplyType {
339 type_: ReplyEnum.Reply,
341 published: r.comment.published,
345 mentionToReplyType(r: PersonMentionView): ReplyType {
347 id: r.person_mention.id,
348 type_: ReplyEnum.Mention,
350 published: r.comment.published,
354 messageToReplyType(r: PrivateMessageView): ReplyType {
356 id: r.private_message.id,
357 type_: ReplyEnum.Message,
359 published: r.private_message.published,
363 buildCombined(): ReplyType[] {
364 let replies: ReplyType[] = this.state.replies.map(r =>
365 this.replyToReplyType(r)
367 let mentions: ReplyType[] = this.state.mentions.map(r =>
368 this.mentionToReplyType(r)
370 let messages: ReplyType[] = this.state.messages.map(r =>
371 this.messageToReplyType(r)
374 return [...replies, ...mentions, ...messages].sort((a, b) =>
375 b.published.localeCompare(a.published)
379 renderReplyType(i: ReplyType) {
381 case ReplyEnum.Reply:
385 nodes={[{ comment_view: i.view as CommentView }]}
388 maxCommentsShown={None}
393 enableDownvotes={enableDownvotes(this.state.siteRes)}
396 case ReplyEnum.Mention:
400 nodes={[{ comment_view: i.view as PersonMentionView }]}
403 maxCommentsShown={None}
408 enableDownvotes={enableDownvotes(this.state.siteRes)}
411 case ReplyEnum.Message:
415 private_message_view={i.view as PrivateMessageView}
424 return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
431 nodes={commentsToFlatNodes(this.state.replies)}
434 maxCommentsShown={None}
439 enableDownvotes={enableDownvotes(this.state.siteRes)}
448 {this.state.mentions.map(umv => (
450 key={umv.person_mention.id}
451 nodes={[{ comment_view: umv }]}
454 maxCommentsShown={None}
459 enableDownvotes={enableDownvotes(this.state.siteRes)}
469 {this.state.messages.map(pmv => (
471 key={pmv.private_message.id}
472 private_message_view={pmv}
479 handlePageChange(page: number) {
480 this.setState({ page });
484 handleUnreadOrAllChange(i: Inbox, event: any) {
485 i.state.unreadOrAll = Number(event.target.value);
491 handleMessageTypeChange(i: Inbox, event: any) {
492 i.state.messageType = Number(event.target.value);
498 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
499 let promises: Promise<any>[] = [];
501 // It can be /u/me, or /username/1
502 let repliesForm = new GetReplies({
503 sort: Some(SortType.New),
504 unread_only: Some(true),
506 limit: Some(fetchLimit),
507 auth: req.auth.unwrap(),
509 promises.push(req.client.getReplies(repliesForm));
511 let personMentionsForm = new GetPersonMentions({
512 sort: Some(SortType.New),
513 unread_only: Some(true),
515 limit: Some(fetchLimit),
516 auth: req.auth.unwrap(),
518 promises.push(req.client.getPersonMentions(personMentionsForm));
520 let privateMessagesForm = new GetPrivateMessages({
521 unread_only: Some(true),
523 limit: Some(fetchLimit),
524 auth: req.auth.unwrap(),
526 promises.push(req.client.getPrivateMessages(privateMessagesForm));
532 let sort = Some(this.state.sort);
533 let unread_only = Some(this.state.unreadOrAll == UnreadOrAll.Unread);
534 let page = Some(this.state.page);
535 let limit = Some(fetchLimit);
537 let repliesForm = new GetReplies({
542 auth: auth().unwrap(),
544 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
546 let personMentionsForm = new GetPersonMentions({
551 auth: auth().unwrap(),
553 WebSocketService.Instance.send(
554 wsClient.getPersonMentions(personMentionsForm)
557 let privateMessagesForm = new GetPrivateMessages({
561 auth: auth().unwrap(),
563 WebSocketService.Instance.send(
564 wsClient.getPrivateMessages(privateMessagesForm)
568 handleSortChange(val: SortType) {
569 this.state.sort = val;
571 this.setState(this.state);
575 markAllAsRead(i: Inbox) {
576 WebSocketService.Instance.send(
577 wsClient.markAllAsRead({
578 auth: auth().unwrap(),
581 i.state.replies = [];
582 i.state.mentions = [];
583 i.state.messages = [];
584 UserService.Instance.unreadInboxCountSub.next(0);
585 window.scrollTo(0, 0);
589 sendUnreadCount(read: boolean) {
590 let urcs = UserService.Instance.unreadInboxCountSub;
592 urcs.next(urcs.getValue() - 1);
594 urcs.next(urcs.getValue() + 1);
598 parseMessage(msg: any) {
599 let op = wsUserOp(msg);
602 toast(i18n.t(msg.error), "danger");
604 } else if (msg.reconnect) {
606 } else if (op == UserOperation.GetReplies) {
607 let data = wsJsonToRes<GetRepliesResponse>(msg, GetRepliesResponse);
608 this.state.replies = data.replies;
609 this.state.combined = this.buildCombined();
610 this.state.loading = false;
611 window.scrollTo(0, 0);
612 this.setState(this.state);
614 } else if (op == UserOperation.GetPersonMentions) {
615 let data = wsJsonToRes<GetPersonMentionsResponse>(
617 GetPersonMentionsResponse
619 this.state.mentions = data.mentions;
620 this.state.combined = this.buildCombined();
621 window.scrollTo(0, 0);
622 this.setState(this.state);
624 } else if (op == UserOperation.GetPrivateMessages) {
625 let data = wsJsonToRes<PrivateMessagesResponse>(
627 PrivateMessagesResponse
629 this.state.messages = data.private_messages;
630 this.state.combined = this.buildCombined();
631 window.scrollTo(0, 0);
632 this.setState(this.state);
634 } else if (op == UserOperation.EditPrivateMessage) {
635 let data = wsJsonToRes<PrivateMessageResponse>(
637 PrivateMessageResponse
639 let found: PrivateMessageView = this.state.messages.find(
641 m.private_message.id === data.private_message_view.private_message.id
644 let combinedView = this.state.combined.find(
645 i => i.id == data.private_message_view.private_message.id
646 ).view as PrivateMessageView;
647 found.private_message.content = combinedView.private_message.content =
648 data.private_message_view.private_message.content;
649 found.private_message.updated = combinedView.private_message.updated =
650 data.private_message_view.private_message.updated;
652 this.setState(this.state);
653 } else if (op == UserOperation.DeletePrivateMessage) {
654 let data = wsJsonToRes<PrivateMessageResponse>(
656 PrivateMessageResponse
658 let found: PrivateMessageView = this.state.messages.find(
660 m.private_message.id === data.private_message_view.private_message.id
663 let combinedView = this.state.combined.find(
664 i => i.id == data.private_message_view.private_message.id
665 ).view as PrivateMessageView;
666 found.private_message.deleted = combinedView.private_message.deleted =
667 data.private_message_view.private_message.deleted;
668 found.private_message.updated = combinedView.private_message.updated =
669 data.private_message_view.private_message.updated;
671 this.setState(this.state);
672 } else if (op == UserOperation.MarkPrivateMessageAsRead) {
673 let data = wsJsonToRes<PrivateMessageResponse>(
675 PrivateMessageResponse
677 let found: PrivateMessageView = this.state.messages.find(
679 m.private_message.id === data.private_message_view.private_message.id
683 let combinedView = this.state.combined.find(
684 i => i.id == data.private_message_view.private_message.id
685 ).view as PrivateMessageView;
686 found.private_message.updated = combinedView.private_message.updated =
687 data.private_message_view.private_message.updated;
689 // If youre in the unread view, just remove it from the list
691 this.state.unreadOrAll == UnreadOrAll.Unread &&
692 data.private_message_view.private_message.read
694 this.state.messages = this.state.messages.filter(
696 r.private_message.id !==
697 data.private_message_view.private_message.id
699 this.state.combined = this.state.combined.filter(
700 r => r.id !== data.private_message_view.private_message.id
703 found.private_message.read = combinedView.private_message.read =
704 data.private_message_view.private_message.read;
707 this.sendUnreadCount(data.private_message_view.private_message.read);
708 this.setState(this.state);
709 } else if (op == UserOperation.MarkAllAsRead) {
710 // Moved to be instant
712 op == UserOperation.EditComment ||
713 op == UserOperation.DeleteComment ||
714 op == UserOperation.RemoveComment
716 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
717 editCommentRes(data.comment_view, this.state.replies);
718 this.setState(this.state);
719 } else if (op == UserOperation.MarkCommentAsRead) {
720 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
722 // If youre in the unread view, just remove it from the list
724 this.state.unreadOrAll == UnreadOrAll.Unread &&
725 data.comment_view.comment.read
727 this.state.replies = this.state.replies.filter(
728 r => r.comment.id !== data.comment_view.comment.id
730 this.state.combined = this.state.combined.filter(
731 r => r.id !== data.comment_view.comment.id
734 let found = this.state.replies.find(
735 c => c.comment.id == data.comment_view.comment.id
737 let combinedView = this.state.combined.find(
738 i => i.id == data.comment_view.comment.id
739 ).view as CommentView;
740 found.comment.read = combinedView.comment.read =
741 data.comment_view.comment.read;
744 this.sendUnreadCount(data.comment_view.comment.read);
745 this.setState(this.state);
747 } else if (op == UserOperation.MarkPersonMentionAsRead) {
748 let data = wsJsonToRes<PersonMentionResponse>(msg, PersonMentionResponse);
750 // TODO this might not be correct, it might need to use the comment id
751 let found = this.state.mentions.find(
752 c => c.person_mention.id == data.person_mention_view.person_mention.id
756 let combinedView = this.state.combined.find(
757 i => i.id == data.person_mention_view.person_mention.id
758 ).view as PersonMentionView;
759 found.comment.content = combinedView.comment.content =
760 data.person_mention_view.comment.content;
761 found.comment.updated = combinedView.comment.updated =
762 data.person_mention_view.comment.updated;
763 found.comment.removed = combinedView.comment.removed =
764 data.person_mention_view.comment.removed;
765 found.comment.deleted = combinedView.comment.deleted =
766 data.person_mention_view.comment.deleted;
767 found.counts.upvotes = combinedView.counts.upvotes =
768 data.person_mention_view.counts.upvotes;
769 found.counts.downvotes = combinedView.counts.downvotes =
770 data.person_mention_view.counts.downvotes;
771 found.counts.score = combinedView.counts.score =
772 data.person_mention_view.counts.score;
774 // If youre in the unread view, just remove it from the list
776 this.state.unreadOrAll == UnreadOrAll.Unread &&
777 data.person_mention_view.person_mention.read
779 this.state.mentions = this.state.mentions.filter(
781 r.person_mention.id !== data.person_mention_view.person_mention.id
783 this.state.combined = this.state.combined.filter(
784 r => r.id !== data.person_mention_view.person_mention.id
787 // TODO test to make sure these mentions are getting marked as read
788 found.person_mention.read = combinedView.person_mention.read =
789 data.person_mention_view.person_mention.read;
792 this.sendUnreadCount(data.person_mention_view.person_mention.read);
793 this.setState(this.state);
794 } else if (op == UserOperation.CreateComment) {
795 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
797 UserService.Instance.myUserInfo.match({
799 if (data.recipient_ids.includes(mui.local_user_view.local_user.id)) {
800 this.state.replies.unshift(data.comment_view);
801 this.state.combined.unshift(
802 this.replyToReplyType(data.comment_view)
804 this.setState(this.state);
806 data.comment_view.creator.id == mui.local_user_view.person.id
808 // If youre in the unread view, just remove it from the list
809 if (this.state.unreadOrAll == UnreadOrAll.Unread) {
810 this.state.replies = this.state.replies.filter(
813 data.comment_view.comment.parent_id.unwrapOr(0)
815 this.state.mentions = this.state.mentions.filter(
818 data.comment_view.comment.parent_id.unwrapOr(0)
820 this.state.combined = this.state.combined.filter(r => {
821 if (this.isMention(r.view))
823 r.view.comment.id !==
824 data.comment_view.comment.parent_id.unwrapOr(0)
828 r.id !== data.comment_view.comment.parent_id.unwrapOr(0)
832 let mention_found = this.state.mentions.find(
835 data.comment_view.comment.parent_id.unwrapOr(0)
838 mention_found.person_mention.read = true;
840 let reply_found = this.state.replies.find(
843 data.comment_view.comment.parent_id.unwrapOr(0)
846 reply_found.comment.read = true;
848 this.state.combined = this.buildCombined();
850 this.sendUnreadCount(true);
851 this.setState(this.state);
853 // TODO this seems wrong, you should be using form_id
854 toast(i18n.t("reply_sent"));
859 } else if (op == UserOperation.CreatePrivateMessage) {
860 let data = wsJsonToRes<PrivateMessageResponse>(
862 PrivateMessageResponse
864 UserService.Instance.myUserInfo.match({
867 data.private_message_view.recipient.id ==
868 mui.local_user_view.person.id
870 this.state.messages.unshift(data.private_message_view);
871 this.state.combined.unshift(
872 this.messageToReplyType(data.private_message_view)
874 this.setState(this.state);
879 } else if (op == UserOperation.SaveComment) {
880 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
881 saveCommentRes(data.comment_view, this.state.replies);
882 this.setState(this.state);
884 } else if (op == UserOperation.CreateCommentLike) {
885 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
886 createCommentLikeRes(data.comment_view, this.state.replies);
887 this.setState(this.state);
888 } else if (op == UserOperation.BlockPerson) {
889 let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
890 updatePersonBlock(data);
891 } else if (op == UserOperation.CreatePostReport) {
892 let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
894 toast(i18n.t("report_created"));
896 } else if (op == UserOperation.CreateCommentReport) {
897 let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
899 toast(i18n.t("report_created"));
904 isMention(view: any): view is PersonMentionView {
905 return (view as PersonMentionView).person_mention !== undefined;