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) {
131 (this.isoData.routeData[0] as GetRepliesResponse).replies || [];
132 this.state.mentions =
133 (this.isoData.routeData[1] as GetPersonMentionsResponse).mentions || [];
134 this.state.messages =
135 (this.isoData.routeData[2] as PrivateMessagesResponse)
136 .private_messages || [];
137 this.state.combined = this.buildCombined();
138 this.state.loading = false;
144 componentWillUnmount() {
146 this.subscription.unsubscribe();
150 get documentTitle(): string {
151 return this.state.siteRes.site_view.match({
153 UserService.Instance.myUserInfo.match({
155 `@${mui.local_user_view.person.name} ${i18n.t("inbox")} - ${
165 let inboxRss = auth()
167 .map(a => `/feeds/inbox/${a}.xml`);
169 <div class="container">
170 {this.state.loading ? (
178 title={this.documentTitle}
179 path={this.context.router.route.match.url}
188 <a href={rss} title="RSS" rel={relTags}>
189 <Icon icon="rss" classes="ml-2 text-muted small" />
193 type="application/atom+xml"
201 {this.state.replies.length +
202 this.state.mentions.length +
203 this.state.messages.length >
205 this.state.unreadOrAll == UnreadOrAll.Unread && (
207 class="btn btn-secondary mb-2"
208 onClick={linkEvent(this, this.markAllAsRead)}
210 {i18n.t("mark_all_as_read")}
214 {this.state.messageType == MessageType.All && this.all()}
215 {this.state.messageType == MessageType.Replies && this.replies()}
216 {this.state.messageType == MessageType.Mentions &&
218 {this.state.messageType == MessageType.Messages &&
221 page={this.state.page}
222 onChange={this.handlePageChange}
231 unreadOrAllRadios() {
233 <div class="btn-group btn-group-toggle flex-wrap mb-2">
235 className={`btn btn-outline-secondary pointer
236 ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
241 value={UnreadOrAll.Unread}
242 checked={this.state.unreadOrAll == UnreadOrAll.Unread}
243 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
248 className={`btn btn-outline-secondary pointer
249 ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
254 value={UnreadOrAll.All}
255 checked={this.state.unreadOrAll == UnreadOrAll.All}
256 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
264 messageTypeRadios() {
266 <div class="btn-group btn-group-toggle flex-wrap mb-2">
268 className={`btn btn-outline-secondary pointer
269 ${this.state.messageType == MessageType.All && "active"}
274 value={MessageType.All}
275 checked={this.state.messageType == MessageType.All}
276 onChange={linkEvent(this, this.handleMessageTypeChange)}
281 className={`btn btn-outline-secondary pointer
282 ${this.state.messageType == MessageType.Replies && "active"}
287 value={MessageType.Replies}
288 checked={this.state.messageType == MessageType.Replies}
289 onChange={linkEvent(this, this.handleMessageTypeChange)}
294 className={`btn btn-outline-secondary pointer
295 ${this.state.messageType == MessageType.Mentions && "active"}
300 value={MessageType.Mentions}
301 checked={this.state.messageType == MessageType.Mentions}
302 onChange={linkEvent(this, this.handleMessageTypeChange)}
307 className={`btn btn-outline-secondary pointer
308 ${this.state.messageType == MessageType.Messages && "active"}
313 value={MessageType.Messages}
314 checked={this.state.messageType == MessageType.Messages}
315 onChange={linkEvent(this, this.handleMessageTypeChange)}
325 <div className="mb-2">
326 <span class="mr-3">{this.unreadOrAllRadios()}</span>
327 <span class="mr-3">{this.messageTypeRadios()}</span>
329 sort={this.state.sort}
330 onChange={this.handleSortChange}
336 replyToReplyType(r: CommentReplyView): ReplyType {
338 id: r.comment_reply.id,
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:
386 { comment_view: i.view as CommentView, children: [], depth: 0 },
388 viewType={CommentViewType.Flat}
391 maxCommentsShown={None}
396 enableDownvotes={enableDownvotes(this.state.siteRes)}
399 case ReplyEnum.Mention:
405 comment_view: i.view as PersonMentionView,
410 viewType={CommentViewType.Flat}
413 maxCommentsShown={None}
418 enableDownvotes={enableDownvotes(this.state.siteRes)}
421 case ReplyEnum.Message:
425 private_message_view={i.view as PrivateMessageView}
434 return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
441 nodes={commentsToFlatNodes(this.state.replies)}
442 viewType={CommentViewType.Flat}
445 maxCommentsShown={None}
450 enableDownvotes={enableDownvotes(this.state.siteRes)}
459 {this.state.mentions.map(umv => (
461 key={umv.person_mention.id}
462 nodes={[{ comment_view: umv, children: [], depth: 0 }]}
463 viewType={CommentViewType.Flat}
466 maxCommentsShown={None}
471 enableDownvotes={enableDownvotes(this.state.siteRes)}
481 {this.state.messages.map(pmv => (
483 key={pmv.private_message.id}
484 private_message_view={pmv}
491 handlePageChange(page: number) {
492 this.setState({ page });
496 handleUnreadOrAllChange(i: Inbox, event: any) {
497 i.state.unreadOrAll = Number(event.target.value);
503 handleMessageTypeChange(i: Inbox, event: any) {
504 i.state.messageType = Number(event.target.value);
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.state.sort = val;
585 this.setState(this.state);
589 markAllAsRead(i: Inbox) {
590 WebSocketService.Instance.send(
591 wsClient.markAllAsRead({
592 auth: auth().unwrap(),
595 i.state.replies = [];
596 i.state.mentions = [];
597 i.state.messages = [];
598 i.state.combined = i.buildCombined();
599 UserService.Instance.unreadInboxCountSub.next(0);
600 window.scrollTo(0, 0);
604 sendUnreadCount(read: boolean) {
605 let urcs = UserService.Instance.unreadInboxCountSub;
607 urcs.next(urcs.getValue() - 1);
609 urcs.next(urcs.getValue() + 1);
613 parseMessage(msg: any) {
614 let op = wsUserOp(msg);
617 toast(i18n.t(msg.error), "danger");
619 } else if (msg.reconnect) {
621 } else if (op == UserOperation.GetReplies) {
622 let data = wsJsonToRes<GetRepliesResponse>(msg, GetRepliesResponse);
623 this.state.replies = data.replies;
624 this.state.combined = this.buildCombined();
625 this.state.loading = false;
626 window.scrollTo(0, 0);
627 this.setState(this.state);
629 } else if (op == UserOperation.GetPersonMentions) {
630 let data = wsJsonToRes<GetPersonMentionsResponse>(
632 GetPersonMentionsResponse
634 this.state.mentions = data.mentions;
635 this.state.combined = this.buildCombined();
636 window.scrollTo(0, 0);
637 this.setState(this.state);
639 } else if (op == UserOperation.GetPrivateMessages) {
640 let data = wsJsonToRes<PrivateMessagesResponse>(
642 PrivateMessagesResponse
644 this.state.messages = data.private_messages;
645 this.state.combined = this.buildCombined();
646 window.scrollTo(0, 0);
647 this.setState(this.state);
649 } else if (op == UserOperation.EditPrivateMessage) {
650 let data = wsJsonToRes<PrivateMessageResponse>(
652 PrivateMessageResponse
654 let found: PrivateMessageView = this.state.messages.find(
656 m.private_message.id === data.private_message_view.private_message.id
659 let combinedView = this.state.combined.find(
660 i => i.id == data.private_message_view.private_message.id
661 ).view as PrivateMessageView;
662 found.private_message.content = combinedView.private_message.content =
663 data.private_message_view.private_message.content;
664 found.private_message.updated = combinedView.private_message.updated =
665 data.private_message_view.private_message.updated;
667 this.setState(this.state);
668 } else if (op == UserOperation.DeletePrivateMessage) {
669 let data = wsJsonToRes<PrivateMessageResponse>(
671 PrivateMessageResponse
673 let found: PrivateMessageView = this.state.messages.find(
675 m.private_message.id === data.private_message_view.private_message.id
678 let combinedView = this.state.combined.find(
679 i => i.id == data.private_message_view.private_message.id
680 ).view as PrivateMessageView;
681 found.private_message.deleted = combinedView.private_message.deleted =
682 data.private_message_view.private_message.deleted;
683 found.private_message.updated = combinedView.private_message.updated =
684 data.private_message_view.private_message.updated;
686 this.setState(this.state);
687 } else if (op == UserOperation.MarkPrivateMessageAsRead) {
688 let data = wsJsonToRes<PrivateMessageResponse>(
690 PrivateMessageResponse
692 let found: PrivateMessageView = this.state.messages.find(
694 m.private_message.id === data.private_message_view.private_message.id
698 let combinedView = this.state.combined.find(
699 i => i.id == data.private_message_view.private_message.id
700 ).view as PrivateMessageView;
701 found.private_message.updated = combinedView.private_message.updated =
702 data.private_message_view.private_message.updated;
704 // If youre in the unread view, just remove it from the list
706 this.state.unreadOrAll == UnreadOrAll.Unread &&
707 data.private_message_view.private_message.read
709 this.state.messages = this.state.messages.filter(
711 r.private_message.id !==
712 data.private_message_view.private_message.id
714 this.state.combined = this.state.combined.filter(
715 r => r.id !== data.private_message_view.private_message.id
718 found.private_message.read = combinedView.private_message.read =
719 data.private_message_view.private_message.read;
722 this.sendUnreadCount(data.private_message_view.private_message.read);
723 this.setState(this.state);
724 } else if (op == UserOperation.MarkAllAsRead) {
725 // Moved to be instant
727 op == UserOperation.EditComment ||
728 op == UserOperation.DeleteComment ||
729 op == UserOperation.RemoveComment
731 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
732 editCommentRes(data.comment_view, this.state.replies);
733 this.setState(this.state);
734 } else if (op == UserOperation.MarkCommentReplyAsRead) {
735 let data = wsJsonToRes<CommentReplyResponse>(msg, CommentReplyResponse);
738 let found = this.state.replies.find(
739 c => c.comment_reply.id == data.comment_reply_view.comment_reply.id
743 let combinedView = this.state.combined.find(
744 i => i.id == data.comment_reply_view.comment_reply.id
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
766 this.state.replies = this.state.replies.filter(
767 r => r.comment_reply.id !== data.comment_reply_view.comment_reply.id
769 this.state.combined = this.state.combined.filter(
770 r => r.id !== data.comment_reply_view.comment_reply.id
773 found.comment_reply.read = combinedView.comment_reply.read =
774 data.comment_reply_view.comment_reply.read;
777 this.sendUnreadCount(data.comment_reply_view.comment_reply.read);
778 this.setState(this.state);
779 } else if (op == UserOperation.MarkPersonMentionAsRead) {
780 let data = wsJsonToRes<PersonMentionResponse>(msg, PersonMentionResponse);
782 // TODO this might not be correct, it might need to use the comment id
783 let found = this.state.mentions.find(
784 c => c.person_mention.id == data.person_mention_view.person_mention.id
788 let combinedView = this.state.combined.find(
789 i => i.id == data.person_mention_view.person_mention.id
790 ).view as PersonMentionView;
791 found.comment.content = combinedView.comment.content =
792 data.person_mention_view.comment.content;
793 found.comment.updated = combinedView.comment.updated =
794 data.person_mention_view.comment.updated;
795 found.comment.removed = combinedView.comment.removed =
796 data.person_mention_view.comment.removed;
797 found.comment.deleted = combinedView.comment.deleted =
798 data.person_mention_view.comment.deleted;
799 found.counts.upvotes = combinedView.counts.upvotes =
800 data.person_mention_view.counts.upvotes;
801 found.counts.downvotes = combinedView.counts.downvotes =
802 data.person_mention_view.counts.downvotes;
803 found.counts.score = combinedView.counts.score =
804 data.person_mention_view.counts.score;
806 // If youre in the unread view, just remove it from the list
808 this.state.unreadOrAll == UnreadOrAll.Unread &&
809 data.person_mention_view.person_mention.read
811 this.state.mentions = this.state.mentions.filter(
813 r.person_mention.id !== data.person_mention_view.person_mention.id
815 this.state.combined = this.state.combined.filter(
816 r => r.id !== data.person_mention_view.person_mention.id
819 // TODO test to make sure these mentions are getting marked as read
820 found.person_mention.read = combinedView.person_mention.read =
821 data.person_mention_view.person_mention.read;
824 this.sendUnreadCount(data.person_mention_view.person_mention.read);
825 this.setState(this.state);
826 } else if (op == UserOperation.CreatePrivateMessage) {
827 let data = wsJsonToRes<PrivateMessageResponse>(
829 PrivateMessageResponse
831 UserService.Instance.myUserInfo.match({
834 data.private_message_view.recipient.id ==
835 mui.local_user_view.person.id
837 this.state.messages.unshift(data.private_message_view);
838 this.state.combined.unshift(
839 this.messageToReplyType(data.private_message_view)
841 this.setState(this.state);
846 } else if (op == UserOperation.SaveComment) {
847 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
848 saveCommentRes(data.comment_view, this.state.replies);
849 this.setState(this.state);
851 } else if (op == UserOperation.CreateCommentLike) {
852 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
853 createCommentLikeRes(data.comment_view, this.state.replies);
854 this.setState(this.state);
855 } else if (op == UserOperation.BlockPerson) {
856 let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
857 updatePersonBlock(data);
858 } else if (op == UserOperation.CreatePostReport) {
859 let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
861 toast(i18n.t("report_created"));
863 } else if (op == UserOperation.CreateCommentReport) {
864 let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
866 toast(i18n.t("report_created"));
871 isMention(view: any): view is PersonMentionView {
872 return (view as PersonMentionView).person_mention !== undefined;
875 isReply(view: any): view is CommentReplyView {
876 return (view as CommentReplyView).comment_reply !== undefined;