1 import { None, Some } from "@sniptt/monads";
2 import { Component, linkEvent } from "inferno";
12 GetPersonMentionsResponse,
17 PersonMentionResponse,
20 PrivateMessageReportResponse,
21 PrivateMessageResponse,
22 PrivateMessagesResponse,
27 } from "lemmy-js-client";
28 import { Subscription } from "rxjs";
29 import { i18n } from "../../i18next";
30 import { CommentViewType, InitialFetchRequest } from "../../interfaces";
31 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";
76 view: CommentView | PrivateMessageView | PersonMentionView | CommentReplyView;
80 interface InboxState {
81 unreadOrAll: UnreadOrAll;
82 messageType: MessageType;
83 replies: CommentReplyView[];
84 mentions: PersonMentionView[];
85 messages: PrivateMessageView[];
86 combined: ReplyType[];
87 sort: CommentSortType;
89 siteRes: GetSiteResponse;
93 export class Inbox extends Component<any, InboxState> {
94 private isoData = setIsoData(
97 GetPersonMentionsResponse,
98 PrivateMessagesResponse
100 private subscription: Subscription;
101 private emptyState: InboxState = {
102 unreadOrAll: UnreadOrAll.Unread,
103 messageType: MessageType.All,
108 sort: CommentSortType.New,
110 siteRes: this.isoData.site_res,
114 constructor(props: any, context: any) {
115 super(props, context);
117 this.state = this.emptyState;
118 this.handleSortChange = this.handleSortChange.bind(this);
119 this.handlePageChange = this.handlePageChange.bind(this);
121 if (UserService.Instance.myUserInfo.isNone() && isBrowser()) {
122 toast(i18n.t("not_logged_in"), "danger");
123 this.context.router.history.push(`/login`);
126 this.parseMessage = this.parseMessage.bind(this);
127 this.subscription = wsSubscribe(this.parseMessage);
129 // Only fetch the data if coming from another route
130 if (this.isoData.path == this.context.router.route.match.url) {
134 (this.isoData.routeData[0] as GetRepliesResponse).replies || [],
136 (this.isoData.routeData[1] as GetPersonMentionsResponse).mentions ||
139 (this.isoData.routeData[2] as PrivateMessagesResponse)
140 .private_messages || [],
143 this.state = { ...this.state, combined: this.buildCombined() };
149 componentWillUnmount() {
151 this.subscription.unsubscribe();
155 get documentTitle(): string {
156 return UserService.Instance.myUserInfo.match({
158 `@${mui.local_user_view.person.name} ${i18n.t("inbox")} - ${
159 this.state.siteRes.site_view.site.name
166 let inboxRss = auth()
168 .map(a => `/feeds/inbox/${a}.xml`);
170 <div className="container-lg">
171 {this.state.loading ? (
176 <div className="row">
177 <div className="col-12">
179 title={this.documentTitle}
180 path={this.context.router.route.match.url}
184 <h5 className="mb-2">
189 <a href={rss} title="RSS" rel={relTags}>
190 <Icon icon="rss" classes="ml-2 text-muted small" />
194 type="application/atom+xml"
202 {this.state.replies.length +
203 this.state.mentions.length +
204 this.state.messages.length >
206 this.state.unreadOrAll == UnreadOrAll.Unread && (
208 className="btn btn-secondary mb-2"
209 onClick={linkEvent(this, this.markAllAsRead)}
211 {i18n.t("mark_all_as_read")}
215 {this.state.messageType == MessageType.All && this.all()}
216 {this.state.messageType == MessageType.Replies && this.replies()}
217 {this.state.messageType == MessageType.Mentions &&
219 {this.state.messageType == MessageType.Messages &&
222 page={this.state.page}
223 onChange={this.handlePageChange}
232 unreadOrAllRadios() {
234 <div className="btn-group btn-group-toggle flex-wrap mb-2">
236 className={`btn btn-outline-secondary pointer
237 ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
242 value={UnreadOrAll.Unread}
243 checked={this.state.unreadOrAll == UnreadOrAll.Unread}
244 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
249 className={`btn btn-outline-secondary pointer
250 ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
255 value={UnreadOrAll.All}
256 checked={this.state.unreadOrAll == UnreadOrAll.All}
257 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
265 messageTypeRadios() {
267 <div className="btn-group btn-group-toggle flex-wrap mb-2">
269 className={`btn btn-outline-secondary pointer
270 ${this.state.messageType == MessageType.All && "active"}
275 value={MessageType.All}
276 checked={this.state.messageType == MessageType.All}
277 onChange={linkEvent(this, this.handleMessageTypeChange)}
282 className={`btn btn-outline-secondary pointer
283 ${this.state.messageType == MessageType.Replies && "active"}
288 value={MessageType.Replies}
289 checked={this.state.messageType == MessageType.Replies}
290 onChange={linkEvent(this, this.handleMessageTypeChange)}
295 className={`btn btn-outline-secondary pointer
296 ${this.state.messageType == MessageType.Mentions && "active"}
301 value={MessageType.Mentions}
302 checked={this.state.messageType == MessageType.Mentions}
303 onChange={linkEvent(this, this.handleMessageTypeChange)}
308 className={`btn btn-outline-secondary pointer
309 ${this.state.messageType == MessageType.Messages && "active"}
314 value={MessageType.Messages}
315 checked={this.state.messageType == MessageType.Messages}
316 onChange={linkEvent(this, this.handleMessageTypeChange)}
326 <div className="mb-2">
327 <span className="mr-3">{this.unreadOrAllRadios()}</span>
328 <span className="mr-3">{this.messageTypeRadios()}</span>
330 sort={this.state.sort}
331 onChange={this.handleSortChange}
337 replyToReplyType(r: CommentReplyView): ReplyType {
339 id: r.comment_reply.id,
340 type_: ReplyEnum.Reply,
342 published: r.comment.published,
346 mentionToReplyType(r: PersonMentionView): ReplyType {
348 id: r.person_mention.id,
349 type_: ReplyEnum.Mention,
351 published: r.comment.published,
355 messageToReplyType(r: PrivateMessageView): ReplyType {
357 id: r.private_message.id,
358 type_: ReplyEnum.Message,
360 published: r.private_message.published,
364 buildCombined(): ReplyType[] {
365 let replies: ReplyType[] = this.state.replies.map(r =>
366 this.replyToReplyType(r)
368 let mentions: ReplyType[] = this.state.mentions.map(r =>
369 this.mentionToReplyType(r)
371 let messages: ReplyType[] = this.state.messages.map(r =>
372 this.messageToReplyType(r)
375 return [...replies, ...mentions, ...messages].sort((a, b) =>
376 b.published.localeCompare(a.published)
380 renderReplyType(i: ReplyType) {
382 case ReplyEnum.Reply:
387 { comment_view: i.view as CommentView, children: [], depth: 0 },
389 viewType={CommentViewType.Flat}
392 maxCommentsShown={None}
397 enableDownvotes={enableDownvotes(this.state.siteRes)}
398 allLanguages={this.state.siteRes.all_languages}
401 case ReplyEnum.Mention:
407 comment_view: i.view as PersonMentionView,
412 viewType={CommentViewType.Flat}
415 maxCommentsShown={None}
420 enableDownvotes={enableDownvotes(this.state.siteRes)}
421 allLanguages={this.state.siteRes.all_languages}
424 case ReplyEnum.Message:
428 private_message_view={i.view as PrivateMessageView}
437 return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
444 nodes={commentsToFlatNodes(this.state.replies)}
445 viewType={CommentViewType.Flat}
448 maxCommentsShown={None}
453 enableDownvotes={enableDownvotes(this.state.siteRes)}
454 allLanguages={this.state.siteRes.all_languages}
463 {this.state.mentions.map(umv => (
465 key={umv.person_mention.id}
466 nodes={[{ comment_view: umv, children: [], depth: 0 }]}
467 viewType={CommentViewType.Flat}
470 maxCommentsShown={None}
475 enableDownvotes={enableDownvotes(this.state.siteRes)}
476 allLanguages={this.state.siteRes.all_languages}
486 {this.state.messages.map(pmv => (
488 key={pmv.private_message.id}
489 private_message_view={pmv}
496 handlePageChange(page: number) {
497 this.setState({ page });
501 handleUnreadOrAllChange(i: Inbox, event: any) {
502 i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
506 handleMessageTypeChange(i: Inbox, event: any) {
507 i.setState({ messageType: Number(event.target.value), page: 1 });
511 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
512 let promises: Promise<any>[] = [];
514 let sort = Some(CommentSortType.New);
516 // It can be /u/me, or /username/1
517 let repliesForm = new GetReplies({
519 unread_only: Some(true),
521 limit: Some(fetchLimit),
522 auth: req.auth.unwrap(),
524 promises.push(req.client.getReplies(repliesForm));
526 let personMentionsForm = new GetPersonMentions({
528 unread_only: Some(true),
530 limit: Some(fetchLimit),
531 auth: req.auth.unwrap(),
533 promises.push(req.client.getPersonMentions(personMentionsForm));
535 let privateMessagesForm = new GetPrivateMessages({
536 unread_only: Some(true),
538 limit: Some(fetchLimit),
539 auth: req.auth.unwrap(),
541 promises.push(req.client.getPrivateMessages(privateMessagesForm));
547 let sort = Some(this.state.sort);
548 let unread_only = Some(this.state.unreadOrAll == UnreadOrAll.Unread);
549 let page = Some(this.state.page);
550 let limit = Some(fetchLimit);
552 let repliesForm = new GetReplies({
557 auth: auth().unwrap(),
559 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
561 let personMentionsForm = new GetPersonMentions({
566 auth: auth().unwrap(),
568 WebSocketService.Instance.send(
569 wsClient.getPersonMentions(personMentionsForm)
572 let privateMessagesForm = new GetPrivateMessages({
576 auth: auth().unwrap(),
578 WebSocketService.Instance.send(
579 wsClient.getPrivateMessages(privateMessagesForm)
583 handleSortChange(val: CommentSortType) {
584 this.setState({ sort: val, page: 1 });
588 markAllAsRead(i: Inbox) {
589 WebSocketService.Instance.send(
590 wsClient.markAllAsRead({
591 auth: auth().unwrap(),
594 i.setState({ replies: [], mentions: [], messages: [] });
595 i.setState({ combined: i.buildCombined() });
596 UserService.Instance.unreadInboxCountSub.next(0);
597 window.scrollTo(0, 0);
601 sendUnreadCount(read: boolean) {
602 let urcs = UserService.Instance.unreadInboxCountSub;
604 urcs.next(urcs.getValue() - 1);
606 urcs.next(urcs.getValue() + 1);
610 parseMessage(msg: any) {
611 let op = wsUserOp(msg);
614 toast(i18n.t(msg.error), "danger");
616 } else if (msg.reconnect) {
618 } else if (op == UserOperation.GetReplies) {
619 let data = wsJsonToRes<GetRepliesResponse>(msg, GetRepliesResponse);
620 this.setState({ replies: data.replies });
621 this.setState({ combined: this.buildCombined(), loading: false });
622 window.scrollTo(0, 0);
624 } else if (op == UserOperation.GetPersonMentions) {
625 let data = wsJsonToRes<GetPersonMentionsResponse>(
627 GetPersonMentionsResponse
629 this.setState({ mentions: data.mentions });
630 this.setState({ combined: this.buildCombined() });
631 window.scrollTo(0, 0);
633 } else if (op == UserOperation.GetPrivateMessages) {
634 let data = wsJsonToRes<PrivateMessagesResponse>(
636 PrivateMessagesResponse
638 this.setState({ messages: data.private_messages });
639 this.setState({ combined: this.buildCombined() });
640 window.scrollTo(0, 0);
642 } else if (op == UserOperation.EditPrivateMessage) {
643 let data = wsJsonToRes<PrivateMessageResponse>(
645 PrivateMessageResponse
647 let found: PrivateMessageView = this.state.messages.find(
649 m.private_message.id === data.private_message_view.private_message.id
652 let combinedView = this.state.combined.find(
653 i => i.id == data.private_message_view.private_message.id
654 ).view as PrivateMessageView;
655 found.private_message.content = combinedView.private_message.content =
656 data.private_message_view.private_message.content;
657 found.private_message.updated = combinedView.private_message.updated =
658 data.private_message_view.private_message.updated;
660 this.setState(this.state);
661 } else if (op == UserOperation.DeletePrivateMessage) {
662 let data = wsJsonToRes<PrivateMessageResponse>(
664 PrivateMessageResponse
666 let found: PrivateMessageView = this.state.messages.find(
668 m.private_message.id === data.private_message_view.private_message.id
671 let combinedView = this.state.combined.find(
672 i => i.id == data.private_message_view.private_message.id
673 ).view as PrivateMessageView;
674 found.private_message.deleted = combinedView.private_message.deleted =
675 data.private_message_view.private_message.deleted;
676 found.private_message.updated = combinedView.private_message.updated =
677 data.private_message_view.private_message.updated;
679 this.setState(this.state);
680 } else if (op == UserOperation.MarkPrivateMessageAsRead) {
681 let data = wsJsonToRes<PrivateMessageResponse>(
683 PrivateMessageResponse
685 let found: PrivateMessageView = this.state.messages.find(
687 m.private_message.id === data.private_message_view.private_message.id
691 let combinedView = this.state.combined.find(
693 i.id == data.private_message_view.private_message.id &&
694 i.type_ == ReplyEnum.Message
695 ).view as PrivateMessageView;
696 found.private_message.updated = combinedView.private_message.updated =
697 data.private_message_view.private_message.updated;
699 // If youre in the unread view, just remove it from the list
701 this.state.unreadOrAll == UnreadOrAll.Unread &&
702 data.private_message_view.private_message.read
705 messages: this.state.messages.filter(
707 r.private_message.id !==
708 data.private_message_view.private_message.id
712 combined: this.state.combined.filter(
713 r => r.id !== data.private_message_view.private_message.id
717 found.private_message.read = combinedView.private_message.read =
718 data.private_message_view.private_message.read;
721 this.sendUnreadCount(data.private_message_view.private_message.read);
722 this.setState(this.state);
723 } else if (op == UserOperation.MarkAllAsRead) {
724 // Moved to be instant
726 op == UserOperation.EditComment ||
727 op == UserOperation.DeleteComment ||
728 op == UserOperation.RemoveComment
730 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
731 editCommentRes(data.comment_view, this.state.replies);
732 this.setState(this.state);
733 } else if (op == UserOperation.MarkCommentReplyAsRead) {
734 let data = wsJsonToRes<CommentReplyResponse>(msg, CommentReplyResponse);
736 let found = this.state.replies.find(
737 c => c.comment_reply.id == data.comment_reply_view.comment_reply.id
741 let combinedView = this.state.combined.find(
743 i.id == data.comment_reply_view.comment_reply.id &&
744 i.type_ == ReplyEnum.Reply
745 ).view as CommentReplyView;
746 found.comment.content = combinedView.comment.content =
747 data.comment_reply_view.comment.content;
748 found.comment.updated = combinedView.comment.updated =
749 data.comment_reply_view.comment.updated;
750 found.comment.removed = combinedView.comment.removed =
751 data.comment_reply_view.comment.removed;
752 found.comment.deleted = combinedView.comment.deleted =
753 data.comment_reply_view.comment.deleted;
754 found.counts.upvotes = combinedView.counts.upvotes =
755 data.comment_reply_view.counts.upvotes;
756 found.counts.downvotes = combinedView.counts.downvotes =
757 data.comment_reply_view.counts.downvotes;
758 found.counts.score = combinedView.counts.score =
759 data.comment_reply_view.counts.score;
761 // If youre in the unread view, just remove it from the list
763 this.state.unreadOrAll == UnreadOrAll.Unread &&
764 data.comment_reply_view.comment_reply.read
767 replies: this.state.replies.filter(
769 r.comment_reply.id !== data.comment_reply_view.comment_reply.id
773 combined: this.state.combined.filter(
774 r => r.id !== data.comment_reply_view.comment_reply.id
778 found.comment_reply.read = combinedView.comment_reply.read =
779 data.comment_reply_view.comment_reply.read;
782 this.sendUnreadCount(data.comment_reply_view.comment_reply.read);
783 this.setState(this.state);
784 } else if (op == UserOperation.MarkPersonMentionAsRead) {
785 let data = wsJsonToRes<PersonMentionResponse>(msg, PersonMentionResponse);
787 // TODO this might not be correct, it might need to use the comment id
788 let found = this.state.mentions.find(
789 c => c.person_mention.id == data.person_mention_view.person_mention.id
793 let combinedView = this.state.combined.find(
795 i.id == data.person_mention_view.person_mention.id &&
796 i.type_ == ReplyEnum.Mention
797 ).view as PersonMentionView;
798 found.comment.content = combinedView.comment.content =
799 data.person_mention_view.comment.content;
800 found.comment.updated = combinedView.comment.updated =
801 data.person_mention_view.comment.updated;
802 found.comment.removed = combinedView.comment.removed =
803 data.person_mention_view.comment.removed;
804 found.comment.deleted = combinedView.comment.deleted =
805 data.person_mention_view.comment.deleted;
806 found.counts.upvotes = combinedView.counts.upvotes =
807 data.person_mention_view.counts.upvotes;
808 found.counts.downvotes = combinedView.counts.downvotes =
809 data.person_mention_view.counts.downvotes;
810 found.counts.score = combinedView.counts.score =
811 data.person_mention_view.counts.score;
813 // If youre in the unread view, just remove it from the list
815 this.state.unreadOrAll == UnreadOrAll.Unread &&
816 data.person_mention_view.person_mention.read
819 mentions: this.state.mentions.filter(
821 r.person_mention.id !==
822 data.person_mention_view.person_mention.id
826 combined: this.state.combined.filter(
827 r => r.id !== data.person_mention_view.person_mention.id
831 // TODO test to make sure these mentions are getting marked as read
832 found.person_mention.read = combinedView.person_mention.read =
833 data.person_mention_view.person_mention.read;
836 this.sendUnreadCount(data.person_mention_view.person_mention.read);
837 this.setState(this.state);
838 } else if (op == UserOperation.CreatePrivateMessage) {
839 let data = wsJsonToRes<PrivateMessageResponse>(
841 PrivateMessageResponse
843 UserService.Instance.myUserInfo.match({
846 data.private_message_view.recipient.id ==
847 mui.local_user_view.person.id
849 this.state.messages.unshift(data.private_message_view);
850 this.state.combined.unshift(
851 this.messageToReplyType(data.private_message_view)
853 this.setState(this.state);
858 } else if (op == UserOperation.SaveComment) {
859 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
860 saveCommentRes(data.comment_view, this.state.replies);
861 this.setState(this.state);
863 } else if (op == UserOperation.CreateCommentLike) {
864 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
865 createCommentLikeRes(data.comment_view, this.state.replies);
866 this.setState(this.state);
867 } else if (op == UserOperation.BlockPerson) {
868 let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
869 updatePersonBlock(data);
870 } else if (op == UserOperation.CreatePostReport) {
871 let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
873 toast(i18n.t("report_created"));
875 } else if (op == UserOperation.CreateCommentReport) {
876 let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
878 toast(i18n.t("report_created"));
880 } else if (op == UserOperation.CreatePrivateMessageReport) {
881 let data = wsJsonToRes<PrivateMessageReportResponse>(
883 PrivateMessageReportResponse
886 toast(i18n.t("report_created"));
891 isMention(view: any): view is PersonMentionView {
892 return (view as PersonMentionView).person_mention !== undefined;
895 isReply(view: any): view is CommentReplyView {
896 return (view as CommentReplyView).comment_reply !== undefined;