1 import { Component, linkEvent } from 'inferno';
2 import { Subscription } from 'rxjs';
10 GetUserMentionsResponse,
15 PrivateMessagesResponse,
16 PrivateMessageResponse,
19 } from 'lemmy-js-client';
20 import { WebSocketService, UserService } from '../services';
35 import { CommentNodes } from './comment-nodes';
36 import { PrivateMessage } from './private-message';
37 import { HtmlTags } from './html-tags';
38 import { SortSelect } from './sort-select';
39 import { i18n } from '../i18next';
40 import { InitialFetchRequest } from 'shared/interfaces';
62 view: CommentView | PrivateMessageView | UserMentionView;
66 interface InboxState {
67 unreadOrAll: UnreadOrAll;
68 messageType: MessageType;
69 replies: CommentView[];
70 mentions: UserMentionView[];
71 messages: PrivateMessageView[];
78 export class Inbox extends Component<any, InboxState> {
79 private isoData = setIsoData(this.context);
80 private subscription: Subscription;
81 private emptyState: InboxState = {
82 unreadOrAll: UnreadOrAll.Unread,
83 messageType: MessageType.All,
89 site_view: this.isoData.site_res.site_view,
93 constructor(props: any, context: any) {
94 super(props, context);
96 this.state = this.emptyState;
97 this.handleSortChange = this.handleSortChange.bind(this);
99 if (!UserService.Instance.user && isBrowser()) {
100 toast(i18n.t('not_logged_in'), 'danger');
101 this.context.router.history.push(`/login`);
104 this.parseMessage = this.parseMessage.bind(this);
105 this.subscription = wsSubscribe(this.parseMessage);
107 // Only fetch the data if coming from another route
108 if (this.isoData.path == this.context.router.route.match.url) {
109 this.state.replies = this.isoData.routeData[0].replies;
110 this.state.mentions = this.isoData.routeData[1].mentions;
111 this.state.messages = this.isoData.routeData[2].messages;
112 this.sendUnreadCount();
113 this.state.loading = false;
119 componentWillUnmount() {
121 this.subscription.unsubscribe();
125 get documentTitle(): string {
126 return `@${UserService.Instance.user.name} ${i18n.t('inbox')} - ${
127 this.state.site_view.site.name
133 <div class="container">
134 {this.state.loading ? (
136 <svg class="icon icon-spinner spin">
137 <use xlinkHref="#icon-spinner"></use>
144 title={this.documentTitle}
145 path={this.context.router.route.match.url}
151 href={`/feeds/inbox/${UserService.Instance.auth}.xml`}
156 <svg class="icon ml-2 text-muted small">
157 <use xlinkHref="#icon-rss">#</use>
162 {this.state.replies.length +
163 this.state.mentions.length +
164 this.state.messages.length >
166 this.state.unreadOrAll == UnreadOrAll.Unread && (
167 <ul class="list-inline mb-1 text-muted small font-weight-bold">
168 <li className="list-inline-item">
171 onClick={linkEvent(this, this.markAllAsRead)}
173 {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 &&
193 unreadOrAllRadios() {
195 <div class="btn-group btn-group-toggle flex-wrap mb-2">
197 className={`btn btn-outline-secondary pointer
198 ${this.state.unreadOrAll == UnreadOrAll.Unread && 'active'}
203 value={UnreadOrAll.Unread}
204 checked={this.state.unreadOrAll == UnreadOrAll.Unread}
205 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
210 className={`btn btn-outline-secondary pointer
211 ${this.state.unreadOrAll == UnreadOrAll.All && 'active'}
216 value={UnreadOrAll.All}
217 checked={this.state.unreadOrAll == UnreadOrAll.All}
218 onChange={linkEvent(this, this.handleUnreadOrAllChange)}
226 messageTypeRadios() {
228 <div class="btn-group btn-group-toggle flex-wrap mb-2">
230 className={`btn btn-outline-secondary pointer
231 ${this.state.messageType == MessageType.All && 'active'}
236 value={MessageType.All}
237 checked={this.state.messageType == MessageType.All}
238 onChange={linkEvent(this, this.handleMessageTypeChange)}
243 className={`btn btn-outline-secondary pointer
244 ${this.state.messageType == MessageType.Replies && 'active'}
249 value={MessageType.Replies}
250 checked={this.state.messageType == MessageType.Replies}
251 onChange={linkEvent(this, this.handleMessageTypeChange)}
256 className={`btn btn-outline-secondary pointer
257 ${this.state.messageType == MessageType.Mentions && 'active'}
262 value={MessageType.Mentions}
263 checked={this.state.messageType == MessageType.Mentions}
264 onChange={linkEvent(this, this.handleMessageTypeChange)}
269 className={`btn btn-outline-secondary pointer
270 ${this.state.messageType == MessageType.Messages && 'active'}
275 value={MessageType.Messages}
276 checked={this.state.messageType == MessageType.Messages}
277 onChange={linkEvent(this, this.handleMessageTypeChange)}
287 <div className="mb-2">
288 <span class="mr-3">{this.unreadOrAllRadios()}</span>
289 <span class="mr-3">{this.messageTypeRadios()}</span>
291 sort={this.state.sort}
292 onChange={this.handleSortChange}
299 combined(): ReplyType[] {
301 let replies: ReplyType[] = this.state.replies.map(r => ({
303 type_: ReplyEnum.Reply,
305 published: r.comment.published,
307 let mentions: ReplyType[] = this.state.mentions.map(r => ({
309 type_: ReplyEnum.Mention,
311 published: r.comment.published,
313 let messages: ReplyType[] = this.state.messages.map(r => ({
315 type_: ReplyEnum.Message,
317 published: r.private_message.published,
320 return [...replies, ...mentions, ...messages].sort((a, b) =>
321 b.published.localeCompare(a.published)
325 renderReplyType(i: ReplyType) {
327 case ReplyEnum.Reply:
331 nodes={[{ comment_view: i.view as CommentView }]}
336 enableDownvotes={this.state.site_view.site.enable_downvotes}
339 case ReplyEnum.Mention:
343 nodes={[{ comment_view: i.view as UserMentionView }]}
348 enableDownvotes={this.state.site_view.site.enable_downvotes}
351 case ReplyEnum.Message:
355 private_message_view={i.view as PrivateMessageView}
364 return <div>{this.combined().map(i => this.renderReplyType(i))}</div>;
371 nodes={commentsToFlatNodes(this.state.replies)}
376 enableDownvotes={this.state.site_view.site.enable_downvotes}
385 {this.state.mentions.map(umv => (
387 key={umv.user_mention.id}
388 nodes={[{ comment_view: umv }]}
393 enableDownvotes={this.state.site_view.site.enable_downvotes}
403 {this.state.messages.map(pmv => (
405 key={pmv.private_message.id}
406 private_message_view={pmv}
416 {this.state.page > 1 && (
418 class="btn btn-secondary mr-1"
419 onClick={linkEvent(this, this.prevPage)}
424 {this.unreadCount() > 0 && (
426 class="btn btn-secondary"
427 onClick={linkEvent(this, this.nextPage)}
448 handleUnreadOrAllChange(i: Inbox, event: any) {
449 i.state.unreadOrAll = Number(event.target.value);
455 handleMessageTypeChange(i: Inbox, event: any) {
456 i.state.messageType = Number(event.target.value);
462 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
463 let promises: Promise<any>[] = [];
465 // It can be /u/me, or /username/1
466 let repliesForm: GetReplies = {
473 promises.push(req.client.getReplies(repliesForm));
475 let userMentionsForm: GetUserMentions = {
482 promises.push(req.client.getUserMentions(userMentionsForm));
484 let privateMessagesForm: GetPrivateMessages = {
490 promises.push(req.client.getPrivateMessages(privateMessagesForm));
496 let repliesForm: GetReplies = {
497 sort: this.state.sort,
498 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
499 page: this.state.page,
501 auth: UserService.Instance.authField(),
503 WebSocketService.Instance.client.getReplies(repliesForm);
505 let userMentionsForm: GetUserMentions = {
506 sort: this.state.sort,
507 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
508 page: this.state.page,
510 auth: UserService.Instance.authField(),
512 WebSocketService.Instance.client.getUserMentions(userMentionsForm);
514 let privateMessagesForm: GetPrivateMessages = {
515 unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
516 page: this.state.page,
518 auth: UserService.Instance.authField(),
520 WebSocketService.Instance.client.getPrivateMessages(privateMessagesForm);
523 handleSortChange(val: SortType) {
524 this.state.sort = val;
526 this.setState(this.state);
530 markAllAsRead(i: Inbox) {
531 WebSocketService.Instance.client.markAllAsRead({
532 auth: UserService.Instance.authField(),
534 i.state.replies = [];
535 i.state.mentions = [];
536 i.state.messages = [];
538 window.scrollTo(0, 0);
542 parseMessage(msg: any) {
543 let op = wsUserOp(msg);
545 toast(i18n.t(msg.error), 'danger');
547 } else if (msg.reconnect) {
549 } else if (op == UserOperation.GetReplies) {
550 let data = wsJsonToRes<GetRepliesResponse>(msg).data;
551 this.state.replies = data.replies;
552 this.state.loading = false;
553 this.sendUnreadCount();
554 window.scrollTo(0, 0);
555 this.setState(this.state);
557 } else if (op == UserOperation.GetUserMentions) {
558 let data = wsJsonToRes<GetUserMentionsResponse>(msg).data;
559 this.state.mentions = data.mentions;
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.sendUnreadCount();
568 window.scrollTo(0, 0);
569 this.setState(this.state);
571 } else if (op == UserOperation.EditPrivateMessage) {
572 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
573 let found: PrivateMessageView = this.state.messages.find(
575 m.private_message.id === data.private_message_view.private_message.id
578 found.private_message.content =
579 data.private_message_view.private_message.content;
580 found.private_message.updated =
581 data.private_message_view.private_message.updated;
583 this.setState(this.state);
584 } else if (op == UserOperation.DeletePrivateMessage) {
585 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
586 let found: PrivateMessageView = this.state.messages.find(
588 m.private_message.id === data.private_message_view.private_message.id
591 found.private_message.deleted =
592 data.private_message_view.private_message.deleted;
593 found.private_message.updated =
594 data.private_message_view.private_message.updated;
596 this.setState(this.state);
597 } else if (op == UserOperation.MarkPrivateMessageAsRead) {
598 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
599 let found: PrivateMessageView = this.state.messages.find(
601 m.private_message.id === data.private_message_view.private_message.id
605 found.private_message.updated =
606 data.private_message_view.private_message.updated;
608 // If youre in the unread view, just remove it from the list
610 this.state.unreadOrAll == UnreadOrAll.Unread &&
611 data.private_message_view.private_message.read
613 this.state.messages = this.state.messages.filter(
615 r.private_message.id !==
616 data.private_message_view.private_message.id
619 let found = this.state.messages.find(
621 c.private_message.id ==
622 data.private_message_view.private_message.id
624 found.private_message.read =
625 data.private_message_view.private_message.read;
628 this.sendUnreadCount();
629 this.setState(this.state);
630 } else if (op == UserOperation.MarkAllAsRead) {
631 // Moved to be instant
633 op == UserOperation.EditComment ||
634 op == UserOperation.DeleteComment ||
635 op == UserOperation.RemoveComment
637 let data = wsJsonToRes<CommentResponse>(msg).data;
638 editCommentRes(data.comment_view, this.state.replies);
639 this.setState(this.state);
640 } else if (op == UserOperation.MarkCommentAsRead) {
641 let data = wsJsonToRes<CommentResponse>(msg).data;
643 // If youre in the unread view, just remove it from the list
645 this.state.unreadOrAll == UnreadOrAll.Unread &&
646 data.comment_view.comment.read
648 this.state.replies = this.state.replies.filter(
649 r => r.comment.id !== data.comment_view.comment.id
652 let found = this.state.replies.find(
653 c => c.comment.id == data.comment_view.comment.id
655 found.comment.read = data.comment_view.comment.read;
657 this.sendUnreadCount();
658 this.setState(this.state);
660 } else if (op == UserOperation.MarkUserMentionAsRead) {
661 let data = wsJsonToRes<UserMentionResponse>(msg).data;
663 // TODO this might not be correct, it might need to use the comment id
664 let found = this.state.mentions.find(
665 c => c.user_mention.id == data.user_mention_view.user_mention.id
667 found.comment.content = data.user_mention_view.comment.content;
668 found.comment.updated = data.user_mention_view.comment.updated;
669 found.comment.removed = data.user_mention_view.comment.removed;
670 found.comment.deleted = data.user_mention_view.comment.deleted;
671 found.counts.upvotes = data.user_mention_view.counts.upvotes;
672 found.counts.downvotes = data.user_mention_view.counts.downvotes;
673 found.counts.score = data.user_mention_view.counts.score;
675 // If youre in the unread view, just remove it from the list
677 this.state.unreadOrAll == UnreadOrAll.Unread &&
678 data.user_mention_view.user_mention.read
680 this.state.mentions = this.state.mentions.filter(
681 r => r.user_mention.id !== data.user_mention_view.user_mention.id
684 let found = this.state.mentions.find(
685 c => c.user_mention.id == data.user_mention_view.user_mention.id
687 // TODO test to make sure these mentions are getting marked as read
688 found.user_mention.read = data.user_mention_view.user_mention.read;
690 this.sendUnreadCount();
691 this.setState(this.state);
692 } else if (op == UserOperation.CreateComment) {
693 let data = wsJsonToRes<CommentResponse>(msg).data;
695 if (data.recipient_ids.includes(UserService.Instance.user.id)) {
696 this.state.replies.unshift(data.comment_view);
697 this.setState(this.state);
698 } else if (data.comment_view.creator.id == UserService.Instance.user.id) {
699 // TODO this seems wrong, you should be using form_id
700 toast(i18n.t('reply_sent'));
702 } else if (op == UserOperation.CreatePrivateMessage) {
703 let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
705 data.private_message_view.recipient.id == UserService.Instance.user.id
707 this.state.messages.unshift(data.private_message_view);
708 this.setState(this.state);
710 } else if (op == UserOperation.SaveComment) {
711 let data = wsJsonToRes<CommentResponse>(msg).data;
712 saveCommentRes(data.comment_view, this.state.replies);
713 this.setState(this.state);
715 } else if (op == UserOperation.CreateCommentLike) {
716 let data = wsJsonToRes<CommentResponse>(msg).data;
717 createCommentLikeRes(data.comment_view, this.state.replies);
718 this.setState(this.state);
723 UserService.Instance.unreadCountSub.next(this.unreadCount());
726 unreadCount(): number {
728 this.state.replies.filter(r => !r.comment.read).length +
729 this.state.mentions.filter(r => !r.user_mention.read).length +
730 this.state.messages.filter(
732 UserService.Instance.user &&
733 !r.private_message.read &&
734 // TODO also seems very strang and wrong
735 r.creator.id !== UserService.Instance.user.id