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 this.state.siteRes.site_view.match({
158 UserService.Instance.myUserInfo.match({
160 `@${mui.local_user_view.person.name} ${i18n.t("inbox")} - ${
170 let inboxRss = auth()
172 .map(a => `/feeds/inbox/${a}.xml`);
174 <div className="container-lg">
175 {this.state.loading ? (
180 <div className="row">
181 <div className="col-12">
183 title={this.documentTitle}
184 path={this.context.router.route.match.url}
188 <h5 className="mb-2">
193 <a href={rss} title="RSS" rel={relTags}>
194 <Icon icon="rss" classes="ml-2 text-muted small" />
198 type="application/atom+xml"
206 {this.state.replies.length +
207 this.state.mentions.length +
208 this.state.messages.length >
210 this.state.unreadOrAll == UnreadOrAll.Unread && (
212 className="btn btn-secondary mb-2"
213 onClick={linkEvent(this, this.markAllAsRead)}
215 {i18n.t("mark_all_as_read")}
219 {this.state.messageType == MessageType.All && this.all()}
220 {this.state.messageType == MessageType.Replies && this.replies()}
221 {this.state.messageType == MessageType.Mentions &&
223 {this.state.messageType == MessageType.Messages &&
226 page={this.state.page}
227 onChange={this.handlePageChange}
236 unreadOrAllRadios() {
238 <div className="btn-group btn-group-toggle flex-wrap mb-2">
240 className={`btn btn-outline-secondary pointer
241 ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
246 value={UnreadOrAll.Unread}
247 checked={this.state.unreadOrAll == UnreadOrAll.Unread}
248 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
253 className={`btn btn-outline-secondary pointer
254 ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
259 value={UnreadOrAll.All}
260 checked={this.state.unreadOrAll == UnreadOrAll.All}
261 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
269 messageTypeRadios() {
271 <div className="btn-group btn-group-toggle flex-wrap mb-2">
273 className={`btn btn-outline-secondary pointer
274 ${this.state.messageType == MessageType.All && "active"}
279 value={MessageType.All}
280 checked={this.state.messageType == MessageType.All}
281 onChange={linkEvent(this, this.handleMessageTypeChange)}
286 className={`btn btn-outline-secondary pointer
287 ${this.state.messageType == MessageType.Replies && "active"}
292 value={MessageType.Replies}
293 checked={this.state.messageType == MessageType.Replies}
294 onChange={linkEvent(this, this.handleMessageTypeChange)}
299 className={`btn btn-outline-secondary pointer
300 ${this.state.messageType == MessageType.Mentions && "active"}
305 value={MessageType.Mentions}
306 checked={this.state.messageType == MessageType.Mentions}
307 onChange={linkEvent(this, this.handleMessageTypeChange)}
312 className={`btn btn-outline-secondary pointer
313 ${this.state.messageType == MessageType.Messages && "active"}
318 value={MessageType.Messages}
319 checked={this.state.messageType == MessageType.Messages}
320 onChange={linkEvent(this, this.handleMessageTypeChange)}
330 <div className="mb-2">
331 <span className="mr-3">{this.unreadOrAllRadios()}</span>
332 <span className="mr-3">{this.messageTypeRadios()}</span>
334 sort={this.state.sort}
335 onChange={this.handleSortChange}
341 replyToReplyType(r: CommentReplyView): ReplyType {
343 id: r.comment_reply.id,
344 type_: ReplyEnum.Reply,
346 published: r.comment.published,
350 mentionToReplyType(r: PersonMentionView): ReplyType {
352 id: r.person_mention.id,
353 type_: ReplyEnum.Mention,
355 published: r.comment.published,
359 messageToReplyType(r: PrivateMessageView): ReplyType {
361 id: r.private_message.id,
362 type_: ReplyEnum.Message,
364 published: r.private_message.published,
368 buildCombined(): ReplyType[] {
369 let replies: ReplyType[] = this.state.replies.map(r =>
370 this.replyToReplyType(r)
372 let mentions: ReplyType[] = this.state.mentions.map(r =>
373 this.mentionToReplyType(r)
375 let messages: ReplyType[] = this.state.messages.map(r =>
376 this.messageToReplyType(r)
379 return [...replies, ...mentions, ...messages].sort((a, b) =>
380 b.published.localeCompare(a.published)
384 renderReplyType(i: ReplyType) {
386 case ReplyEnum.Reply:
391 { comment_view: i.view as CommentView, children: [], depth: 0 },
393 viewType={CommentViewType.Flat}
396 maxCommentsShown={None}
401 enableDownvotes={enableDownvotes(this.state.siteRes)}
402 allLanguages={this.state.siteRes.all_languages}
405 case ReplyEnum.Mention:
411 comment_view: i.view as PersonMentionView,
416 viewType={CommentViewType.Flat}
419 maxCommentsShown={None}
424 enableDownvotes={enableDownvotes(this.state.siteRes)}
425 allLanguages={this.state.siteRes.all_languages}
428 case ReplyEnum.Message:
432 private_message_view={i.view as PrivateMessageView}
441 return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
448 nodes={commentsToFlatNodes(this.state.replies)}
449 viewType={CommentViewType.Flat}
452 maxCommentsShown={None}
457 enableDownvotes={enableDownvotes(this.state.siteRes)}
458 allLanguages={this.state.siteRes.all_languages}
467 {this.state.mentions.map(umv => (
469 key={umv.person_mention.id}
470 nodes={[{ comment_view: umv, children: [], depth: 0 }]}
471 viewType={CommentViewType.Flat}
474 maxCommentsShown={None}
479 enableDownvotes={enableDownvotes(this.state.siteRes)}
480 allLanguages={this.state.siteRes.all_languages}
490 {this.state.messages.map(pmv => (
492 key={pmv.private_message.id}
493 private_message_view={pmv}
500 handlePageChange(page: number) {
501 this.setState({ page });
505 handleUnreadOrAllChange(i: Inbox, event: any) {
506 i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
510 handleMessageTypeChange(i: Inbox, event: any) {
511 i.setState({ messageType: Number(event.target.value), page: 1 });
515 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
516 let promises: Promise<any>[] = [];
518 let sort = Some(CommentSortType.New);
520 // It can be /u/me, or /username/1
521 let repliesForm = new GetReplies({
523 unread_only: Some(true),
525 limit: Some(fetchLimit),
526 auth: req.auth.unwrap(),
528 promises.push(req.client.getReplies(repliesForm));
530 let personMentionsForm = new GetPersonMentions({
532 unread_only: Some(true),
534 limit: Some(fetchLimit),
535 auth: req.auth.unwrap(),
537 promises.push(req.client.getPersonMentions(personMentionsForm));
539 let privateMessagesForm = new GetPrivateMessages({
540 unread_only: Some(true),
542 limit: Some(fetchLimit),
543 auth: req.auth.unwrap(),
545 promises.push(req.client.getPrivateMessages(privateMessagesForm));
551 let sort = Some(this.state.sort);
552 let unread_only = Some(this.state.unreadOrAll == UnreadOrAll.Unread);
553 let page = Some(this.state.page);
554 let limit = Some(fetchLimit);
556 let repliesForm = new GetReplies({
561 auth: auth().unwrap(),
563 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
565 let personMentionsForm = new GetPersonMentions({
570 auth: auth().unwrap(),
572 WebSocketService.Instance.send(
573 wsClient.getPersonMentions(personMentionsForm)
576 let privateMessagesForm = new GetPrivateMessages({
580 auth: auth().unwrap(),
582 WebSocketService.Instance.send(
583 wsClient.getPrivateMessages(privateMessagesForm)
587 handleSortChange(val: CommentSortType) {
588 this.setState({ sort: val, page: 1 });
592 markAllAsRead(i: Inbox) {
593 WebSocketService.Instance.send(
594 wsClient.markAllAsRead({
595 auth: auth().unwrap(),
598 i.setState({ replies: [], mentions: [], messages: [] });
599 i.setState({ combined: i.buildCombined() });
600 UserService.Instance.unreadInboxCountSub.next(0);
601 window.scrollTo(0, 0);
605 sendUnreadCount(read: boolean) {
606 let urcs = UserService.Instance.unreadInboxCountSub;
608 urcs.next(urcs.getValue() - 1);
610 urcs.next(urcs.getValue() + 1);
614 parseMessage(msg: any) {
615 let op = wsUserOp(msg);
618 toast(i18n.t(msg.error), "danger");
620 } else if (msg.reconnect) {
622 } else if (op == UserOperation.GetReplies) {
623 let data = wsJsonToRes<GetRepliesResponse>(msg, GetRepliesResponse);
624 this.setState({ replies: data.replies });
625 this.setState({ combined: this.buildCombined(), loading: false });
626 window.scrollTo(0, 0);
628 } else if (op == UserOperation.GetPersonMentions) {
629 let data = wsJsonToRes<GetPersonMentionsResponse>(
631 GetPersonMentionsResponse
633 this.setState({ mentions: data.mentions });
634 this.setState({ combined: this.buildCombined() });
635 window.scrollTo(0, 0);
637 } else if (op == UserOperation.GetPrivateMessages) {
638 let data = wsJsonToRes<PrivateMessagesResponse>(
640 PrivateMessagesResponse
642 this.setState({ messages: data.private_messages });
643 this.setState({ combined: this.buildCombined() });
644 window.scrollTo(0, 0);
646 } else if (op == UserOperation.EditPrivateMessage) {
647 let data = wsJsonToRes<PrivateMessageResponse>(
649 PrivateMessageResponse
651 let found: PrivateMessageView = this.state.messages.find(
653 m.private_message.id === data.private_message_view.private_message.id
656 let combinedView = this.state.combined.find(
657 i => i.id == data.private_message_view.private_message.id
658 ).view as PrivateMessageView;
659 found.private_message.content = combinedView.private_message.content =
660 data.private_message_view.private_message.content;
661 found.private_message.updated = combinedView.private_message.updated =
662 data.private_message_view.private_message.updated;
664 this.setState(this.state);
665 } else if (op == UserOperation.DeletePrivateMessage) {
666 let data = wsJsonToRes<PrivateMessageResponse>(
668 PrivateMessageResponse
670 let found: PrivateMessageView = this.state.messages.find(
672 m.private_message.id === data.private_message_view.private_message.id
675 let combinedView = this.state.combined.find(
676 i => i.id == data.private_message_view.private_message.id
677 ).view as PrivateMessageView;
678 found.private_message.deleted = combinedView.private_message.deleted =
679 data.private_message_view.private_message.deleted;
680 found.private_message.updated = combinedView.private_message.updated =
681 data.private_message_view.private_message.updated;
683 this.setState(this.state);
684 } else if (op == UserOperation.MarkPrivateMessageAsRead) {
685 let data = wsJsonToRes<PrivateMessageResponse>(
687 PrivateMessageResponse
689 let found: PrivateMessageView = this.state.messages.find(
691 m.private_message.id === data.private_message_view.private_message.id
695 let combinedView = this.state.combined.find(
697 i.id == data.private_message_view.private_message.id &&
698 i.type_ == ReplyEnum.Message
699 ).view as PrivateMessageView;
700 found.private_message.updated = combinedView.private_message.updated =
701 data.private_message_view.private_message.updated;
703 // If youre in the unread view, just remove it from the list
705 this.state.unreadOrAll == UnreadOrAll.Unread &&
706 data.private_message_view.private_message.read
709 messages: this.state.messages.filter(
711 r.private_message.id !==
712 data.private_message_view.private_message.id
716 combined: this.state.combined.filter(
717 r => r.id !== data.private_message_view.private_message.id
721 found.private_message.read = combinedView.private_message.read =
722 data.private_message_view.private_message.read;
725 this.sendUnreadCount(data.private_message_view.private_message.read);
726 this.setState(this.state);
727 } else if (op == UserOperation.MarkAllAsRead) {
728 // Moved to be instant
730 op == UserOperation.EditComment ||
731 op == UserOperation.DeleteComment ||
732 op == UserOperation.RemoveComment
734 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
735 editCommentRes(data.comment_view, this.state.replies);
736 this.setState(this.state);
737 } else if (op == UserOperation.MarkCommentReplyAsRead) {
738 let data = wsJsonToRes<CommentReplyResponse>(msg, CommentReplyResponse);
740 let found = this.state.replies.find(
741 c => c.comment_reply.id == data.comment_reply_view.comment_reply.id
745 let combinedView = this.state.combined.find(
747 i.id == data.comment_reply_view.comment_reply.id &&
748 i.type_ == ReplyEnum.Reply
749 ).view as CommentReplyView;
750 found.comment.content = combinedView.comment.content =
751 data.comment_reply_view.comment.content;
752 found.comment.updated = combinedView.comment.updated =
753 data.comment_reply_view.comment.updated;
754 found.comment.removed = combinedView.comment.removed =
755 data.comment_reply_view.comment.removed;
756 found.comment.deleted = combinedView.comment.deleted =
757 data.comment_reply_view.comment.deleted;
758 found.counts.upvotes = combinedView.counts.upvotes =
759 data.comment_reply_view.counts.upvotes;
760 found.counts.downvotes = combinedView.counts.downvotes =
761 data.comment_reply_view.counts.downvotes;
762 found.counts.score = combinedView.counts.score =
763 data.comment_reply_view.counts.score;
765 // If youre in the unread view, just remove it from the list
767 this.state.unreadOrAll == UnreadOrAll.Unread &&
768 data.comment_reply_view.comment_reply.read
771 replies: this.state.replies.filter(
773 r.comment_reply.id !== data.comment_reply_view.comment_reply.id
777 combined: this.state.combined.filter(
778 r => r.id !== data.comment_reply_view.comment_reply.id
782 found.comment_reply.read = combinedView.comment_reply.read =
783 data.comment_reply_view.comment_reply.read;
786 this.sendUnreadCount(data.comment_reply_view.comment_reply.read);
787 this.setState(this.state);
788 } else if (op == UserOperation.MarkPersonMentionAsRead) {
789 let data = wsJsonToRes<PersonMentionResponse>(msg, PersonMentionResponse);
791 // TODO this might not be correct, it might need to use the comment id
792 let found = this.state.mentions.find(
793 c => c.person_mention.id == data.person_mention_view.person_mention.id
797 let combinedView = this.state.combined.find(
799 i.id == data.person_mention_view.person_mention.id &&
800 i.type_ == ReplyEnum.Mention
801 ).view as PersonMentionView;
802 found.comment.content = combinedView.comment.content =
803 data.person_mention_view.comment.content;
804 found.comment.updated = combinedView.comment.updated =
805 data.person_mention_view.comment.updated;
806 found.comment.removed = combinedView.comment.removed =
807 data.person_mention_view.comment.removed;
808 found.comment.deleted = combinedView.comment.deleted =
809 data.person_mention_view.comment.deleted;
810 found.counts.upvotes = combinedView.counts.upvotes =
811 data.person_mention_view.counts.upvotes;
812 found.counts.downvotes = combinedView.counts.downvotes =
813 data.person_mention_view.counts.downvotes;
814 found.counts.score = combinedView.counts.score =
815 data.person_mention_view.counts.score;
817 // If youre in the unread view, just remove it from the list
819 this.state.unreadOrAll == UnreadOrAll.Unread &&
820 data.person_mention_view.person_mention.read
823 mentions: this.state.mentions.filter(
825 r.person_mention.id !==
826 data.person_mention_view.person_mention.id
830 combined: this.state.combined.filter(
831 r => r.id !== data.person_mention_view.person_mention.id
835 // TODO test to make sure these mentions are getting marked as read
836 found.person_mention.read = combinedView.person_mention.read =
837 data.person_mention_view.person_mention.read;
840 this.sendUnreadCount(data.person_mention_view.person_mention.read);
841 this.setState(this.state);
842 } else if (op == UserOperation.CreatePrivateMessage) {
843 let data = wsJsonToRes<PrivateMessageResponse>(
845 PrivateMessageResponse
847 UserService.Instance.myUserInfo.match({
850 data.private_message_view.recipient.id ==
851 mui.local_user_view.person.id
853 this.state.messages.unshift(data.private_message_view);
854 this.state.combined.unshift(
855 this.messageToReplyType(data.private_message_view)
857 this.setState(this.state);
862 } else if (op == UserOperation.SaveComment) {
863 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
864 saveCommentRes(data.comment_view, this.state.replies);
865 this.setState(this.state);
867 } else if (op == UserOperation.CreateCommentLike) {
868 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
869 createCommentLikeRes(data.comment_view, this.state.replies);
870 this.setState(this.state);
871 } else if (op == UserOperation.BlockPerson) {
872 let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
873 updatePersonBlock(data);
874 } else if (op == UserOperation.CreatePostReport) {
875 let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
877 toast(i18n.t("report_created"));
879 } else if (op == UserOperation.CreateCommentReport) {
880 let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
882 toast(i18n.t("report_created"));
884 } else if (op == UserOperation.CreatePrivateMessageReport) {
885 let data = wsJsonToRes<PrivateMessageReportResponse>(
887 PrivateMessageReportResponse
890 toast(i18n.t("report_created"));
895 isMention(view: any): view is PersonMentionView {
896 return (view as PersonMentionView).person_mention !== undefined;
899 isReply(view: any): view is CommentReplyView {
900 return (view as CommentReplyView).comment_reply !== undefined;