1 import { Component, linkEvent } from "inferno";
7 GetPersonMentionsResponse,
11 PersonMentionResponse,
13 PrivateMessageResponse,
14 PrivateMessagesResponse,
19 } from "lemmy-js-client";
20 import { Subscription } from "rxjs";
21 import { i18n } from "../../i18next";
22 import { InitialFetchRequest } from "../../interfaces";
23 import { UserService, WebSocketService } from "../../services";
41 import { CommentNodes } from "../comment/comment-nodes";
42 import { HtmlTags } from "../common/html-tags";
43 import { Icon, Spinner } from "../common/icon";
44 import { Paginator } from "../common/paginator";
45 import { SortSelect } from "../common/sort-select";
46 import { PrivateMessage } from "../private_message/private-message";
68 view: CommentView | PrivateMessageView | PersonMentionView;
72 interface InboxState {
73 unreadOrAll: UnreadOrAll;
74 messageType: MessageType;
75 replies: CommentView[];
76 mentions: PersonMentionView[];
77 messages: PrivateMessageView[];
78 combined: ReplyType[];
85 export class Inbox extends Component<any, InboxState> {
86 private isoData = setIsoData(this.context);
87 private subscription: Subscription;
88 private emptyState: InboxState = {
89 unreadOrAll: UnreadOrAll.Unread,
90 messageType: MessageType.All,
97 site_view: this.isoData.site_res.site_view,
101 constructor(props: any, context: any) {
102 super(props, context);
104 this.state = this.emptyState;
105 this.handleSortChange = this.handleSortChange.bind(this);
106 this.handlePageChange = this.handlePageChange.bind(this);
108 if (!UserService.Instance.myUserInfo && isBrowser()) {
109 toast(i18n.t("not_logged_in"), "danger");
110 this.context.router.history.push(`/login`);
113 this.parseMessage = this.parseMessage.bind(this);
114 this.subscription = wsSubscribe(this.parseMessage);
116 // Only fetch the data if coming from another route
117 if (this.isoData.path == this.context.router.route.match.url) {
118 this.state.replies = this.isoData.routeData[0].replies || [];
119 this.state.mentions = this.isoData.routeData[1].mentions || [];
120 this.state.messages = this.isoData.routeData[2].messages || [];
121 this.state.combined = this.buildCombined();
122 this.state.loading = false;
128 componentWillUnmount() {
130 this.subscription.unsubscribe();
134 get documentTitle(): string {
136 UserService.Instance.myUserInfo.local_user_view.person.name
137 } ${i18n.t("inbox")} - ${this.state.site_view.site.name}`;
142 <div class="container">
143 {this.state.loading ? (
151 title={this.documentTitle}
152 path={this.context.router.route.match.url}
158 href={`/feeds/inbox/${UserService.Instance.auth}.xml`}
162 <Icon icon="rss" classes="ml-2 text-muted small" />
166 {this.state.replies.length +
167 this.state.mentions.length +
168 this.state.messages.length >
170 this.state.unreadOrAll == UnreadOrAll.Unread && (
172 class="btn btn-secondary mb-2"
173 onClick={linkEvent(this, this.markAllAsRead)}
175 {i18n.t("mark_all_as_read")}
179 {this.state.messageType == MessageType.All && this.all()}
180 {this.state.messageType == MessageType.Replies && this.replies()}
181 {this.state.messageType == MessageType.Mentions &&
183 {this.state.messageType == MessageType.Messages &&
186 page={this.state.page}
187 onChange={this.handlePageChange}
196 unreadOrAllRadios() {
198 <div class="btn-group btn-group-toggle flex-wrap mb-2">
200 className={`btn btn-outline-secondary pointer
201 ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
206 value={UnreadOrAll.Unread}
207 checked={this.state.unreadOrAll == UnreadOrAll.Unread}
208 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
213 className={`btn btn-outline-secondary pointer
214 ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
219 value={UnreadOrAll.All}
220 checked={this.state.unreadOrAll == UnreadOrAll.All}
221 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
229 messageTypeRadios() {
231 <div class="btn-group btn-group-toggle flex-wrap mb-2">
233 className={`btn btn-outline-secondary pointer
234 ${this.state.messageType == MessageType.All && "active"}
239 value={MessageType.All}
240 checked={this.state.messageType == MessageType.All}
241 onChange={linkEvent(this, this.handleMessageTypeChange)}
246 className={`btn btn-outline-secondary pointer
247 ${this.state.messageType == MessageType.Replies && "active"}
252 value={MessageType.Replies}
253 checked={this.state.messageType == MessageType.Replies}
254 onChange={linkEvent(this, this.handleMessageTypeChange)}
259 className={`btn btn-outline-secondary pointer
260 ${this.state.messageType == MessageType.Mentions && "active"}
265 value={MessageType.Mentions}
266 checked={this.state.messageType == MessageType.Mentions}
267 onChange={linkEvent(this, this.handleMessageTypeChange)}
272 className={`btn btn-outline-secondary pointer
273 ${this.state.messageType == MessageType.Messages && "active"}
278 value={MessageType.Messages}
279 checked={this.state.messageType == MessageType.Messages}
280 onChange={linkEvent(this, this.handleMessageTypeChange)}
290 <div className="mb-2">
291 <span class="mr-3">{this.unreadOrAllRadios()}</span>
292 <span class="mr-3">{this.messageTypeRadios()}</span>
294 sort={this.state.sort}
295 onChange={this.handleSortChange}
303 replyToReplyType(r: CommentView): ReplyType {
306 type_: ReplyEnum.Reply,
308 published: r.comment.published,
312 mentionToReplyType(r: PersonMentionView): ReplyType {
314 id: r.person_mention.id,
315 type_: ReplyEnum.Mention,
317 published: r.comment.published,
321 messageToReplyType(r: PrivateMessageView): ReplyType {
323 id: r.private_message.id,
324 type_: ReplyEnum.Message,
326 published: r.private_message.published,
330 buildCombined(): ReplyType[] {
331 let replies: ReplyType[] = this.state.replies.map(r =>
332 this.replyToReplyType(r)
334 let mentions: ReplyType[] = this.state.mentions.map(r =>
335 this.mentionToReplyType(r)
337 let messages: ReplyType[] = this.state.messages.map(r =>
338 this.messageToReplyType(r)
341 return [...replies, ...mentions, ...messages].sort((a, b) =>
342 b.published.localeCompare(a.published)
346 renderReplyType(i: ReplyType) {
348 case ReplyEnum.Reply:
352 nodes={[{ comment_view: i.view as CommentView }]}
357 enableDownvotes={this.state.site_view.site.enable_downvotes}
360 case ReplyEnum.Mention:
364 nodes={[{ comment_view: i.view as PersonMentionView }]}
369 enableDownvotes={this.state.site_view.site.enable_downvotes}
372 case ReplyEnum.Message:
376 private_message_view={i.view as PrivateMessageView}
385 return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
392 nodes={commentsToFlatNodes(this.state.replies)}
397 enableDownvotes={this.state.site_view.site.enable_downvotes}
406 {this.state.mentions.map(umv => (
408 key={umv.person_mention.id}
409 nodes={[{ comment_view: umv }]}
414 enableDownvotes={this.state.site_view.site.enable_downvotes}
424 {this.state.messages.map(pmv => (
426 key={pmv.private_message.id}
427 private_message_view={pmv}
434 handlePageChange(page: number) {
435 this.setState({ page });
439 handleUnreadOrAllChange(i: Inbox, event: any) {
440 i.state.unreadOrAll = Number(event.target.value);
446 handleMessageTypeChange(i: Inbox, event: any) {
447 i.state.messageType = Number(event.target.value);
453 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
454 let promises: Promise<any>[] = [];
456 // It can be /u/me, or /username/1
457 let repliesForm: GetReplies = {
464 promises.push(req.client.getReplies(repliesForm));
466 let personMentionsForm: GetPersonMentions = {
473 promises.push(req.client.getPersonMentions(personMentionsForm));
475 let privateMessagesForm: GetPrivateMessages = {
481 promises.push(req.client.getPrivateMessages(privateMessagesForm));
487 let repliesForm: GetReplies = {
488 sort: this.state.sort,
489 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
490 page: this.state.page,
494 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
496 let personMentionsForm: GetPersonMentions = {
497 sort: this.state.sort,
498 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
499 page: this.state.page,
503 WebSocketService.Instance.send(
504 wsClient.getPersonMentions(personMentionsForm)
507 let privateMessagesForm: GetPrivateMessages = {
508 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
509 page: this.state.page,
513 WebSocketService.Instance.send(
514 wsClient.getPrivateMessages(privateMessagesForm)
518 handleSortChange(val: SortType) {
519 this.state.sort = val;
521 this.setState(this.state);
525 markAllAsRead(i: Inbox) {
526 WebSocketService.Instance.send(
527 wsClient.markAllAsRead({
531 i.state.replies = [];
532 i.state.mentions = [];
533 i.state.messages = [];
535 window.scrollTo(0, 0);
539 parseMessage(msg: any) {
540 let op = wsUserOp(msg);
543 toast(i18n.t(msg.error), "danger");
545 } else if (msg.reconnect) {
547 } else if (op == UserOperation.GetReplies) {
548 let data = wsJsonToRes<GetRepliesResponse>(msg).data;
549 this.state.replies = data.replies;
550 this.state.combined = this.buildCombined();
551 this.state.loading = false;
552 this.sendUnreadCount();
553 window.scrollTo(0, 0);
554 this.setState(this.state);
556 } else if (op == UserOperation.GetPersonMentions) {
557 let data = wsJsonToRes<GetPersonMentionsResponse>(msg).data;
558 this.state.mentions = data.mentions;
559 this.state.combined = this.buildCombined();
560 this.sendUnreadCount();
561 window.scrollTo(0, 0);
562 this.setState(this.state);
564 } else if (op == UserOperation.GetPrivateMessages) {
565 let data = wsJsonToRes<PrivateMessagesResponse>(msg).data;
566 this.state.messages = data.private_messages;
567 this.state.combined = this.buildCombined();
568 this.sendUnreadCount();
569 window.scrollTo(0, 0);
570 this.setState(this.state);
572 } else if (op == UserOperation.EditPrivateMessage) {
573 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
574 let found: PrivateMessageView = this.state.messages.find(
576 m.private_message.id === data.private_message_view.private_message.id
579 let combinedView = this.state.combined.find(
580 i => i.id == data.private_message_view.private_message.id
581 ).view as PrivateMessageView;
582 found.private_message.content = combinedView.private_message.content =
583 data.private_message_view.private_message.content;
584 found.private_message.updated = combinedView.private_message.updated =
585 data.private_message_view.private_message.updated;
587 this.setState(this.state);
588 } else if (op == UserOperation.DeletePrivateMessage) {
589 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
590 let found: PrivateMessageView = this.state.messages.find(
592 m.private_message.id === data.private_message_view.private_message.id
595 let combinedView = this.state.combined.find(
596 i => i.id == data.private_message_view.private_message.id
597 ).view as PrivateMessageView;
598 found.private_message.deleted = combinedView.private_message.deleted =
599 data.private_message_view.private_message.deleted;
600 found.private_message.updated = combinedView.private_message.updated =
601 data.private_message_view.private_message.updated;
603 this.setState(this.state);
604 } else if (op == UserOperation.MarkPrivateMessageAsRead) {
605 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
606 let found: PrivateMessageView = this.state.messages.find(
608 m.private_message.id === data.private_message_view.private_message.id
612 let combinedView = this.state.combined.find(
613 i => i.id == data.private_message_view.private_message.id
614 ).view as PrivateMessageView;
615 found.private_message.updated = combinedView.private_message.updated =
616 data.private_message_view.private_message.updated;
618 // If youre in the unread view, just remove it from the list
620 this.state.unreadOrAll == UnreadOrAll.Unread &&
621 data.private_message_view.private_message.read
623 this.state.messages = this.state.messages.filter(
625 r.private_message.id !==
626 data.private_message_view.private_message.id
628 this.state.combined = this.state.combined.filter(
629 r => r.id !== data.private_message_view.private_message.id
632 found.private_message.read = combinedView.private_message.read =
633 data.private_message_view.private_message.read;
636 this.sendUnreadCount();
637 this.setState(this.state);
638 } else if (op == UserOperation.MarkAllAsRead) {
639 // Moved to be instant
641 op == UserOperation.EditComment ||
642 op == UserOperation.DeleteComment ||
643 op == UserOperation.RemoveComment
645 let data = wsJsonToRes<CommentResponse>(msg).data;
646 editCommentRes(data.comment_view, this.state.replies);
647 this.setState(this.state);
648 } else if (op == UserOperation.MarkCommentAsRead) {
649 let data = wsJsonToRes<CommentResponse>(msg).data;
651 // If youre in the unread view, just remove it from the list
653 this.state.unreadOrAll == UnreadOrAll.Unread &&
654 data.comment_view.comment.read
656 this.state.replies = this.state.replies.filter(
657 r => r.comment.id !== data.comment_view.comment.id
659 this.state.combined = this.state.combined.filter(
660 r => r.id !== data.comment_view.comment.id
663 let found = this.state.replies.find(
664 c => c.comment.id == data.comment_view.comment.id
666 let combinedView = this.state.combined.find(
667 i => i.id == data.comment_view.comment.id
668 ).view as CommentView;
669 found.comment.read = combinedView.comment.read =
670 data.comment_view.comment.read;
672 this.sendUnreadCount();
673 this.setState(this.state);
675 } else if (op == UserOperation.MarkPersonMentionAsRead) {
676 let data = wsJsonToRes<PersonMentionResponse>(msg).data;
678 // TODO this might not be correct, it might need to use the comment id
679 let found = this.state.mentions.find(
680 c => c.person_mention.id == data.person_mention_view.person_mention.id
684 let combinedView = this.state.combined.find(
685 i => i.id == data.person_mention_view.person_mention.id
686 ).view as PersonMentionView;
687 found.comment.content = combinedView.comment.content =
688 data.person_mention_view.comment.content;
689 found.comment.updated = combinedView.comment.updated =
690 data.person_mention_view.comment.updated;
691 found.comment.removed = combinedView.comment.removed =
692 data.person_mention_view.comment.removed;
693 found.comment.deleted = combinedView.comment.deleted =
694 data.person_mention_view.comment.deleted;
695 found.counts.upvotes = combinedView.counts.upvotes =
696 data.person_mention_view.counts.upvotes;
697 found.counts.downvotes = combinedView.counts.downvotes =
698 data.person_mention_view.counts.downvotes;
699 found.counts.score = combinedView.counts.score =
700 data.person_mention_view.counts.score;
702 // If youre in the unread view, just remove it from the list
704 this.state.unreadOrAll == UnreadOrAll.Unread &&
705 data.person_mention_view.person_mention.read
707 this.state.mentions = this.state.mentions.filter(
709 r.person_mention.id !== data.person_mention_view.person_mention.id
711 this.state.combined = this.state.combined.filter(
712 r => r.id !== data.person_mention_view.person_mention.id
715 // TODO test to make sure these mentions are getting marked as read
716 found.person_mention.read = combinedView.person_mention.read =
717 data.person_mention_view.person_mention.read;
720 this.sendUnreadCount();
721 this.setState(this.state);
722 } else if (op == UserOperation.CreateComment) {
723 let data = wsJsonToRes<CommentResponse>(msg).data;
726 data.recipient_ids.includes(
727 UserService.Instance.myUserInfo.local_user_view.local_user.id
730 this.state.replies.unshift(data.comment_view);
731 this.state.combined.unshift(this.replyToReplyType(data.comment_view));
732 this.setState(this.state);
734 data.comment_view.creator.id ==
735 UserService.Instance.myUserInfo.local_user_view.person.id
737 // TODO this seems wrong, you should be using form_id
738 toast(i18n.t("reply_sent"));
740 } else if (op == UserOperation.CreatePrivateMessage) {
741 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
743 data.private_message_view.recipient.id ==
744 UserService.Instance.myUserInfo.local_user_view.person.id
746 this.state.messages.unshift(data.private_message_view);
747 this.state.combined.unshift(
748 this.messageToReplyType(data.private_message_view)
750 this.setState(this.state);
752 } else if (op == UserOperation.SaveComment) {
753 let data = wsJsonToRes<CommentResponse>(msg).data;
754 saveCommentRes(data.comment_view, this.state.replies);
755 this.setState(this.state);
757 } else if (op == UserOperation.CreateCommentLike) {
758 let data = wsJsonToRes<CommentResponse>(msg).data;
759 createCommentLikeRes(data.comment_view, this.state.replies);
760 this.setState(this.state);
761 } else if (op == UserOperation.BlockPerson) {
762 let data = wsJsonToRes<BlockPersonResponse>(msg).data;
763 updatePersonBlock(data);
768 UserService.Instance.unreadCountSub.next(this.unreadCount());
771 unreadCount(): number {
773 this.state.replies.filter(r => !r.comment.read).length +
774 this.state.mentions.filter(r => !r.person_mention.read).length +
775 this.state.messages.filter(
777 UserService.Instance.myUserInfo &&
778 !r.private_message.read &&
779 // TODO also seems very strange and wrong
781 UserService.Instance.myUserInfo.local_user_view.person.id