1 import { Component, linkEvent } from "inferno";
6 GetPersonMentionsResponse,
10 PersonMentionResponse,
12 PrivateMessageResponse,
13 PrivateMessagesResponse,
18 } from "lemmy-js-client";
19 import { Subscription } from "rxjs";
20 import { i18n } from "../../i18next";
21 import { InitialFetchRequest } from "../../interfaces";
22 import { UserService, WebSocketService } from "../../services";
39 import { CommentNodes } from "../comment/comment-nodes";
40 import { HtmlTags } from "../common/html-tags";
41 import { Icon, Spinner } from "../common/icon";
42 import { Paginator } from "../common/paginator";
43 import { SortSelect } from "../common/sort-select";
44 import { PrivateMessage } from "../private_message/private-message";
66 view: CommentView | PrivateMessageView | PersonMentionView;
70 interface InboxState {
71 unreadOrAll: UnreadOrAll;
72 messageType: MessageType;
73 replies: CommentView[];
74 mentions: PersonMentionView[];
75 messages: PrivateMessageView[];
76 combined: ReplyType[];
83 export class Inbox extends Component<any, InboxState> {
84 private isoData = setIsoData(this.context);
85 private subscription: Subscription;
86 private emptyState: InboxState = {
87 unreadOrAll: UnreadOrAll.Unread,
88 messageType: MessageType.All,
95 site_view: this.isoData.site_res.site_view,
99 constructor(props: any, context: any) {
100 super(props, context);
102 this.state = this.emptyState;
103 this.handleSortChange = this.handleSortChange.bind(this);
104 this.handlePageChange = this.handlePageChange.bind(this);
106 if (!UserService.Instance.localUserView && isBrowser()) {
107 toast(i18n.t("not_logged_in"), "danger");
108 this.context.router.history.push(`/login`);
111 this.parseMessage = this.parseMessage.bind(this);
112 this.subscription = wsSubscribe(this.parseMessage);
114 // Only fetch the data if coming from another route
115 if (this.isoData.path == this.context.router.route.match.url) {
116 this.state.replies = this.isoData.routeData[0].replies || [];
117 this.state.mentions = this.isoData.routeData[1].mentions || [];
118 this.state.messages = this.isoData.routeData[2].messages || [];
119 this.state.combined = this.buildCombined();
120 this.state.loading = false;
126 componentWillUnmount() {
128 this.subscription.unsubscribe();
132 get documentTitle(): string {
133 return `@${UserService.Instance.localUserView.person.name} ${i18n.t(
135 )} - ${this.state.site_view.site.name}`;
140 <div class="container">
141 {this.state.loading ? (
149 title={this.documentTitle}
150 path={this.context.router.route.match.url}
156 href={`/feeds/inbox/${UserService.Instance.auth}.xml`}
160 <Icon icon="rss" classes="ml-2 text-muted small" />
164 {this.state.replies.length +
165 this.state.mentions.length +
166 this.state.messages.length >
168 this.state.unreadOrAll == UnreadOrAll.Unread && (
170 class="btn btn-secondary mb-2"
171 onClick={linkEvent(this, this.markAllAsRead)}
173 {i18n.t("mark_all_as_read")}
177 {this.state.messageType == MessageType.All && this.all()}
178 {this.state.messageType == MessageType.Replies && this.replies()}
179 {this.state.messageType == MessageType.Mentions &&
181 {this.state.messageType == MessageType.Messages &&
184 page={this.state.page}
185 onChange={this.handlePageChange}
194 unreadOrAllRadios() {
196 <div class="btn-group btn-group-toggle flex-wrap mb-2">
198 className={`btn btn-outline-secondary pointer
199 ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
204 value={UnreadOrAll.Unread}
205 checked={this.state.unreadOrAll == UnreadOrAll.Unread}
206 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
211 className={`btn btn-outline-secondary pointer
212 ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
217 value={UnreadOrAll.All}
218 checked={this.state.unreadOrAll == UnreadOrAll.All}
219 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
227 messageTypeRadios() {
229 <div class="btn-group btn-group-toggle flex-wrap mb-2">
231 className={`btn btn-outline-secondary pointer
232 ${this.state.messageType == MessageType.All && "active"}
237 value={MessageType.All}
238 checked={this.state.messageType == MessageType.All}
239 onChange={linkEvent(this, this.handleMessageTypeChange)}
244 className={`btn btn-outline-secondary pointer
245 ${this.state.messageType == MessageType.Replies && "active"}
250 value={MessageType.Replies}
251 checked={this.state.messageType == MessageType.Replies}
252 onChange={linkEvent(this, this.handleMessageTypeChange)}
257 className={`btn btn-outline-secondary pointer
258 ${this.state.messageType == MessageType.Mentions && "active"}
263 value={MessageType.Mentions}
264 checked={this.state.messageType == MessageType.Mentions}
265 onChange={linkEvent(this, this.handleMessageTypeChange)}
270 className={`btn btn-outline-secondary pointer
271 ${this.state.messageType == MessageType.Messages && "active"}
276 value={MessageType.Messages}
277 checked={this.state.messageType == MessageType.Messages}
278 onChange={linkEvent(this, this.handleMessageTypeChange)}
288 <div className="mb-2">
289 <span class="mr-3">{this.unreadOrAllRadios()}</span>
290 <span class="mr-3">{this.messageTypeRadios()}</span>
292 sort={this.state.sort}
293 onChange={this.handleSortChange}
301 replyToReplyType(r: CommentView): ReplyType {
304 type_: ReplyEnum.Reply,
306 published: r.comment.published,
310 mentionToReplyType(r: PersonMentionView): ReplyType {
312 id: r.person_mention.id,
313 type_: ReplyEnum.Mention,
315 published: r.comment.published,
319 messageToReplyType(r: PrivateMessageView): ReplyType {
321 id: r.private_message.id,
322 type_: ReplyEnum.Message,
324 published: r.private_message.published,
328 buildCombined(): ReplyType[] {
329 let replies: ReplyType[] = this.state.replies.map(r =>
330 this.replyToReplyType(r)
332 let mentions: ReplyType[] = this.state.mentions.map(r =>
333 this.mentionToReplyType(r)
335 let messages: ReplyType[] = this.state.messages.map(r =>
336 this.messageToReplyType(r)
339 return [...replies, ...mentions, ...messages].sort((a, b) =>
340 b.published.localeCompare(a.published)
344 renderReplyType(i: ReplyType) {
346 case ReplyEnum.Reply:
350 nodes={[{ comment_view: i.view as CommentView }]}
355 enableDownvotes={this.state.site_view.site.enable_downvotes}
358 case ReplyEnum.Mention:
362 nodes={[{ comment_view: i.view as PersonMentionView }]}
367 enableDownvotes={this.state.site_view.site.enable_downvotes}
370 case ReplyEnum.Message:
374 private_message_view={i.view as PrivateMessageView}
383 return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
390 nodes={commentsToFlatNodes(this.state.replies)}
395 enableDownvotes={this.state.site_view.site.enable_downvotes}
404 {this.state.mentions.map(umv => (
406 key={umv.person_mention.id}
407 nodes={[{ comment_view: umv }]}
412 enableDownvotes={this.state.site_view.site.enable_downvotes}
422 {this.state.messages.map(pmv => (
424 key={pmv.private_message.id}
425 private_message_view={pmv}
432 handlePageChange(page: number) {
433 this.setState({ page });
437 handleUnreadOrAllChange(i: Inbox, event: any) {
438 i.state.unreadOrAll = Number(event.target.value);
444 handleMessageTypeChange(i: Inbox, event: any) {
445 i.state.messageType = Number(event.target.value);
451 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
452 let promises: Promise<any>[] = [];
454 // It can be /u/me, or /username/1
455 let repliesForm: GetReplies = {
462 promises.push(req.client.getReplies(repliesForm));
464 let personMentionsForm: GetPersonMentions = {
471 promises.push(req.client.getPersonMentions(personMentionsForm));
473 let privateMessagesForm: GetPrivateMessages = {
479 promises.push(req.client.getPrivateMessages(privateMessagesForm));
485 let repliesForm: GetReplies = {
486 sort: this.state.sort,
487 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
488 page: this.state.page,
492 WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
494 let personMentionsForm: GetPersonMentions = {
495 sort: this.state.sort,
496 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
497 page: this.state.page,
501 WebSocketService.Instance.send(
502 wsClient.getPersonMentions(personMentionsForm)
505 let privateMessagesForm: GetPrivateMessages = {
506 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
507 page: this.state.page,
511 WebSocketService.Instance.send(
512 wsClient.getPrivateMessages(privateMessagesForm)
516 handleSortChange(val: SortType) {
517 this.state.sort = val;
519 this.setState(this.state);
523 markAllAsRead(i: Inbox) {
524 WebSocketService.Instance.send(
525 wsClient.markAllAsRead({
529 i.state.replies = [];
530 i.state.mentions = [];
531 i.state.messages = [];
533 window.scrollTo(0, 0);
537 parseMessage(msg: any) {
538 let op = wsUserOp(msg);
541 toast(i18n.t(msg.error), "danger");
543 } else if (msg.reconnect) {
545 } else if (op == UserOperation.GetReplies) {
546 let data = wsJsonToRes<GetRepliesResponse>(msg).data;
547 this.state.replies = data.replies;
548 this.state.combined = this.buildCombined();
549 this.state.loading = false;
550 this.sendUnreadCount();
551 window.scrollTo(0, 0);
552 this.setState(this.state);
554 } else if (op == UserOperation.GetPersonMentions) {
555 let data = wsJsonToRes<GetPersonMentionsResponse>(msg).data;
556 this.state.mentions = data.mentions;
557 this.state.combined = this.buildCombined();
558 this.sendUnreadCount();
559 window.scrollTo(0, 0);
560 this.setState(this.state);
562 } else if (op == UserOperation.GetPrivateMessages) {
563 let data = wsJsonToRes<PrivateMessagesResponse>(msg).data;
564 this.state.messages = data.private_messages;
565 this.state.combined = this.buildCombined();
566 this.sendUnreadCount();
567 window.scrollTo(0, 0);
568 this.setState(this.state);
570 } else if (op == UserOperation.EditPrivateMessage) {
571 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
572 let found: PrivateMessageView = this.state.messages.find(
574 m.private_message.id === data.private_message_view.private_message.id
577 let combinedView = this.state.combined.find(
578 i => i.id == data.private_message_view.private_message.id
579 ).view as PrivateMessageView;
580 found.private_message.content = combinedView.private_message.content =
581 data.private_message_view.private_message.content;
582 found.private_message.updated = combinedView.private_message.updated =
583 data.private_message_view.private_message.updated;
585 this.setState(this.state);
586 } else if (op == UserOperation.DeletePrivateMessage) {
587 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
588 let found: PrivateMessageView = this.state.messages.find(
590 m.private_message.id === data.private_message_view.private_message.id
593 let combinedView = this.state.combined.find(
594 i => i.id == data.private_message_view.private_message.id
595 ).view as PrivateMessageView;
596 found.private_message.deleted = combinedView.private_message.deleted =
597 data.private_message_view.private_message.deleted;
598 found.private_message.updated = combinedView.private_message.updated =
599 data.private_message_view.private_message.updated;
601 this.setState(this.state);
602 } else if (op == UserOperation.MarkPrivateMessageAsRead) {
603 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
604 let found: PrivateMessageView = this.state.messages.find(
606 m.private_message.id === data.private_message_view.private_message.id
610 let combinedView = this.state.combined.find(
611 i => i.id == data.private_message_view.private_message.id
612 ).view as PrivateMessageView;
613 found.private_message.updated = combinedView.private_message.updated =
614 data.private_message_view.private_message.updated;
616 // If youre in the unread view, just remove it from the list
618 this.state.unreadOrAll == UnreadOrAll.Unread &&
619 data.private_message_view.private_message.read
621 this.state.messages = this.state.messages.filter(
623 r.private_message.id !==
624 data.private_message_view.private_message.id
626 this.state.combined = this.state.combined.filter(
627 r => r.id !== data.private_message_view.private_message.id
630 found.private_message.read = combinedView.private_message.read =
631 data.private_message_view.private_message.read;
634 this.sendUnreadCount();
635 this.setState(this.state);
636 } else if (op == UserOperation.MarkAllAsRead) {
637 // Moved to be instant
639 op == UserOperation.EditComment ||
640 op == UserOperation.DeleteComment ||
641 op == UserOperation.RemoveComment
643 let data = wsJsonToRes<CommentResponse>(msg).data;
644 editCommentRes(data.comment_view, this.state.replies);
645 this.setState(this.state);
646 } else if (op == UserOperation.MarkCommentAsRead) {
647 let data = wsJsonToRes<CommentResponse>(msg).data;
649 // If youre in the unread view, just remove it from the list
651 this.state.unreadOrAll == UnreadOrAll.Unread &&
652 data.comment_view.comment.read
654 this.state.replies = this.state.replies.filter(
655 r => r.comment.id !== data.comment_view.comment.id
657 this.state.combined = this.state.combined.filter(
658 r => r.id !== data.comment_view.comment.id
661 let found = this.state.replies.find(
662 c => c.comment.id == data.comment_view.comment.id
664 let combinedView = this.state.combined.find(
665 i => i.id == data.comment_view.comment.id
666 ).view as CommentView;
667 found.comment.read = combinedView.comment.read =
668 data.comment_view.comment.read;
670 this.sendUnreadCount();
671 this.setState(this.state);
673 } else if (op == UserOperation.MarkPersonMentionAsRead) {
674 let data = wsJsonToRes<PersonMentionResponse>(msg).data;
676 // TODO this might not be correct, it might need to use the comment id
677 let found = this.state.mentions.find(
678 c => c.person_mention.id == data.person_mention_view.person_mention.id
682 let combinedView = this.state.combined.find(
683 i => i.id == data.person_mention_view.person_mention.id
684 ).view as PersonMentionView;
685 found.comment.content = combinedView.comment.content =
686 data.person_mention_view.comment.content;
687 found.comment.updated = combinedView.comment.updated =
688 data.person_mention_view.comment.updated;
689 found.comment.removed = combinedView.comment.removed =
690 data.person_mention_view.comment.removed;
691 found.comment.deleted = combinedView.comment.deleted =
692 data.person_mention_view.comment.deleted;
693 found.counts.upvotes = combinedView.counts.upvotes =
694 data.person_mention_view.counts.upvotes;
695 found.counts.downvotes = combinedView.counts.downvotes =
696 data.person_mention_view.counts.downvotes;
697 found.counts.score = combinedView.counts.score =
698 data.person_mention_view.counts.score;
700 // If youre in the unread view, just remove it from the list
702 this.state.unreadOrAll == UnreadOrAll.Unread &&
703 data.person_mention_view.person_mention.read
705 this.state.mentions = this.state.mentions.filter(
707 r.person_mention.id !== data.person_mention_view.person_mention.id
709 this.state.combined = this.state.combined.filter(
710 r => r.id !== data.person_mention_view.person_mention.id
713 // TODO test to make sure these mentions are getting marked as read
714 found.person_mention.read = combinedView.person_mention.read =
715 data.person_mention_view.person_mention.read;
718 this.sendUnreadCount();
719 this.setState(this.state);
720 } else if (op == UserOperation.CreateComment) {
721 let data = wsJsonToRes<CommentResponse>(msg).data;
724 data.recipient_ids.includes(
725 UserService.Instance.localUserView.local_user.id
728 this.state.replies.unshift(data.comment_view);
729 this.state.combined.unshift(this.replyToReplyType(data.comment_view));
730 this.setState(this.state);
732 data.comment_view.creator.id ==
733 UserService.Instance.localUserView.person.id
735 // TODO this seems wrong, you should be using form_id
736 toast(i18n.t("reply_sent"));
738 } else if (op == UserOperation.CreatePrivateMessage) {
739 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
741 data.private_message_view.recipient.id ==
742 UserService.Instance.localUserView.person.id
744 this.state.messages.unshift(data.private_message_view);
745 this.state.combined.unshift(
746 this.messageToReplyType(data.private_message_view)
748 this.setState(this.state);
750 } else if (op == UserOperation.SaveComment) {
751 let data = wsJsonToRes<CommentResponse>(msg).data;
752 saveCommentRes(data.comment_view, this.state.replies);
753 this.setState(this.state);
755 } else if (op == UserOperation.CreateCommentLike) {
756 let data = wsJsonToRes<CommentResponse>(msg).data;
757 createCommentLikeRes(data.comment_view, this.state.replies);
758 this.setState(this.state);
763 UserService.Instance.unreadCountSub.next(this.unreadCount());
766 unreadCount(): number {
768 this.state.replies.filter(r => !r.comment.read).length +
769 this.state.mentions.filter(r => !r.person_mention.read).length +
770 this.state.messages.filter(
772 UserService.Instance.localUserView &&
773 !r.private_message.read &&
774 // TODO also seems very strange and wrong
775 r.creator.id !== UserService.Instance.localUserView.person.id