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)}
401 allLanguages={this.state.siteRes.all_languages}
404 case ReplyEnum.Mention:
410 comment_view: i.view as PersonMentionView,
415 viewType={CommentViewType.Flat}
418 maxCommentsShown={None}
423 enableDownvotes={enableDownvotes(this.state.siteRes)}
424 allLanguages={this.state.siteRes.all_languages}
427 case ReplyEnum.Message:
431 private_message_view={i.view as PrivateMessageView}
440 return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
447 nodes={commentsToFlatNodes(this.state.replies)}
448 viewType={CommentViewType.Flat}
451 maxCommentsShown={None}
456 enableDownvotes={enableDownvotes(this.state.siteRes)}
457 allLanguages={this.state.siteRes.all_languages}
466 {this.state.mentions.map(umv => (
468 key={umv.person_mention.id}
469 nodes={[{ comment_view: umv, children: [], depth: 0 }]}
470 viewType={CommentViewType.Flat}
473 maxCommentsShown={None}
478 enableDownvotes={enableDownvotes(this.state.siteRes)}
479 allLanguages={this.state.siteRes.all_languages}
489 {this.state.messages.map(pmv => (
491 key={pmv.private_message.id}
492 private_message_view={pmv}
499 handlePageChange(page: number) {
500 this.setState({ page });
504 handleUnreadOrAllChange(i: Inbox, event: any) {
505 i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
509 handleMessageTypeChange(i: Inbox, event: any) {
510 i.setState({ messageType: Number(event.target.value), page: 1 });
514 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
515 let promises: Promise<any>[] = [];
517 let sort = Some(CommentSortType.New);
519 // It can be /u/me, or /username/1
520 let repliesForm = new GetReplies({
522 unread_only: Some(true),
524 limit: Some(fetchLimit),
525 auth: req.auth.unwrap(),
527 promises.push(req.client.getReplies(repliesForm));
529 let personMentionsForm = new GetPersonMentions({
531 unread_only: Some(true),
533 limit: Some(fetchLimit),
534 auth: req.auth.unwrap(),
536 promises.push(req.client.getPersonMentions(personMentionsForm));
538 let privateMessagesForm = new GetPrivateMessages({
539 unread_only: Some(true),
541 limit: Some(fetchLimit),
542 auth: req.auth.unwrap(),
544 promises.push(req.client.getPrivateMessages(privateMessagesForm));
550 let sort = Some(this.state.sort);
551 let unread_only = Some(this.state.unreadOrAll == UnreadOrAll.Unread);
552 let page = Some(this.state.page);
553 let limit = Some(fetchLimit);
555 let repliesForm = new GetReplies({
560 auth: auth().unwrap(),
562 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
564 let personMentionsForm = new GetPersonMentions({
569 auth: auth().unwrap(),
571 WebSocketService.Instance.send(
572 wsClient.getPersonMentions(personMentionsForm)
575 let privateMessagesForm = new GetPrivateMessages({
579 auth: auth().unwrap(),
581 WebSocketService.Instance.send(
582 wsClient.getPrivateMessages(privateMessagesForm)
586 handleSortChange(val: CommentSortType) {
587 this.setState({ sort: val, page: 1 });
591 markAllAsRead(i: Inbox) {
592 WebSocketService.Instance.send(
593 wsClient.markAllAsRead({
594 auth: auth().unwrap(),
597 i.setState({ replies: [], mentions: [], messages: [] });
598 i.setState({ 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.setState({ replies: data.replies });
624 this.setState({ combined: this.buildCombined(), loading: false });
625 window.scrollTo(0, 0);
627 } else if (op == UserOperation.GetPersonMentions) {
628 let data = wsJsonToRes<GetPersonMentionsResponse>(
630 GetPersonMentionsResponse
632 this.setState({ mentions: data.mentions });
633 this.setState({ combined: this.buildCombined() });
634 window.scrollTo(0, 0);
636 } else if (op == UserOperation.GetPrivateMessages) {
637 let data = wsJsonToRes<PrivateMessagesResponse>(
639 PrivateMessagesResponse
641 this.setState({ messages: data.private_messages });
642 this.setState({ combined: this.buildCombined() });
643 window.scrollTo(0, 0);
645 } else if (op == UserOperation.EditPrivateMessage) {
646 let data = wsJsonToRes<PrivateMessageResponse>(
648 PrivateMessageResponse
650 let found: PrivateMessageView = this.state.messages.find(
652 m.private_message.id === data.private_message_view.private_message.id
655 let combinedView = this.state.combined.find(
656 i => i.id == data.private_message_view.private_message.id
657 ).view as PrivateMessageView;
658 found.private_message.content = combinedView.private_message.content =
659 data.private_message_view.private_message.content;
660 found.private_message.updated = combinedView.private_message.updated =
661 data.private_message_view.private_message.updated;
663 this.setState(this.state);
664 } else if (op == UserOperation.DeletePrivateMessage) {
665 let data = wsJsonToRes<PrivateMessageResponse>(
667 PrivateMessageResponse
669 let found: PrivateMessageView = this.state.messages.find(
671 m.private_message.id === data.private_message_view.private_message.id
674 let combinedView = this.state.combined.find(
675 i => i.id == data.private_message_view.private_message.id
676 ).view as PrivateMessageView;
677 found.private_message.deleted = combinedView.private_message.deleted =
678 data.private_message_view.private_message.deleted;
679 found.private_message.updated = combinedView.private_message.updated =
680 data.private_message_view.private_message.updated;
682 this.setState(this.state);
683 } else if (op == UserOperation.MarkPrivateMessageAsRead) {
684 let data = wsJsonToRes<PrivateMessageResponse>(
686 PrivateMessageResponse
688 let found: PrivateMessageView = this.state.messages.find(
690 m.private_message.id === data.private_message_view.private_message.id
694 let combinedView = this.state.combined.find(
696 i.id == data.private_message_view.private_message.id &&
697 i.type_ == ReplyEnum.Message
698 ).view as PrivateMessageView;
699 found.private_message.updated = combinedView.private_message.updated =
700 data.private_message_view.private_message.updated;
702 // If youre in the unread view, just remove it from the list
704 this.state.unreadOrAll == UnreadOrAll.Unread &&
705 data.private_message_view.private_message.read
708 messages: this.state.messages.filter(
710 r.private_message.id !==
711 data.private_message_view.private_message.id
715 combined: this.state.combined.filter(
716 r => r.id !== data.private_message_view.private_message.id
720 found.private_message.read = combinedView.private_message.read =
721 data.private_message_view.private_message.read;
724 this.sendUnreadCount(data.private_message_view.private_message.read);
725 this.setState(this.state);
726 } else if (op == UserOperation.MarkAllAsRead) {
727 // Moved to be instant
729 op == UserOperation.EditComment ||
730 op == UserOperation.DeleteComment ||
731 op == UserOperation.RemoveComment
733 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
734 editCommentRes(data.comment_view, this.state.replies);
735 this.setState(this.state);
736 } else if (op == UserOperation.MarkCommentReplyAsRead) {
737 let data = wsJsonToRes<CommentReplyResponse>(msg, CommentReplyResponse);
739 let found = this.state.replies.find(
740 c => c.comment_reply.id == data.comment_reply_view.comment_reply.id
744 let combinedView = this.state.combined.find(
746 i.id == data.comment_reply_view.comment_reply.id &&
747 i.type_ == ReplyEnum.Reply
748 ).view as CommentReplyView;
749 found.comment.content = combinedView.comment.content =
750 data.comment_reply_view.comment.content;
751 found.comment.updated = combinedView.comment.updated =
752 data.comment_reply_view.comment.updated;
753 found.comment.removed = combinedView.comment.removed =
754 data.comment_reply_view.comment.removed;
755 found.comment.deleted = combinedView.comment.deleted =
756 data.comment_reply_view.comment.deleted;
757 found.counts.upvotes = combinedView.counts.upvotes =
758 data.comment_reply_view.counts.upvotes;
759 found.counts.downvotes = combinedView.counts.downvotes =
760 data.comment_reply_view.counts.downvotes;
761 found.counts.score = combinedView.counts.score =
762 data.comment_reply_view.counts.score;
764 // If youre in the unread view, just remove it from the list
766 this.state.unreadOrAll == UnreadOrAll.Unread &&
767 data.comment_reply_view.comment_reply.read
770 replies: this.state.replies.filter(
772 r.comment_reply.id !== data.comment_reply_view.comment_reply.id
776 combined: this.state.combined.filter(
777 r => r.id !== data.comment_reply_view.comment_reply.id
781 found.comment_reply.read = combinedView.comment_reply.read =
782 data.comment_reply_view.comment_reply.read;
785 this.sendUnreadCount(data.comment_reply_view.comment_reply.read);
786 this.setState(this.state);
787 } else if (op == UserOperation.MarkPersonMentionAsRead) {
788 let data = wsJsonToRes<PersonMentionResponse>(msg, PersonMentionResponse);
790 // TODO this might not be correct, it might need to use the comment id
791 let found = this.state.mentions.find(
792 c => c.person_mention.id == data.person_mention_view.person_mention.id
796 let combinedView = this.state.combined.find(
798 i.id == data.person_mention_view.person_mention.id &&
799 i.type_ == ReplyEnum.Mention
800 ).view as PersonMentionView;
801 found.comment.content = combinedView.comment.content =
802 data.person_mention_view.comment.content;
803 found.comment.updated = combinedView.comment.updated =
804 data.person_mention_view.comment.updated;
805 found.comment.removed = combinedView.comment.removed =
806 data.person_mention_view.comment.removed;
807 found.comment.deleted = combinedView.comment.deleted =
808 data.person_mention_view.comment.deleted;
809 found.counts.upvotes = combinedView.counts.upvotes =
810 data.person_mention_view.counts.upvotes;
811 found.counts.downvotes = combinedView.counts.downvotes =
812 data.person_mention_view.counts.downvotes;
813 found.counts.score = combinedView.counts.score =
814 data.person_mention_view.counts.score;
816 // If youre in the unread view, just remove it from the list
818 this.state.unreadOrAll == UnreadOrAll.Unread &&
819 data.person_mention_view.person_mention.read
822 mentions: this.state.mentions.filter(
824 r.person_mention.id !==
825 data.person_mention_view.person_mention.id
829 combined: this.state.combined.filter(
830 r => r.id !== data.person_mention_view.person_mention.id
834 // TODO test to make sure these mentions are getting marked as read
835 found.person_mention.read = combinedView.person_mention.read =
836 data.person_mention_view.person_mention.read;
839 this.sendUnreadCount(data.person_mention_view.person_mention.read);
840 this.setState(this.state);
841 } else if (op == UserOperation.CreatePrivateMessage) {
842 let data = wsJsonToRes<PrivateMessageResponse>(
844 PrivateMessageResponse
846 UserService.Instance.myUserInfo.match({
849 data.private_message_view.recipient.id ==
850 mui.local_user_view.person.id
852 this.state.messages.unshift(data.private_message_view);
853 this.state.combined.unshift(
854 this.messageToReplyType(data.private_message_view)
856 this.setState(this.state);
861 } else if (op == UserOperation.SaveComment) {
862 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
863 saveCommentRes(data.comment_view, this.state.replies);
864 this.setState(this.state);
866 } else if (op == UserOperation.CreateCommentLike) {
867 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
868 createCommentLikeRes(data.comment_view, this.state.replies);
869 this.setState(this.state);
870 } else if (op == UserOperation.BlockPerson) {
871 let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
872 updatePersonBlock(data);
873 } else if (op == UserOperation.CreatePostReport) {
874 let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
876 toast(i18n.t("report_created"));
878 } else if (op == UserOperation.CreateCommentReport) {
879 let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
881 toast(i18n.t("report_created"));
886 isMention(view: any): view is PersonMentionView {
887 return (view as PersonMentionView).person_mention !== undefined;
890 isReply(view: any): view is CommentReplyView {
891 return (view as CommentReplyView).comment_reply !== undefined;