1 import { None, Some } from "@sniptt/monads";
2 import { Component, linkEvent } from "inferno";
12 GetPersonMentionsResponse,
17 PersonMentionResponse,
20 PrivateMessageResponse,
21 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(
96 GetPersonMentionsResponse,
97 PrivateMessagesResponse
99 private subscription: Subscription;
100 private emptyState: InboxState = {
101 unreadOrAll: UnreadOrAll.Unread,
102 messageType: MessageType.All,
107 sort: CommentSortType.New,
109 siteRes: this.isoData.site_res,
113 constructor(props: any, context: any) {
114 super(props, context);
116 this.state = this.emptyState;
117 this.handleSortChange = this.handleSortChange.bind(this);
118 this.handlePageChange = this.handlePageChange.bind(this);
120 if (UserService.Instance.myUserInfo.isNone() && isBrowser()) {
121 toast(i18n.t("not_logged_in"), "danger");
122 this.context.router.history.push(`/login`);
125 this.parseMessage = this.parseMessage.bind(this);
126 this.subscription = wsSubscribe(this.parseMessage);
128 // Only fetch the data if coming from another route
129 if (this.isoData.path == this.context.router.route.match.url) {
133 (this.isoData.routeData[0] as GetRepliesResponse).replies || [],
135 (this.isoData.routeData[1] as GetPersonMentionsResponse).mentions ||
138 (this.isoData.routeData[2] as PrivateMessagesResponse)
139 .private_messages || [],
142 this.state = { ...this.state, combined: this.buildCombined() };
148 componentWillUnmount() {
150 this.subscription.unsubscribe();
154 get documentTitle(): string {
155 return this.state.siteRes.site_view.match({
157 UserService.Instance.myUserInfo.match({
159 `@${mui.local_user_view.person.name} ${i18n.t("inbox")} - ${
169 let inboxRss = auth()
171 .map(a => `/feeds/inbox/${a}.xml`);
173 <div className="container">
174 {this.state.loading ? (
179 <div className="row">
180 <div className="col-12">
182 title={this.documentTitle}
183 path={this.context.router.route.match.url}
187 <h5 className="mb-2">
192 <a href={rss} title="RSS" rel={relTags}>
193 <Icon icon="rss" classes="ml-2 text-muted small" />
197 type="application/atom+xml"
205 {this.state.replies.length +
206 this.state.mentions.length +
207 this.state.messages.length >
209 this.state.unreadOrAll == UnreadOrAll.Unread && (
211 className="btn btn-secondary mb-2"
212 onClick={linkEvent(this, this.markAllAsRead)}
214 {i18n.t("mark_all_as_read")}
218 {this.state.messageType == MessageType.All && this.all()}
219 {this.state.messageType == MessageType.Replies && this.replies()}
220 {this.state.messageType == MessageType.Mentions &&
222 {this.state.messageType == MessageType.Messages &&
225 page={this.state.page}
226 onChange={this.handlePageChange}
235 unreadOrAllRadios() {
237 <div className="btn-group btn-group-toggle flex-wrap mb-2">
239 className={`btn btn-outline-secondary pointer
240 ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
245 value={UnreadOrAll.Unread}
246 checked={this.state.unreadOrAll == UnreadOrAll.Unread}
247 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
252 className={`btn btn-outline-secondary pointer
253 ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
258 value={UnreadOrAll.All}
259 checked={this.state.unreadOrAll == UnreadOrAll.All}
260 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
268 messageTypeRadios() {
270 <div className="btn-group btn-group-toggle flex-wrap mb-2">
272 className={`btn btn-outline-secondary pointer
273 ${this.state.messageType == MessageType.All && "active"}
278 value={MessageType.All}
279 checked={this.state.messageType == MessageType.All}
280 onChange={linkEvent(this, this.handleMessageTypeChange)}
285 className={`btn btn-outline-secondary pointer
286 ${this.state.messageType == MessageType.Replies && "active"}
291 value={MessageType.Replies}
292 checked={this.state.messageType == MessageType.Replies}
293 onChange={linkEvent(this, this.handleMessageTypeChange)}
298 className={`btn btn-outline-secondary pointer
299 ${this.state.messageType == MessageType.Mentions && "active"}
304 value={MessageType.Mentions}
305 checked={this.state.messageType == MessageType.Mentions}
306 onChange={linkEvent(this, this.handleMessageTypeChange)}
311 className={`btn btn-outline-secondary pointer
312 ${this.state.messageType == MessageType.Messages && "active"}
317 value={MessageType.Messages}
318 checked={this.state.messageType == MessageType.Messages}
319 onChange={linkEvent(this, this.handleMessageTypeChange)}
329 <div className="mb-2">
330 <span className="mr-3">{this.unreadOrAllRadios()}</span>
331 <span className="mr-3">{this.messageTypeRadios()}</span>
333 sort={this.state.sort}
334 onChange={this.handleSortChange}
340 replyToReplyType(r: CommentReplyView): ReplyType {
342 id: r.comment_reply.id,
343 type_: ReplyEnum.Reply,
345 published: r.comment.published,
349 mentionToReplyType(r: PersonMentionView): ReplyType {
351 id: r.person_mention.id,
352 type_: ReplyEnum.Mention,
354 published: r.comment.published,
358 messageToReplyType(r: PrivateMessageView): ReplyType {
360 id: r.private_message.id,
361 type_: ReplyEnum.Message,
363 published: r.private_message.published,
367 buildCombined(): ReplyType[] {
368 let replies: ReplyType[] = this.state.replies.map(r =>
369 this.replyToReplyType(r)
371 let mentions: ReplyType[] = this.state.mentions.map(r =>
372 this.mentionToReplyType(r)
374 let messages: ReplyType[] = this.state.messages.map(r =>
375 this.messageToReplyType(r)
378 return [...replies, ...mentions, ...messages].sort((a, b) =>
379 b.published.localeCompare(a.published)
383 renderReplyType(i: ReplyType) {
385 case ReplyEnum.Reply:
390 { comment_view: i.view as CommentView, children: [], depth: 0 },
392 viewType={CommentViewType.Flat}
395 maxCommentsShown={None}
400 enableDownvotes={enableDownvotes(this.state.siteRes)}
403 case ReplyEnum.Mention:
409 comment_view: i.view as PersonMentionView,
414 viewType={CommentViewType.Flat}
417 maxCommentsShown={None}
422 enableDownvotes={enableDownvotes(this.state.siteRes)}
425 case ReplyEnum.Message:
429 private_message_view={i.view as PrivateMessageView}
438 return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
445 nodes={commentsToFlatNodes(this.state.replies)}
446 viewType={CommentViewType.Flat}
449 maxCommentsShown={None}
454 enableDownvotes={enableDownvotes(this.state.siteRes)}
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)}
485 {this.state.messages.map(pmv => (
487 key={pmv.private_message.id}
488 private_message_view={pmv}
495 handlePageChange(page: number) {
496 this.setState({ page });
500 handleUnreadOrAllChange(i: Inbox, event: any) {
501 i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
505 handleMessageTypeChange(i: Inbox, event: any) {
506 i.setState({ messageType: Number(event.target.value), page: 1 });
510 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
511 let promises: Promise<any>[] = [];
513 let sort = Some(CommentSortType.New);
515 // It can be /u/me, or /username/1
516 let repliesForm = new GetReplies({
518 unread_only: Some(true),
520 limit: Some(fetchLimit),
521 auth: req.auth.unwrap(),
523 promises.push(req.client.getReplies(repliesForm));
525 let personMentionsForm = new GetPersonMentions({
527 unread_only: Some(true),
529 limit: Some(fetchLimit),
530 auth: req.auth.unwrap(),
532 promises.push(req.client.getPersonMentions(personMentionsForm));
534 let privateMessagesForm = new GetPrivateMessages({
535 unread_only: Some(true),
537 limit: Some(fetchLimit),
538 auth: req.auth.unwrap(),
540 promises.push(req.client.getPrivateMessages(privateMessagesForm));
546 let sort = Some(this.state.sort);
547 let unread_only = Some(this.state.unreadOrAll == UnreadOrAll.Unread);
548 let page = Some(this.state.page);
549 let limit = Some(fetchLimit);
551 let repliesForm = new GetReplies({
556 auth: auth().unwrap(),
558 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
560 let personMentionsForm = new GetPersonMentions({
565 auth: auth().unwrap(),
567 WebSocketService.Instance.send(
568 wsClient.getPersonMentions(personMentionsForm)
571 let privateMessagesForm = new GetPrivateMessages({
575 auth: auth().unwrap(),
577 WebSocketService.Instance.send(
578 wsClient.getPrivateMessages(privateMessagesForm)
582 handleSortChange(val: CommentSortType) {
583 this.setState({ sort: val, page: 1 });
587 markAllAsRead(i: Inbox) {
588 WebSocketService.Instance.send(
589 wsClient.markAllAsRead({
590 auth: auth().unwrap(),
593 i.setState({ replies: [], mentions: [], messages: [] });
594 i.setState({ combined: i.buildCombined() });
595 UserService.Instance.unreadInboxCountSub.next(0);
596 window.scrollTo(0, 0);
600 sendUnreadCount(read: boolean) {
601 let urcs = UserService.Instance.unreadInboxCountSub;
603 urcs.next(urcs.getValue() - 1);
605 urcs.next(urcs.getValue() + 1);
609 parseMessage(msg: any) {
610 let op = wsUserOp(msg);
613 toast(i18n.t(msg.error), "danger");
615 } else if (msg.reconnect) {
617 } else if (op == UserOperation.GetReplies) {
618 let data = wsJsonToRes<GetRepliesResponse>(msg, GetRepliesResponse);
619 this.setState({ replies: data.replies });
620 this.setState({ combined: this.buildCombined(), loading: false });
621 window.scrollTo(0, 0);
623 } else if (op == UserOperation.GetPersonMentions) {
624 let data = wsJsonToRes<GetPersonMentionsResponse>(
626 GetPersonMentionsResponse
628 this.setState({ mentions: data.mentions });
629 this.setState({ combined: this.buildCombined() });
630 window.scrollTo(0, 0);
632 } else if (op == UserOperation.GetPrivateMessages) {
633 let data = wsJsonToRes<PrivateMessagesResponse>(
635 PrivateMessagesResponse
637 this.setState({ messages: data.private_messages });
638 this.setState({ combined: this.buildCombined() });
639 window.scrollTo(0, 0);
641 } else if (op == UserOperation.EditPrivateMessage) {
642 let data = wsJsonToRes<PrivateMessageResponse>(
644 PrivateMessageResponse
646 let found: PrivateMessageView = this.state.messages.find(
648 m.private_message.id === data.private_message_view.private_message.id
651 let combinedView = this.state.combined.find(
652 i => i.id == data.private_message_view.private_message.id
653 ).view as PrivateMessageView;
654 found.private_message.content = combinedView.private_message.content =
655 data.private_message_view.private_message.content;
656 found.private_message.updated = combinedView.private_message.updated =
657 data.private_message_view.private_message.updated;
659 this.setState(this.state);
660 } else if (op == UserOperation.DeletePrivateMessage) {
661 let data = wsJsonToRes<PrivateMessageResponse>(
663 PrivateMessageResponse
665 let found: PrivateMessageView = this.state.messages.find(
667 m.private_message.id === data.private_message_view.private_message.id
670 let combinedView = this.state.combined.find(
671 i => i.id == data.private_message_view.private_message.id
672 ).view as PrivateMessageView;
673 found.private_message.deleted = combinedView.private_message.deleted =
674 data.private_message_view.private_message.deleted;
675 found.private_message.updated = combinedView.private_message.updated =
676 data.private_message_view.private_message.updated;
678 this.setState(this.state);
679 } else if (op == UserOperation.MarkPrivateMessageAsRead) {
680 let data = wsJsonToRes<PrivateMessageResponse>(
682 PrivateMessageResponse
684 let found: PrivateMessageView = this.state.messages.find(
686 m.private_message.id === data.private_message_view.private_message.id
690 let combinedView = this.state.combined.find(
692 i.id == data.private_message_view.private_message.id &&
693 i.type_ == ReplyEnum.Message
694 ).view as PrivateMessageView;
695 found.private_message.updated = combinedView.private_message.updated =
696 data.private_message_view.private_message.updated;
698 // If youre in the unread view, just remove it from the list
700 this.state.unreadOrAll == UnreadOrAll.Unread &&
701 data.private_message_view.private_message.read
704 messages: this.state.messages.filter(
706 r.private_message.id !==
707 data.private_message_view.private_message.id
711 combined: this.state.combined.filter(
712 r => r.id !== data.private_message_view.private_message.id
716 found.private_message.read = combinedView.private_message.read =
717 data.private_message_view.private_message.read;
720 this.sendUnreadCount(data.private_message_view.private_message.read);
721 this.setState(this.state);
722 } else if (op == UserOperation.MarkAllAsRead) {
723 // Moved to be instant
725 op == UserOperation.EditComment ||
726 op == UserOperation.DeleteComment ||
727 op == UserOperation.RemoveComment
729 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
730 editCommentRes(data.comment_view, this.state.replies);
731 this.setState(this.state);
732 } else if (op == UserOperation.MarkCommentReplyAsRead) {
733 let data = wsJsonToRes<CommentReplyResponse>(msg, CommentReplyResponse);
735 let found = this.state.replies.find(
736 c => c.comment_reply.id == data.comment_reply_view.comment_reply.id
740 let combinedView = this.state.combined.find(
742 i.id == data.comment_reply_view.comment_reply.id &&
743 i.type_ == ReplyEnum.Reply
744 ).view as CommentReplyView;
745 found.comment.content = combinedView.comment.content =
746 data.comment_reply_view.comment.content;
747 found.comment.updated = combinedView.comment.updated =
748 data.comment_reply_view.comment.updated;
749 found.comment.removed = combinedView.comment.removed =
750 data.comment_reply_view.comment.removed;
751 found.comment.deleted = combinedView.comment.deleted =
752 data.comment_reply_view.comment.deleted;
753 found.counts.upvotes = combinedView.counts.upvotes =
754 data.comment_reply_view.counts.upvotes;
755 found.counts.downvotes = combinedView.counts.downvotes =
756 data.comment_reply_view.counts.downvotes;
757 found.counts.score = combinedView.counts.score =
758 data.comment_reply_view.counts.score;
760 // If youre in the unread view, just remove it from the list
762 this.state.unreadOrAll == UnreadOrAll.Unread &&
763 data.comment_reply_view.comment_reply.read
766 replies: this.state.replies.filter(
768 r.comment_reply.id !== data.comment_reply_view.comment_reply.id
772 combined: this.state.combined.filter(
773 r => r.id !== data.comment_reply_view.comment_reply.id
777 found.comment_reply.read = combinedView.comment_reply.read =
778 data.comment_reply_view.comment_reply.read;
781 this.sendUnreadCount(data.comment_reply_view.comment_reply.read);
782 this.setState(this.state);
783 } else if (op == UserOperation.MarkPersonMentionAsRead) {
784 let data = wsJsonToRes<PersonMentionResponse>(msg, PersonMentionResponse);
786 // TODO this might not be correct, it might need to use the comment id
787 let found = this.state.mentions.find(
788 c => c.person_mention.id == data.person_mention_view.person_mention.id
792 let combinedView = this.state.combined.find(
794 i.id == data.person_mention_view.person_mention.id &&
795 i.type_ == ReplyEnum.Mention
796 ).view as PersonMentionView;
797 found.comment.content = combinedView.comment.content =
798 data.person_mention_view.comment.content;
799 found.comment.updated = combinedView.comment.updated =
800 data.person_mention_view.comment.updated;
801 found.comment.removed = combinedView.comment.removed =
802 data.person_mention_view.comment.removed;
803 found.comment.deleted = combinedView.comment.deleted =
804 data.person_mention_view.comment.deleted;
805 found.counts.upvotes = combinedView.counts.upvotes =
806 data.person_mention_view.counts.upvotes;
807 found.counts.downvotes = combinedView.counts.downvotes =
808 data.person_mention_view.counts.downvotes;
809 found.counts.score = combinedView.counts.score =
810 data.person_mention_view.counts.score;
812 // If youre in the unread view, just remove it from the list
814 this.state.unreadOrAll == UnreadOrAll.Unread &&
815 data.person_mention_view.person_mention.read
818 mentions: this.state.mentions.filter(
820 r.person_mention.id !==
821 data.person_mention_view.person_mention.id
825 combined: this.state.combined.filter(
826 r => r.id !== data.person_mention_view.person_mention.id
830 // TODO test to make sure these mentions are getting marked as read
831 found.person_mention.read = combinedView.person_mention.read =
832 data.person_mention_view.person_mention.read;
835 this.sendUnreadCount(data.person_mention_view.person_mention.read);
836 this.setState(this.state);
837 } else if (op == UserOperation.CreatePrivateMessage) {
838 let data = wsJsonToRes<PrivateMessageResponse>(
840 PrivateMessageResponse
842 UserService.Instance.myUserInfo.match({
845 data.private_message_view.recipient.id ==
846 mui.local_user_view.person.id
848 this.state.messages.unshift(data.private_message_view);
849 this.state.combined.unshift(
850 this.messageToReplyType(data.private_message_view)
852 this.setState(this.state);
857 } else if (op == UserOperation.SaveComment) {
858 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
859 saveCommentRes(data.comment_view, this.state.replies);
860 this.setState(this.state);
862 } else if (op == UserOperation.CreateCommentLike) {
863 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
864 createCommentLikeRes(data.comment_view, this.state.replies);
865 this.setState(this.state);
866 } else if (op == UserOperation.BlockPerson) {
867 let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
868 updatePersonBlock(data);
869 } else if (op == UserOperation.CreatePostReport) {
870 let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
872 toast(i18n.t("report_created"));
874 } else if (op == UserOperation.CreateCommentReport) {
875 let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
877 toast(i18n.t("report_created"));
882 isMention(view: any): view is PersonMentionView {
883 return (view as PersonMentionView).person_mention !== undefined;
886 isReply(view: any): view is CommentReplyView {
887 return (view as CommentReplyView).comment_reply !== undefined;