lemmy-ui/src/shared/components/inbox.tsx

746 lines
22 KiB
TypeScript
Raw Normal View History

import { Component, linkEvent } from 'inferno';
import { Subscription } from 'rxjs';
import {
UserOperation,
2020-12-24 01:58:27 +00:00
CommentView,
SortType,
2020-12-24 01:58:27 +00:00
GetReplies,
GetRepliesResponse,
2020-12-24 01:58:27 +00:00
GetUserMentions,
GetUserMentionsResponse,
UserMentionResponse,
CommentResponse,
2020-12-24 01:58:27 +00:00
PrivateMessageView,
GetPrivateMessages,
PrivateMessagesResponse,
PrivateMessageResponse,
2020-12-24 01:58:27 +00:00
SiteView,
UserMentionView,
} from 'lemmy-js-client';
import { WebSocketService, UserService } from '../services';
import {
wsJsonToRes,
fetchLimit,
toast,
editCommentRes,
saveCommentRes,
createCommentLikeRes,
commentsToFlatNodes,
setupTippy,
2020-09-09 00:48:17 +00:00
setIsoData,
wsSubscribe,
isBrowser,
2020-12-24 01:58:27 +00:00
wsUserOp,
wsClient,
authField,
} from '../utils';
import { CommentNodes } from './comment-nodes';
import { PrivateMessage } from './private-message';
2020-09-11 18:09:21 +00:00
import { HtmlTags } from './html-tags';
import { SortSelect } from './sort-select';
import { i18n } from '../i18next';
import { InitialFetchRequest } from 'shared/interfaces';
enum UnreadOrAll {
Unread,
All,
}
enum MessageType {
All,
Replies,
Mentions,
Messages,
}
2020-12-24 01:58:27 +00:00
enum ReplyEnum {
Reply,
Mention,
Message,
}
type ReplyType = {
id: number;
type_: ReplyEnum;
view: CommentView | PrivateMessageView | UserMentionView;
published: string;
};
interface InboxState {
unreadOrAll: UnreadOrAll;
messageType: MessageType;
2020-12-24 01:58:27 +00:00
replies: CommentView[];
mentions: UserMentionView[];
messages: PrivateMessageView[];
sort: SortType;
page: number;
2020-12-24 01:58:27 +00:00
site_view: SiteView;
2020-09-09 00:48:17 +00:00
loading: boolean;
}
export class Inbox extends Component<any, InboxState> {
2020-09-09 00:48:17 +00:00
private isoData = setIsoData(this.context);
private subscription: Subscription;
private emptyState: InboxState = {
unreadOrAll: UnreadOrAll.Unread,
messageType: MessageType.All,
replies: [],
mentions: [],
messages: [],
sort: SortType.New,
page: 1,
2020-12-24 01:58:27 +00:00
site_view: this.isoData.site_res.site_view,
2020-09-09 00:48:17 +00:00
loading: true,
};
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
this.handleSortChange = this.handleSortChange.bind(this);
2020-12-15 23:43:35 +00:00
if (!UserService.Instance.user && isBrowser()) {
toast(i18n.t('not_logged_in'), 'danger');
this.context.router.history.push(`/login`);
}
2020-09-09 00:48:17 +00:00
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
2020-09-09 00:48:17 +00:00
// Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) {
this.state.replies = this.isoData.routeData[0].replies;
this.state.mentions = this.isoData.routeData[1].mentions;
this.state.messages = this.isoData.routeData[2].messages;
this.sendUnreadCount();
this.state.loading = false;
} else {
this.refetch();
}
}
componentWillUnmount() {
2020-09-09 00:48:17 +00:00
if (isBrowser()) {
this.subscription.unsubscribe();
}
}
get documentTitle(): string {
2020-09-09 00:48:17 +00:00
return `@${UserService.Instance.user.name} ${i18n.t('inbox')} - ${
2020-12-24 01:58:27 +00:00
this.state.site_view.site.name
2020-09-09 00:48:17 +00:00
}`;
}
render() {
return (
<div class="container">
2020-09-09 00:48:17 +00:00
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
</h5>
) : (
<div class="row">
<div class="col-12">
2020-09-11 18:09:21 +00:00
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
2020-09-09 00:48:17 +00:00
<h5 class="mb-1">
{i18n.t('inbox')}
<small>
<a
href={`/feeds/inbox/${UserService.Instance.auth}.xml`}
target="_blank"
title="RSS"
rel="noopener"
>
<svg class="icon ml-2 text-muted small">
<use xlinkHref="#icon-rss">#</use>
</svg>
</a>
</small>
</h5>
{this.state.replies.length +
this.state.mentions.length +
this.state.messages.length >
0 &&
this.state.unreadOrAll == UnreadOrAll.Unread && (
<ul class="list-inline mb-1 text-muted small font-weight-bold">
<li className="list-inline-item">
<span
class="pointer"
onClick={linkEvent(this, this.markAllAsRead)}
>
{i18n.t('mark_all_as_read')}
</span>
</li>
</ul>
)}
{this.selects()}
{this.state.messageType == MessageType.All && this.all()}
{this.state.messageType == MessageType.Replies && this.replies()}
{this.state.messageType == MessageType.Mentions &&
this.mentions()}
{this.state.messageType == MessageType.Messages &&
this.messages()}
{this.paginator()}
</div>
</div>
2020-09-09 00:48:17 +00:00
)}
</div>
);
}
unreadOrAllRadios() {
return (
<div class="btn-group btn-group-toggle flex-wrap mb-2">
<label
className={`btn btn-outline-secondary pointer
${this.state.unreadOrAll == UnreadOrAll.Unread && 'active'}
`}
>
<input
type="radio"
value={UnreadOrAll.Unread}
checked={this.state.unreadOrAll == UnreadOrAll.Unread}
onChange={linkEvent(this, this.handleUnreadOrAllChange)}
/>
{i18n.t('unread')}
</label>
<label
className={`btn btn-outline-secondary pointer
${this.state.unreadOrAll == UnreadOrAll.All && 'active'}
`}
>
<input
type="radio"
value={UnreadOrAll.All}
checked={this.state.unreadOrAll == UnreadOrAll.All}
onChange={linkEvent(this, this.handleUnreadOrAllChange)}
/>
{i18n.t('all')}
</label>
</div>
);
}
messageTypeRadios() {
return (
<div class="btn-group btn-group-toggle flex-wrap mb-2">
<label
className={`btn btn-outline-secondary pointer
${this.state.messageType == MessageType.All && 'active'}
`}
>
<input
type="radio"
value={MessageType.All}
checked={this.state.messageType == MessageType.All}
onChange={linkEvent(this, this.handleMessageTypeChange)}
/>
{i18n.t('all')}
</label>
<label
className={`btn btn-outline-secondary pointer
${this.state.messageType == MessageType.Replies && 'active'}
`}
>
<input
type="radio"
value={MessageType.Replies}
checked={this.state.messageType == MessageType.Replies}
onChange={linkEvent(this, this.handleMessageTypeChange)}
/>
{i18n.t('replies')}
</label>
<label
className={`btn btn-outline-secondary pointer
${this.state.messageType == MessageType.Mentions && 'active'}
`}
>
<input
type="radio"
value={MessageType.Mentions}
checked={this.state.messageType == MessageType.Mentions}
onChange={linkEvent(this, this.handleMessageTypeChange)}
/>
{i18n.t('mentions')}
</label>
<label
className={`btn btn-outline-secondary pointer
${this.state.messageType == MessageType.Messages && 'active'}
`}
>
<input
type="radio"
value={MessageType.Messages}
checked={this.state.messageType == MessageType.Messages}
onChange={linkEvent(this, this.handleMessageTypeChange)}
/>
{i18n.t('messages')}
</label>
</div>
);
}
selects() {
return (
<div className="mb-2">
<span class="mr-3">{this.unreadOrAllRadios()}</span>
<span class="mr-3">{this.messageTypeRadios()}</span>
<SortSelect
sort={this.state.sort}
onChange={this.handleSortChange}
hideHot
/>
</div>
);
}
combined(): ReplyType[] {
2020-12-24 01:58:27 +00:00
let id = 0;
let replies: ReplyType[] = this.state.replies.map(r => ({
id: id++,
type_: ReplyEnum.Reply,
view: r,
published: r.comment.published,
}));
let mentions: ReplyType[] = this.state.mentions.map(r => ({
id: id++,
type_: ReplyEnum.Mention,
view: r,
published: r.comment.published,
}));
let messages: ReplyType[] = this.state.messages.map(r => ({
id: id++,
type_: ReplyEnum.Message,
view: r,
published: r.private_message.published,
}));
return [...replies, ...mentions, ...messages].sort((a, b) =>
b.published.localeCompare(a.published)
);
}
renderReplyType(i: ReplyType) {
switch (i.type_) {
case ReplyEnum.Reply:
return (
<CommentNodes
key={i.id}
nodes={[{ comment_view: i.view as CommentView }]}
noIndent
markable
showCommunity
showContext
enableDownvotes={this.state.site_view.site.enable_downvotes}
/>
);
case ReplyEnum.Mention:
return (
<CommentNodes
key={i.id}
nodes={[{ comment_view: i.view as UserMentionView }]}
noIndent
markable
showCommunity
showContext
enableDownvotes={this.state.site_view.site.enable_downvotes}
/>
);
case ReplyEnum.Message:
return (
<PrivateMessage
key={i.id}
private_message_view={i.view as PrivateMessageView}
/>
);
default:
return <div />;
}
}
all() {
2020-12-24 01:58:27 +00:00
return <div>{this.combined().map(i => this.renderReplyType(i))}</div>;
}
replies() {
return (
<div>
<CommentNodes
nodes={commentsToFlatNodes(this.state.replies)}
noIndent
markable
showCommunity
showContext
2020-12-24 01:58:27 +00:00
enableDownvotes={this.state.site_view.site.enable_downvotes}
/>
</div>
);
}
mentions() {
return (
<div>
2020-12-24 01:58:27 +00:00
{this.state.mentions.map(umv => (
<CommentNodes
2020-12-24 01:58:27 +00:00
key={umv.user_mention.id}
nodes={[{ comment_view: umv }]}
noIndent
markable
showCommunity
showContext
2020-12-24 01:58:27 +00:00
enableDownvotes={this.state.site_view.site.enable_downvotes}
/>
))}
</div>
);
}
messages() {
return (
<div>
2020-12-24 01:58:27 +00:00
{this.state.messages.map(pmv => (
<PrivateMessage
key={pmv.private_message.id}
private_message_view={pmv}
/>
))}
</div>
);
}
paginator() {
return (
<div class="mt-2">
{this.state.page > 1 && (
<button
class="btn btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
</button>
)}
{this.unreadCount() > 0 && (
<button
class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
</button>
)}
</div>
);
}
nextPage(i: Inbox) {
i.state.page++;
i.setState(i.state);
i.refetch();
}
prevPage(i: Inbox) {
i.state.page--;
i.setState(i.state);
i.refetch();
}
handleUnreadOrAllChange(i: Inbox, event: any) {
i.state.unreadOrAll = Number(event.target.value);
i.state.page = 1;
i.setState(i.state);
i.refetch();
}
handleMessageTypeChange(i: Inbox, event: any) {
i.state.messageType = Number(event.target.value);
i.state.page = 1;
i.setState(i.state);
i.refetch();
}
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
2020-09-09 00:48:17 +00:00
let promises: Promise<any>[] = [];
// It can be /u/me, or /username/1
2020-12-24 01:58:27 +00:00
let repliesForm: GetReplies = {
2020-09-09 00:48:17 +00:00
sort: SortType.New,
unread_only: true,
page: 1,
limit: fetchLimit,
2020-12-24 01:58:27 +00:00
auth: req.auth,
2020-09-09 00:48:17 +00:00
};
promises.push(req.client.getReplies(repliesForm));
2020-09-09 00:48:17 +00:00
2020-12-24 01:58:27 +00:00
let userMentionsForm: GetUserMentions = {
2020-09-09 00:48:17 +00:00
sort: SortType.New,
unread_only: true,
page: 1,
limit: fetchLimit,
2020-12-24 01:58:27 +00:00
auth: req.auth,
2020-09-09 00:48:17 +00:00
};
promises.push(req.client.getUserMentions(userMentionsForm));
2020-09-09 00:48:17 +00:00
2020-12-24 01:58:27 +00:00
let privateMessagesForm: GetPrivateMessages = {
2020-09-09 00:48:17 +00:00
unread_only: true,
page: 1,
limit: fetchLimit,
2020-12-24 01:58:27 +00:00
auth: req.auth,
2020-09-09 00:48:17 +00:00
};
promises.push(req.client.getPrivateMessages(privateMessagesForm));
2020-09-09 00:48:17 +00:00
return promises;
}
refetch() {
2020-12-24 01:58:27 +00:00
let repliesForm: GetReplies = {
sort: this.state.sort,
unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
page: this.state.page,
limit: fetchLimit,
auth: authField(),
};
WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
2020-12-24 01:58:27 +00:00
let userMentionsForm: GetUserMentions = {
sort: this.state.sort,
unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
page: this.state.page,
limit: fetchLimit,
auth: authField(),
};
WebSocketService.Instance.send(wsClient.getUserMentions(userMentionsForm));
2020-12-24 01:58:27 +00:00
let privateMessagesForm: GetPrivateMessages = {
unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
page: this.state.page,
limit: fetchLimit,
auth: authField(),
};
WebSocketService.Instance.send(
wsClient.getPrivateMessages(privateMessagesForm)
);
}
handleSortChange(val: SortType) {
this.state.sort = val;
this.state.page = 1;
this.setState(this.state);
this.refetch();
}
markAllAsRead(i: Inbox) {
WebSocketService.Instance.send(
wsClient.markAllAsRead({
auth: authField(),
})
);
i.state.replies = [];
i.state.mentions = [];
i.state.messages = [];
i.sendUnreadCount();
window.scrollTo(0, 0);
i.setState(i.state);
}
2020-12-24 01:58:27 +00:00
parseMessage(msg: any) {
let op = wsUserOp(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
return;
} else if (msg.reconnect) {
this.refetch();
2020-12-24 01:58:27 +00:00
} else if (op == UserOperation.GetReplies) {
let data = wsJsonToRes<GetRepliesResponse>(msg).data;
this.state.replies = data.replies;
2020-09-09 00:48:17 +00:00
this.state.loading = false;
this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
setupTippy();
2020-12-24 01:58:27 +00:00
} else if (op == UserOperation.GetUserMentions) {
let data = wsJsonToRes<GetUserMentionsResponse>(msg).data;
this.state.mentions = data.mentions;
this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
setupTippy();
2020-12-24 01:58:27 +00:00
} else if (op == UserOperation.GetPrivateMessages) {
let data = wsJsonToRes<PrivateMessagesResponse>(msg).data;
this.state.messages = data.private_messages;
this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
setupTippy();
2020-12-24 01:58:27 +00:00
} else if (op == UserOperation.EditPrivateMessage) {
let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
let found: PrivateMessageView = this.state.messages.find(
m =>
m.private_message.id === data.private_message_view.private_message.id
);
if (found) {
2020-12-24 01:58:27 +00:00
found.private_message.content =
data.private_message_view.private_message.content;
found.private_message.updated =
data.private_message_view.private_message.updated;
}
this.setState(this.state);
2020-12-24 01:58:27 +00:00
} else if (op == UserOperation.DeletePrivateMessage) {
let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
let found: PrivateMessageView = this.state.messages.find(
m =>
m.private_message.id === data.private_message_view.private_message.id
);
if (found) {
2020-12-24 01:58:27 +00:00
found.private_message.deleted =
data.private_message_view.private_message.deleted;
found.private_message.updated =
data.private_message_view.private_message.updated;
}
this.setState(this.state);
2020-12-24 01:58:27 +00:00
} else if (op == UserOperation.MarkPrivateMessageAsRead) {
let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
let found: PrivateMessageView = this.state.messages.find(
m =>
m.private_message.id === data.private_message_view.private_message.id
);
if (found) {
2020-12-24 01:58:27 +00:00
found.private_message.updated =
data.private_message_view.private_message.updated;
// If youre in the unread view, just remove it from the list
2020-12-24 01:58:27 +00:00
if (
this.state.unreadOrAll == UnreadOrAll.Unread &&
data.private_message_view.private_message.read
) {
this.state.messages = this.state.messages.filter(
2020-12-24 01:58:27 +00:00
r =>
r.private_message.id !==
data.private_message_view.private_message.id
);
} else {
2020-12-24 01:58:27 +00:00
let found = this.state.messages.find(
c =>
c.private_message.id ==
data.private_message_view.private_message.id
);
found.private_message.read =
data.private_message_view.private_message.read;
}
}
this.sendUnreadCount();
this.setState(this.state);
2020-12-24 01:58:27 +00:00
} else if (op == UserOperation.MarkAllAsRead) {
// Moved to be instant
} else if (
2020-12-24 01:58:27 +00:00
op == UserOperation.EditComment ||
op == UserOperation.DeleteComment ||
op == UserOperation.RemoveComment
) {
2020-12-24 01:58:27 +00:00
let data = wsJsonToRes<CommentResponse>(msg).data;
editCommentRes(data.comment_view, this.state.replies);
this.setState(this.state);
2020-12-24 01:58:27 +00:00
} else if (op == UserOperation.MarkCommentAsRead) {
let data = wsJsonToRes<CommentResponse>(msg).data;
// If youre in the unread view, just remove it from the list
2020-12-24 01:58:27 +00:00
if (
this.state.unreadOrAll == UnreadOrAll.Unread &&
data.comment_view.comment.read
) {
this.state.replies = this.state.replies.filter(
2020-12-24 01:58:27 +00:00
r => r.comment.id !== data.comment_view.comment.id
);
} else {
2020-12-24 01:58:27 +00:00
let found = this.state.replies.find(
c => c.comment.id == data.comment_view.comment.id
);
found.comment.read = data.comment_view.comment.read;
}
this.sendUnreadCount();
this.setState(this.state);
setupTippy();
2020-12-24 01:58:27 +00:00
} else if (op == UserOperation.MarkUserMentionAsRead) {
let data = wsJsonToRes<UserMentionResponse>(msg).data;
// TODO this might not be correct, it might need to use the comment id
let found = this.state.mentions.find(
c => c.user_mention.id == data.user_mention_view.user_mention.id
);
found.comment.content = data.user_mention_view.comment.content;
found.comment.updated = data.user_mention_view.comment.updated;
found.comment.removed = data.user_mention_view.comment.removed;
found.comment.deleted = data.user_mention_view.comment.deleted;
found.counts.upvotes = data.user_mention_view.counts.upvotes;
found.counts.downvotes = data.user_mention_view.counts.downvotes;
found.counts.score = data.user_mention_view.counts.score;
// If youre in the unread view, just remove it from the list
2020-12-24 01:58:27 +00:00
if (
this.state.unreadOrAll == UnreadOrAll.Unread &&
data.user_mention_view.user_mention.read
) {
this.state.mentions = this.state.mentions.filter(
2020-12-24 01:58:27 +00:00
r => r.user_mention.id !== data.user_mention_view.user_mention.id
);
} else {
2020-12-24 01:58:27 +00:00
let found = this.state.mentions.find(
c => c.user_mention.id == data.user_mention_view.user_mention.id
);
// TODO test to make sure these mentions are getting marked as read
found.user_mention.read = data.user_mention_view.user_mention.read;
}
this.sendUnreadCount();
this.setState(this.state);
2020-12-24 01:58:27 +00:00
} else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg).data;
if (data.recipient_ids.includes(UserService.Instance.user.id)) {
2020-12-24 01:58:27 +00:00
this.state.replies.unshift(data.comment_view);
this.setState(this.state);
2020-12-24 01:58:27 +00:00
} else if (data.comment_view.creator.id == UserService.Instance.user.id) {
// TODO this seems wrong, you should be using form_id
toast(i18n.t('reply_sent'));
}
2020-12-24 01:58:27 +00:00
} else if (op == UserOperation.CreatePrivateMessage) {
let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
if (
data.private_message_view.recipient.id == UserService.Instance.user.id
) {
this.state.messages.unshift(data.private_message_view);
this.setState(this.state);
}
2020-12-24 01:58:27 +00:00
} else if (op == UserOperation.SaveComment) {
let data = wsJsonToRes<CommentResponse>(msg).data;
saveCommentRes(data.comment_view, this.state.replies);
this.setState(this.state);
setupTippy();
2020-12-24 01:58:27 +00:00
} else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg).data;
createCommentLikeRes(data.comment_view, this.state.replies);
this.setState(this.state);
}
}
sendUnreadCount() {
UserService.Instance.unreadCountSub.next(this.unreadCount());
}
unreadCount(): number {
return (
2020-12-24 01:58:27 +00:00
this.state.replies.filter(r => !r.comment.read).length +
this.state.mentions.filter(r => !r.user_mention.read).length +
this.state.messages.filter(
r =>
UserService.Instance.user &&
2020-12-24 01:58:27 +00:00
!r.private_message.read &&
// TODO also seems very strang and wrong
r.creator.id !== UserService.Instance.user.id
).length
);
}
}