Add post, inbox, and user routes.

This commit is contained in:
Dessalines 2020-09-08 19:48:17 -05:00
parent bfce461a14
commit 95b74ad74c
10 changed files with 509 additions and 512 deletions

View file

@ -202,7 +202,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}}
/>
<span class="mx-2"></span>
<Link class="mr-2" to={`/post/${node.comment.post_id}`}>
<Link className="mr-2" to={`/post/${node.comment.post_id}`}>
{node.comment.post_name}
</Link>
</>
@ -343,7 +343,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{!this.myComment && (
<button class="btn btn-link btn-animate">
<Link
class="text-muted"
className="text-muted"
to={`/create_private_message/recipient/${node.comment.creator_id}`}
title={i18n.t('message').toLowerCase()}
>
@ -757,7 +757,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
let node = this.props.node;
return (
<Link
class="btn btn-link btn-animate text-muted"
className="btn btn-link btn-animate text-muted"
to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
title={this.props.showContext ? i18n.t('show_context') : i18n.t('link')}
>

View file

@ -1,7 +1,6 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
UserOperation,
Comment,
@ -17,7 +16,6 @@ import {
GetPrivateMessagesForm,
PrivateMessagesResponse,
PrivateMessageResponse,
GetSiteResponse,
Site,
} from 'lemmy-js-client';
import { WebSocketService, UserService } from '../services';
@ -31,6 +29,11 @@ import {
createCommentLikeRes,
commentsToFlatNodes,
setupTippy,
setIsoData,
wsSubscribe,
lemmyHttp,
setAuth,
isBrowser,
} from '../utils';
import { CommentNodes } from './comment-nodes';
import { PrivateMessage } from './private-message';
@ -60,9 +63,11 @@ interface InboxState {
sort: SortType;
page: number;
site: Site;
loading: boolean;
}
export class Inbox extends Component<any, InboxState> {
private isoData = setIsoData(this.context);
private subscription: Subscription;
private emptyState: InboxState = {
unreadOrAll: UnreadOrAll.Unread,
@ -72,20 +77,8 @@ export class Inbox extends Component<any, InboxState> {
messages: [],
sort: SortType.New,
page: 1,
site: {
id: undefined,
name: undefined,
creator_id: undefined,
published: undefined,
creator_name: undefined,
number_of_users: undefined,
number_of_posts: undefined,
number_of_comments: undefined,
number_of_communities: undefined,
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
},
site: this.isoData.site.site,
loading: true,
};
constructor(props: any, context: any) {
@ -94,36 +87,44 @@ export class Inbox extends Component<any, InboxState> {
this.state = this.emptyState;
this.handleSortChange = this.handleSortChange.bind(this);
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
// 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();
WebSocketService.Instance.getSite();
}
}
componentWillUnmount() {
if (isBrowser()) {
this.subscription.unsubscribe();
}
}
get documentTitle(): string {
if (this.state.site.name) {
return `@${UserService.Instance.user.name} ${i18n.t('inbox')} - ${
this.state.site.name
}`;
} else {
return 'Lemmy';
}
}
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
</h5>
) : (
<div class="row">
<div class="col-12">
<h5 class="mb-1">
@ -160,11 +161,14 @@ export class Inbox extends Component<any, InboxState> {
{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.state.messageType == MessageType.Mentions &&
this.mentions()}
{this.state.messageType == MessageType.Messages &&
this.messages()}
{this.paginator()}
</div>
</div>
)}
</div>
);
}
@ -397,6 +401,39 @@ export class Inbox extends Component<any, InboxState> {
i.refetch();
}
static fetchInitialData(auth: string, _path: string): Promise<any>[] {
let promises: Promise<any>[] = [];
// It can be /u/me, or /username/1
let repliesForm: GetRepliesForm = {
sort: SortType.New,
unread_only: true,
page: 1,
limit: fetchLimit,
};
setAuth(repliesForm, auth);
promises.push(lemmyHttp.getReplies(repliesForm));
let userMentionsForm: GetUserMentionsForm = {
sort: SortType.New,
unread_only: true,
page: 1,
limit: fetchLimit,
};
setAuth(userMentionsForm, auth);
promises.push(lemmyHttp.getUserMentions(userMentionsForm));
let privateMessagesForm: GetPrivateMessagesForm = {
unread_only: true,
page: 1,
limit: fetchLimit,
};
setAuth(privateMessagesForm, auth);
promises.push(lemmyHttp.getPrivateMessages(privateMessagesForm));
return promises;
}
refetch() {
let repliesForm: GetRepliesForm = {
sort: this.state.sort,
@ -450,6 +487,7 @@ export class Inbox extends Component<any, InboxState> {
} else if (res.op == UserOperation.GetReplies) {
let data = res.data as GetRepliesResponse;
this.state.replies = data.replies;
this.state.loading = false;
this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
@ -581,10 +619,6 @@ export class Inbox extends Component<any, InboxState> {
let data = res.data as CommentResponse;
createCommentLikeRes(data, this.state.replies);
this.setState(this.state);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.site = data.site;
this.setState(this.state);
}
}

View file

@ -115,6 +115,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
}
}
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
}

View file

@ -17,6 +17,7 @@ import {
AddAdminForm,
TransferSiteForm,
TransferCommunityForm,
Community,
} from 'lemmy-js-client';
import { BanType } from '../interfaces';
import { MomentTime } from './moment-time';
@ -61,6 +62,7 @@ interface PostListingState {
interface PostListingProps {
post: Post;
communities: Community[];
showCommunity?: boolean;
showBody?: boolean;
moderators?: CommunityUser[];
@ -127,6 +129,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
onCancel={this.handleEditCancel}
enableNsfw={this.props.enableNsfw}
enableDownvotes={this.props.enableDownvotes}
communities={this.props.communities}
/>
</div>
)}
@ -184,6 +187,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
}
} else if (post.thumbnail_url) {
return pictrsImage(post.thumbnail_url, thumbnail);
} else {
return null;
}
}
@ -598,7 +603,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</li>
<li className="list-inline-item">
<Link
class="btn btn-link btn-animate text-muted"
className="btn btn-link btn-animate text-muted"
to={`/create_post${this.crossPostParams}`}
title={i18n.t('cross_post')}
>

View file

@ -1,17 +1,13 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
UserOperation,
Community,
Post as PostI,
GetPostResponse,
PostResponse,
Comment,
MarkCommentAsReadForm,
CommentResponse,
CommunityUser,
CommunityResponse,
CommentNode as CommentNodeI,
BanFromCommunityResponse,
@ -26,6 +22,8 @@ import {
GetSiteResponse,
GetCommunityResponse,
WebSocketJsonResponse,
ListCategoriesResponse,
Category,
} from 'lemmy-js-client';
import { CommentSortType, CommentViewType } from '../interfaces';
import { WebSocketService, UserService } from '../services';
@ -39,6 +37,13 @@ import {
commentsToFlatNodes,
setupTippy,
favIconUrl,
setIsoData,
getIdFromProps,
getCommentIdFromProps,
wsSubscribe,
setAuth,
lemmyHttp,
isBrowser,
} from '../utils';
import { PostListing } from './post-listing';
import { Sidebar } from './sidebar';
@ -48,56 +53,32 @@ import autosize from 'autosize';
import { i18n } from '../i18next';
interface PostState {
post: PostI;
comments: Comment[];
postRes: GetPostResponse;
postId: number;
commentId?: number;
commentSort: CommentSortType;
commentViewType: CommentViewType;
community: Community;
moderators: CommunityUser[];
online: number;
scrolled?: boolean;
scrolled_comment_id?: number;
loading: boolean;
crossPosts: PostI[];
siteRes: GetSiteResponse;
categories: Category[];
}
export class Post extends Component<any, PostState> {
private subscription: Subscription;
private isoData = setIsoData(this.context);
private emptyState: PostState = {
post: null,
comments: [],
postRes: null,
postId: getIdFromProps(this.props),
commentId: getCommentIdFromProps(this.props),
commentSort: CommentSortType.Hot,
commentViewType: CommentViewType.Tree,
community: null,
moderators: [],
online: null,
scrolled: false,
loading: true,
crossPosts: [],
siteRes: {
admins: [],
banned: [],
site: {
id: undefined,
name: undefined,
creator_id: undefined,
published: undefined,
creator_name: undefined,
number_of_users: undefined,
number_of_posts: undefined,
number_of_comments: undefined,
number_of_communities: undefined,
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
icon: undefined,
banner: undefined,
},
online: null,
version: null,
federated_instances: undefined,
},
siteRes: this.isoData.site,
categories: [],
};
constructor(props: any, context: any) {
@ -105,24 +86,46 @@ export class Post extends Component<any, PostState> {
this.state = this.emptyState;
let postId = Number(this.props.match.params.id);
if (this.props.match.params.comment_id) {
this.state.scrolled_comment_id = this.props.match.params.comment_id;
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
// Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) {
this.state.postRes = this.isoData.routeData[0];
this.state.categories = this.isoData.routeData[1].categories;
this.state.loading = false;
if (isBrowser() && this.state.commentId) {
this.scrollCommentIntoView();
}
} else {
this.fetchPost();
WebSocketService.Instance.listCategories();
}
}
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
fetchPost() {
let form: GetPostForm = {
id: postId,
id: this.state.postId,
};
WebSocketService.Instance.getPost(form);
WebSocketService.Instance.getSite();
}
static fetchInitialData(auth: string, path: string): Promise<any>[] {
let pathSplit = path.split('/');
let promises: Promise<any>[] = [];
let id = Number(pathSplit[2]);
let postForm: GetPostForm = {
id,
};
setAuth(postForm, auth);
promises.push(lemmyHttp.getPost(postForm));
promises.push(lemmyHttp.listCategories());
return promises;
}
componentWillUnmount() {
@ -135,17 +138,12 @@ export class Post extends Component<any, PostState> {
componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
if (
this.state.scrolled_comment_id &&
this.state.commentId &&
!this.state.scrolled &&
lastState.comments.length > 0
lastState.postRes &&
lastState.postRes.comments.length > 0
) {
var elmnt = document.getElementById(
`comment-${this.state.scrolled_comment_id}`
);
elmnt.scrollIntoView();
elmnt.classList.add('mark');
this.state.scrolled = true;
this.markScrolledAsRead(this.state.scrolled_comment_id);
this.scrollCommentIntoView();
}
// Necessary if you are on a post and you click another post (same route)
@ -161,12 +159,20 @@ export class Post extends Component<any, PostState> {
}
}
scrollCommentIntoView() {
var elmnt = document.getElementById(`comment-${this.state.commentId}`);
elmnt.scrollIntoView();
elmnt.classList.add('mark');
this.state.scrolled = true;
this.markScrolledAsRead(this.state.commentId);
}
markScrolledAsRead(commentId: number) {
let found = this.state.comments.find(c => c.id == commentId);
let parent = this.state.comments.find(c => found.parent_id == c.id);
let found = this.state.postRes.comments.find(c => c.id == commentId);
let parent = this.state.postRes.comments.find(c => found.parent_id == c.id);
let parent_user_id = parent
? parent.creator_id
: this.state.post.creator_id;
: this.state.postRes.post.creator_id;
if (
UserService.Instance.user &&
@ -185,8 +191,8 @@ export class Post extends Component<any, PostState> {
}
get documentTitle(): string {
if (this.state.post) {
return `${this.state.post.name} - ${this.state.siteRes.site.name}`;
if (this.state.postRes) {
return `${this.state.postRes.post.name} - ${this.state.siteRes.site.name}`;
} else {
return 'Lemmy';
}
@ -219,20 +225,21 @@ export class Post extends Component<any, PostState> {
<div class="row">
<div class="col-12 col-md-8 mb-3">
<PostListing
post={this.state.post}
communities={[this.state.postRes.community]}
post={this.state.postRes.post}
showBody
showCommunity
moderators={this.state.moderators}
moderators={this.state.postRes.moderators}
admins={this.state.siteRes.admins}
enableDownvotes={this.state.siteRes.site.enable_downvotes}
enableNsfw={this.state.siteRes.site.enable_nsfw}
/>
<div className="mb-2" />
<CommentForm
postId={this.state.post.id}
disabled={this.state.post.locked}
postId={this.state.postId}
disabled={this.state.postRes.post.locked}
/>
{this.state.comments.length > 0 && this.sortRadios()}
{this.state.postRes.comments.length > 0 && this.sortRadios()}
{this.state.commentViewType == CommentViewType.Tree &&
this.commentsTree()}
{this.state.commentViewType == CommentViewType.Chat &&
@ -325,12 +332,12 @@ export class Post extends Component<any, PostState> {
return (
<div>
<CommentNodes
nodes={commentsToFlatNodes(this.state.comments)}
nodes={commentsToFlatNodes(this.state.postRes.comments)}
noIndent
locked={this.state.post.locked}
moderators={this.state.moderators}
locked={this.state.postRes.post.locked}
moderators={this.state.postRes.moderators}
admins={this.state.siteRes.admins}
postCreatorId={this.state.post.creator_id}
postCreatorId={this.state.postRes.post.creator_id}
showContext
enableDownvotes={this.state.siteRes.site.enable_downvotes}
sort={this.state.commentSort}
@ -343,12 +350,13 @@ export class Post extends Component<any, PostState> {
return (
<div class="mb-3">
<Sidebar
community={this.state.community}
moderators={this.state.moderators}
community={this.state.postRes.community}
moderators={this.state.postRes.moderators}
admins={this.state.siteRes.admins}
online={this.state.online}
online={this.state.postRes.online}
enableNsfw={this.state.siteRes.site.enable_nsfw}
showIcon
categories={this.state.categories}
/>
</div>
);
@ -368,7 +376,7 @@ export class Post extends Component<any, PostState> {
buildCommentsTree(): CommentNodeI[] {
let map = new Map<number, CommentNodeI>();
for (let comment of this.state.comments) {
for (let comment of this.state.postRes.comments) {
let node: CommentNodeI = {
comment: comment,
children: [],
@ -376,7 +384,7 @@ export class Post extends Component<any, PostState> {
map.set(comment.id, { ...node });
}
let tree: CommentNodeI[] = [];
for (let comment of this.state.comments) {
for (let comment of this.state.postRes.comments) {
let child = map.get(comment.id);
if (comment.parent_id) {
let parent_ = map.get(comment.parent_id);
@ -404,10 +412,10 @@ export class Post extends Component<any, PostState> {
<div>
<CommentNodes
nodes={nodes}
locked={this.state.post.locked}
moderators={this.state.moderators}
locked={this.state.postRes.post.locked}
moderators={this.state.postRes.moderators}
admins={this.state.siteRes.admins}
postCreatorId={this.state.post.creator_id}
postCreatorId={this.state.postRes.post.creator_id}
sort={this.state.commentSort}
enableDownvotes={this.state.siteRes.site.enable_downvotes}
/>
@ -427,17 +435,13 @@ export class Post extends Component<any, PostState> {
});
} else if (res.op == UserOperation.GetPost) {
let data = res.data as GetPostResponse;
this.state.post = data.post;
this.state.comments = data.comments;
this.state.community = data.community;
this.state.moderators = data.moderators;
this.state.online = data.online;
this.state.postRes = data;
this.state.loading = false;
// Get cross-posts
if (this.state.post.url) {
if (this.state.postRes.post.url) {
let form: SearchForm = {
q: this.state.post.url,
q: this.state.postRes.post.url,
type_: SearchType.Url,
sort: SortType.TopAll,
page: 1,
@ -453,7 +457,7 @@ export class Post extends Component<any, PostState> {
// Necessary since it might be a user reply
if (data.recipient_ids.length == 0) {
this.state.comments.unshift(data.comment);
this.state.postRes.comments.unshift(data.comment);
this.setState(this.state);
}
} else if (
@ -462,20 +466,20 @@ export class Post extends Component<any, PostState> {
res.op == UserOperation.RemoveComment
) {
let data = res.data as CommentResponse;
editCommentRes(data, this.state.comments);
editCommentRes(data, this.state.postRes.comments);
this.setState(this.state);
} else if (res.op == UserOperation.SaveComment) {
let data = res.data as CommentResponse;
saveCommentRes(data, this.state.comments);
saveCommentRes(data, this.state.postRes.comments);
this.setState(this.state);
setupTippy();
} else if (res.op == UserOperation.CreateCommentLike) {
let data = res.data as CommentResponse;
createCommentLikeRes(data, this.state.comments);
createCommentLikeRes(data, this.state.postRes.comments);
this.setState(this.state);
} else if (res.op == UserOperation.CreatePostLike) {
let data = res.data as PostResponse;
createPostLikeRes(data, this.state.post);
createPostLikeRes(data, this.state.postRes.post);
this.setState(this.state);
} else if (
res.op == UserOperation.EditPost ||
@ -485,12 +489,12 @@ export class Post extends Component<any, PostState> {
res.op == UserOperation.StickyPost
) {
let data = res.data as PostResponse;
this.state.post = data.post;
this.state.postRes.post = data.post;
this.setState(this.state);
setupTippy();
} else if (res.op == UserOperation.SavePost) {
let data = res.data as PostResponse;
this.state.post = data.post;
this.state.postRes.post = data.post;
this.setState(this.state);
setupTippy();
} else if (
@ -499,36 +503,36 @@ export class Post extends Component<any, PostState> {
res.op == UserOperation.RemoveCommunity
) {
let data = res.data as CommunityResponse;
this.state.community = data.community;
this.state.post.community_id = data.community.id;
this.state.post.community_name = data.community.name;
this.state.postRes.community = data.community;
this.state.postRes.post.community_id = data.community.id;
this.state.postRes.post.community_name = data.community.name;
this.setState(this.state);
} else if (res.op == UserOperation.FollowCommunity) {
let data = res.data as CommunityResponse;
this.state.community.subscribed = data.community.subscribed;
this.state.community.number_of_subscribers =
this.state.postRes.community.subscribed = data.community.subscribed;
this.state.postRes.community.number_of_subscribers =
data.community.number_of_subscribers;
this.setState(this.state);
} else if (res.op == UserOperation.BanFromCommunity) {
let data = res.data as BanFromCommunityResponse;
this.state.comments
this.state.postRes.comments
.filter(c => c.creator_id == data.user.id)
.forEach(c => (c.banned_from_community = data.banned));
if (this.state.post.creator_id == data.user.id) {
this.state.post.banned_from_community = data.banned;
if (this.state.postRes.post.creator_id == data.user.id) {
this.state.postRes.post.banned_from_community = data.banned;
}
this.setState(this.state);
} else if (res.op == UserOperation.AddModToCommunity) {
let data = res.data as AddModToCommunityResponse;
this.state.moderators = data.moderators;
this.state.postRes.moderators = data.moderators;
this.setState(this.state);
} else if (res.op == UserOperation.BanUser) {
let data = res.data as BanUserResponse;
this.state.comments
this.state.postRes.comments
.filter(c => c.creator_id == data.user.id)
.forEach(c => (c.banned = data.banned));
if (this.state.post.creator_id == data.user.id) {
this.state.post.banned = data.banned;
if (this.state.postRes.post.creator_id == data.user.id) {
this.state.postRes.post.banned = data.banned;
}
this.setState(this.state);
} else if (res.op == UserOperation.AddAdmin) {
@ -541,20 +545,21 @@ export class Post extends Component<any, PostState> {
p => p.id != Number(this.props.match.params.id)
);
if (this.state.crossPosts.length) {
this.state.post.duplicates = this.state.crossPosts;
this.state.postRes.post.duplicates = this.state.crossPosts;
}
this.setState(this.state);
} else if (
res.op == UserOperation.TransferSite ||
res.op == UserOperation.GetSite
) {
} else if (res.op == UserOperation.TransferSite) {
let data = res.data as GetSiteResponse;
this.state.siteRes = data;
this.setState(this.state);
} else if (res.op == UserOperation.TransferCommunity) {
let data = res.data as GetCommunityResponse;
this.state.community = data.community;
this.state.moderators = data.moderators;
this.state.postRes.community = data.community;
this.state.postRes.moderators = data.moderators;
this.setState(this.state);
} else if (res.op == UserOperation.ListCategories) {
let data = res.data as ListCategoriesResponse;
this.state.categories = data.categories;
this.setState(this.state);
}
}

View file

@ -1,38 +1,13 @@
import { Component, linkEvent } from 'inferno';
import { WebSocketService, UserService } from '../services';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { i18n } from '../i18next';
import {
UserOperation,
Post,
Comment,
CommunityUser,
SortType,
UserDetailsResponse,
UserView,
WebSocketJsonResponse,
CommentResponse,
BanUserResponse,
PostResponse,
} from 'lemmy-js-client';
import { Post, Comment, SortType, UserDetailsResponse } from 'lemmy-js-client';
import { UserDetailsView } from '../interfaces';
import {
wsJsonToRes,
toast,
commentsToFlatNodes,
setupTippy,
editCommentRes,
saveCommentRes,
createCommentLikeRes,
createPostLikeFindRes,
} from '../utils';
import { commentsToFlatNodes, setupTippy } from '../utils';
import { PostListing } from './post-listing';
import { CommentNodes } from './comment-nodes';
interface UserDetailsProps {
username?: string;
user_id?: number;
userRes: UserDetailsResponse;
page: number;
limit: number;
sort: SortType;
@ -40,67 +15,29 @@ interface UserDetailsProps {
enableNsfw: boolean;
view: UserDetailsView;
onPageChange(page: number): number | any;
admins: UserView[];
}
interface UserDetailsState {
follows: CommunityUser[];
moderates: CommunityUser[];
comments: Comment[];
posts: Post[];
saved?: Post[];
}
interface UserDetailsState {}
export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
private subscription: Subscription;
constructor(props: any, context: any) {
super(props, context);
this.state = {
follows: [],
moderates: [],
comments: [],
posts: [],
saved: [],
};
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
// TODO needed here?
componentDidMount() {
this.fetchUserData();
setupTippy();
}
componentDidUpdate(lastProps: UserDetailsProps) {
for (const key of Object.keys(lastProps)) {
if (lastProps[key] !== this.props[key]) {
this.fetchUserData();
break;
}
}
}
fetchUserData() {
WebSocketService.Instance.getUserDetails({
user_id: this.props.user_id,
username: this.props.username,
sort: this.props.sort,
saved_only: this.props.view === UserDetailsView.Saved,
page: this.props.page,
limit: this.props.limit,
});
}
// TODO wut?
// componentDidUpdate(lastProps: UserDetailsProps) {
// for (const key of Object.keys(lastProps)) {
// if (lastProps[key] !== this.props[key]) {
// this.fetchUserData();
// break;
// }
// }
// }
render() {
return (
@ -114,20 +51,20 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
viewSelector(view: UserDetailsView) {
if (view === UserDetailsView.Overview || view === UserDetailsView.Saved) {
return this.overview();
}
if (view === UserDetailsView.Comments) {
} else if (view === UserDetailsView.Comments) {
return this.comments();
}
if (view === UserDetailsView.Posts) {
} else if (view === UserDetailsView.Posts) {
return this.posts();
} else {
return null;
}
}
overview() {
const comments = this.state.comments.map((c: Comment) => {
const comments = this.props.userRes.comments.map((c: Comment) => {
return { type: 'comments', data: c };
});
const posts = this.state.posts.map((p: Post) => {
const posts = this.props.userRes.posts.map((p: Post) => {
return { type: 'posts', data: p };
});
@ -150,9 +87,10 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
<div>
{i.type === 'posts' ? (
<PostListing
communities={[]}
key={(i.data as Post).id}
post={i.data as Post}
admins={this.props.admins}
admins={this.props.userRes.admins}
showCommunity
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
@ -161,7 +99,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
<CommentNodes
key={(i.data as Comment).id}
nodes={[{ comment: i.data as Comment }]}
admins={this.props.admins}
admins={this.props.userRes.admins}
noBorder
noIndent
showCommunity
@ -181,8 +119,8 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
return (
<div>
<CommentNodes
nodes={commentsToFlatNodes(this.state.comments)}
admins={this.props.admins}
nodes={commentsToFlatNodes(this.props.userRes.comments)}
admins={this.props.userRes.admins}
noIndent
showCommunity
showContext
@ -195,11 +133,12 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
posts() {
return (
<div>
{this.state.posts.map(post => (
{this.props.userRes.posts.map(post => (
<>
<PostListing
communities={[]}
post={post}
admins={this.props.admins}
admins={this.props.userRes.admins}
showCommunity
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
@ -222,7 +161,8 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
{i18n.t('prev')}
</button>
)}
{this.state.comments.length + this.state.posts.length > 0 && (
{this.props.userRes.comments.length + this.props.userRes.posts.length >
0 && (
<button
class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
@ -241,75 +181,4 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
prevPage(i: UserDetails) {
i.props.onPageChange(i.props.page - 1);
}
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
const res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
if (msg.error == 'couldnt_find_that_username_or_email') {
this.context.router.history.push('/');
}
return;
} else if (msg.reconnect) {
this.fetchUserData();
} else if (res.op == UserOperation.GetUserDetails) {
const data = res.data as UserDetailsResponse;
this.setState({
comments: data.comments,
follows: data.follows,
moderates: data.moderates,
posts: data.posts,
});
} else if (res.op == UserOperation.CreateCommentLike) {
const data = res.data as CommentResponse;
createCommentLikeRes(data, this.state.comments);
this.setState({
comments: this.state.comments,
});
} else if (
res.op == UserOperation.EditComment ||
res.op == UserOperation.DeleteComment ||
res.op == UserOperation.RemoveComment
) {
const data = res.data as CommentResponse;
editCommentRes(data, this.state.comments);
this.setState({
comments: this.state.comments,
});
} else if (res.op == UserOperation.CreateComment) {
const data = res.data as CommentResponse;
if (
UserService.Instance.user &&
data.comment.creator_id == UserService.Instance.user.id
) {
toast(i18n.t('reply_sent'));
}
} else if (res.op == UserOperation.SaveComment) {
const data = res.data as CommentResponse;
saveCommentRes(data, this.state.comments);
this.setState({
comments: this.state.comments,
});
} else if (res.op == UserOperation.CreatePostLike) {
const data = res.data as PostResponse;
createPostLikeFindRes(data, this.state.posts);
this.setState({
posts: this.state.posts,
});
} else if (res.op == UserOperation.BanUser) {
const data = res.data as BanUserResponse;
this.state.comments
.filter(c => c.creator_id == data.user.id)
.forEach(c => (c.banned = data.banned));
this.state.posts
.filter(c => c.creator_id == data.user.id)
.forEach(c => (c.banned = data.banned));
this.setState({
posts: this.state.posts,
comments: this.state.comments,
});
}
}
}

View file

@ -2,13 +2,10 @@ import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Link } from 'inferno-router';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
UserOperation,
CommunityUser,
SortType,
ListingType,
UserView,
UserSettingsForm,
LoginResponse,
DeleteAccountForm,
@ -16,6 +13,10 @@ import {
GetSiteResponse,
UserDetailsResponse,
AddAdminResponse,
GetUserDetailsForm,
CommentResponse,
PostResponse,
BanUserResponse,
} from 'lemmy-js-client';
import { UserDetailsView } from '../interfaces';
import { WebSocketService, UserService } from '../services';
@ -33,6 +34,16 @@ import {
mdToHtml,
elementUrl,
favIconUrl,
setIsoData,
getIdFromProps,
getUsernameFromProps,
wsSubscribe,
createCommentLikeRes,
editCommentRes,
saveCommentRes,
createPostLikeFindRes,
setAuth,
lemmyHttp,
} from '../utils';
import { UserListing } from './user-listing';
import { SortSelect } from './sort-select';
@ -46,11 +57,9 @@ import { ImageUploadForm } from './image-upload-form';
import { BannerIconHeader } from './banner-icon-header';
interface UserState {
user: UserView;
user_id: number;
username: string;
follows: CommunityUser[];
moderates: CommunityUser[];
userRes: UserDetailsResponse;
userId: number;
userName: string;
view: UserDetailsView;
sort: SortType;
page: number;
@ -78,25 +87,12 @@ interface UrlParams {
}
export class User extends Component<any, UserState> {
private isoData = setIsoData(this.context);
private subscription: Subscription;
private emptyState: UserState = {
user: {
id: null,
name: null,
published: null,
number_of_posts: null,
post_score: null,
number_of_comments: null,
comment_score: null,
banned: null,
avatar: null,
actor_id: null,
local: null,
},
user_id: null,
username: null,
follows: [],
moderates: [],
userRes: undefined,
userId: getIdFromProps(this.props),
userName: getUsernameFromProps(this.props),
loading: true,
view: User.getViewFromProps(this.props.match.view),
sort: User.getSortTypeFromProps(this.props.match.sort),
@ -119,31 +115,7 @@ export class User extends Component<any, UserState> {
deleteAccountForm: {
password: null,
},
siteRes: {
admins: [],
banned: [],
online: undefined,
site: {
id: undefined,
name: undefined,
creator_id: undefined,
published: undefined,
creator_name: undefined,
number_of_users: undefined,
number_of_posts: undefined,
number_of_comments: undefined,
number_of_communities: undefined,
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
icon: undefined,
banner: undefined,
creator_preferred_username: undefined,
},
version: undefined,
my_user: undefined,
federated_instances: undefined,
},
siteRes: this.isoData.site,
};
constructor(props: any, context: any) {
@ -168,25 +140,37 @@ export class User extends Component<any, UserState> {
this.handleBannerUpload = this.handleBannerUpload.bind(this);
this.handleBannerRemove = this.handleBannerRemove.bind(this);
this.state.user_id = Number(this.props.match.params.id) || null;
this.state.username = this.props.match.params.username;
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
// Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) {
this.state.userRes = this.isoData.routeData[0];
this.setUserInfo();
this.state.loading = false;
} else {
this.fetchUserData();
}
WebSocketService.Instance.getSite();
setupTippy();
}
fetchUserData() {
let form: GetUserDetailsForm = {
user_id: this.state.userId,
username: this.state.userName,
sort: this.state.sort,
saved_only: this.state.view === UserDetailsView.Saved,
page: this.state.page,
limit: fetchLimit,
};
WebSocketService.Instance.getUserDetails(form);
}
get isCurrentUser() {
return (
UserService.Instance.user &&
UserService.Instance.user.id == this.state.user.id
UserService.Instance.user.id == this.state.userRes.user.id
);
}
@ -202,6 +186,44 @@ export class User extends Component<any, UserState> {
return page ? Number(page) : 1;
}
static fetchInitialData(auth: string, path: string): Promise<any>[] {
let pathSplit = path.split('/');
let promises: Promise<any>[] = [];
// It can be /u/me, or /username/1
let idOrName = pathSplit[2];
let user_id: number;
let username: string;
if (isNaN(Number(idOrName))) {
username = idOrName;
} else {
user_id = Number(idOrName);
}
let view = this.getViewFromProps(pathSplit[4]);
let sort = this.getSortTypeFromProps(pathSplit[6]);
let page = this.getPageFromProps(Number(pathSplit[8]));
let form: GetUserDetailsForm = {
sort,
saved_only: view === UserDetailsView.Saved,
page,
limit: fetchLimit,
};
this.setIdOrName(form, user_id, username);
setAuth(form, auth);
promises.push(lemmyHttp.getUserDetails(form));
return promises;
}
static setIdOrName(obj: any, id: number, name_: string) {
if (id) {
obj.user_id = id;
} else {
obj.username = name_;
}
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
@ -229,7 +251,7 @@ export class User extends Component<any, UserState> {
get documentTitle(): string {
if (this.state.siteRes.site.name) {
return `@${this.state.username} - ${this.state.siteRes.site.name}`;
return `@${this.state.userName} - ${this.state.siteRes.site.name}`;
} else {
return 'Lemmy';
}
@ -252,8 +274,6 @@ export class User extends Component<any, UserState> {
href={this.favIcon}
/>
</Helmet>
<div class="row">
<div class="col-12 col-md-8">
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">
@ -261,21 +281,20 @@ export class User extends Component<any, UserState> {
</svg>
</h5>
) : (
<div class="row">
<div class="col-12 col-md-8">
<>
{this.userInfo()}
<hr />
</>
)}
{!this.state.loading && this.selects()}
<UserDetails
user_id={this.state.user_id}
username={this.state.username}
userRes={this.state.userRes}
sort={this.state.sort}
page={this.state.page}
limit={fetchLimit}
enableDownvotes={this.state.siteRes.site.enable_downvotes}
enableNsfw={this.state.siteRes.site.enable_nsfw}
admins={this.state.siteRes.admins}
view={this.state.view}
onPageChange={this.handlePageChange}
/>
@ -289,6 +308,7 @@ export class User extends Component<any, UserState> {
</div>
)}
</div>
)}
</div>
);
}
@ -362,7 +382,7 @@ export class User extends Component<any, UserState> {
hideHot
/>
<a
href={`/feeds/u/${this.state.username}.xml?sort=${this.state.sort}`}
href={`/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`}
target="_blank"
rel="noopener"
title="RSS"
@ -376,14 +396,11 @@ export class User extends Component<any, UserState> {
}
userInfo() {
let user = this.state.user;
let user = this.state.userRes.user;
return (
<div>
<BannerIconHeader
banner={this.state.user.banner}
icon={this.state.user.avatar}
/>
<BannerIconHeader banner={user.banner} icon={user.avatar} />
<div class="mb-3">
<div class="">
<div class="mb-0 d-flex flex-wrap">
@ -420,17 +437,17 @@ export class User extends Component<any, UserState> {
<>
<a
className={`d-flex align-self-start btn btn-secondary ml-2 ${
!this.state.user.matrix_user_id && 'invisible'
!user.matrix_user_id && 'invisible'
}`}
target="_blank"
rel="noopener"
href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
href={`https://matrix.to/#/${user.matrix_user_id}`}
>
{i18n.t('send_secure_message')}
</a>
<Link
class="d-flex align-self-start btn btn-secondary ml-2"
to={`/create_private_message/recipient/${this.state.user.id}`}
to={`/create_private_message/recipient/${user.id}`}
>
{i18n.t('send_message')}
</Link>
@ -818,12 +835,12 @@ export class User extends Component<any, UserState> {
moderates() {
return (
<div>
{this.state.moderates.length > 0 && (
{this.state.userRes.moderates.length > 0 && (
<div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
<h5>{i18n.t('moderates')}</h5>
<ul class="list-unstyled mb-0">
{this.state.moderates.map(community => (
{this.state.userRes.moderates.map(community => (
<li>
<Link to={`/c/${community.community_name}`}>
{community.community_name}
@ -841,12 +858,12 @@ export class User extends Component<any, UserState> {
follows() {
return (
<div>
{this.state.follows.length > 0 && (
{this.state.userRes.follows.length > 0 && (
<div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
<h5>{i18n.t('subscribed')}</h5>
<ul class="list-unstyled mb-0">
{this.state.follows.map(community => (
{this.state.userRes.follows.map(community => (
<li>
<Link to={`/c/${community.community_name}`}>
{community.community_name}
@ -866,7 +883,7 @@ export class User extends Component<any, UserState> {
const viewStr = paramUpdates.view || UserDetailsView[this.state.view];
const sortStr = paramUpdates.sort || this.state.sort;
this.props.history.push(
`/u/${this.state.username}/view/${viewStr}/sort/${sortStr}/page/${page}`
`/u/${this.state.userName}/view/${viewStr}/sort/${sortStr}/page/${page}`
);
}
@ -966,7 +983,7 @@ export class User extends Component<any, UserState> {
i.state.userSettingsForm.matrix_user_id = event.target.value;
if (
i.state.userSettingsForm.matrix_user_id == '' &&
!i.state.user.matrix_user_id
!i.state.userRes.user.matrix_user_id
) {
i.state.userSettingsForm.matrix_user_id = undefined;
}
@ -1029,29 +1046,7 @@ export class User extends Component<any, UserState> {
WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
}
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
const res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
if (msg.error == 'couldnt_find_that_username_or_email') {
this.context.router.history.push('/');
}
this.setState({
deleteAccountLoading: false,
userSettingsLoading: false,
});
return;
} else if (res.op == UserOperation.GetUserDetails) {
// Since the UserDetails contains posts/comments as well as some general user info we listen here as well
// and set the parent state if it is not set or differs
const data = res.data as UserDetailsResponse;
if (this.state.user.id !== data.user.id) {
this.state.user = data.user;
this.state.follows = data.follows;
this.state.moderates = data.moderates;
setUserInfo() {
if (this.isCurrentUser) {
this.state.userSettingsForm.show_nsfw =
UserService.Instance.user.show_nsfw;
@ -1076,16 +1071,39 @@ export class User extends Component<any, UserState> {
this.state.userSettingsForm.matrix_user_id =
UserService.Instance.user.matrix_user_id;
}
}
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
const res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
if (msg.error == 'couldnt_find_that_username_or_email') {
this.context.router.history.push('/');
}
this.setState({
deleteAccountLoading: false,
userSettingsLoading: false,
});
return;
} else if (msg.reconnect) {
this.fetchUserData();
} else if (res.op == UserOperation.GetUserDetails) {
// Since the UserDetails contains posts/comments as well as some general user info we listen here as well
// and set the parent state if it is not set or differs
// TODO this might need to get abstracted
const data = res.data as UserDetailsResponse;
this.state.userRes = data;
this.setUserInfo();
this.state.loading = false;
this.setState(this.state);
}
} else if (res.op == UserOperation.SaveUserSettings) {
const data = res.data as LoginResponse;
UserService.Instance.login(data);
this.state.user.bio = this.state.userSettingsForm.bio;
this.state.user.preferred_username = this.state.userSettingsForm.preferred_username;
this.state.user.banner = this.state.userSettingsForm.banner;
this.state.user.avatar = this.state.userSettingsForm.avatar;
this.state.userRes.user.bio = this.state.userSettingsForm.bio;
this.state.userRes.user.preferred_username = this.state.userSettingsForm.preferred_username;
this.state.userRes.user.banner = this.state.userSettingsForm.banner;
this.state.userRes.user.avatar = this.state.userSettingsForm.avatar;
this.state.userSettingsLoading = false;
this.setState(this.state);
@ -1096,14 +1114,47 @@ export class User extends Component<any, UserState> {
deleteAccountShowConfirm: false,
});
this.context.router.history.push('/');
} else if (res.op == UserOperation.GetSite) {
const data = res.data as GetSiteResponse;
this.state.siteRes = data;
this.setState(this.state);
} else if (res.op == UserOperation.AddAdmin) {
const data = res.data as AddAdminResponse;
this.state.siteRes.admins = data.admins;
this.setState(this.state);
} else if (res.op == UserOperation.CreateCommentLike) {
const data = res.data as CommentResponse;
createCommentLikeRes(data, this.state.userRes.comments);
this.setState(this.state);
} else if (
res.op == UserOperation.EditComment ||
res.op == UserOperation.DeleteComment ||
res.op == UserOperation.RemoveComment
) {
const data = res.data as CommentResponse;
editCommentRes(data, this.state.userRes.comments);
this.setState(this.state);
} else if (res.op == UserOperation.CreateComment) {
const data = res.data as CommentResponse;
if (
UserService.Instance.user &&
data.comment.creator_id == UserService.Instance.user.id
) {
toast(i18n.t('reply_sent'));
}
} else if (res.op == UserOperation.SaveComment) {
const data = res.data as CommentResponse;
saveCommentRes(data, this.state.userRes.comments);
this.setState(this.state);
} else if (res.op == UserOperation.CreatePostLike) {
const data = res.data as PostResponse;
createPostLikeFindRes(data, this.state.userRes.posts);
this.setState(this.state);
} else if (res.op == UserOperation.BanUser) {
const data = res.data as BanUserResponse;
this.state.userRes.comments
.filter(c => c.creator_id == data.user.id)
.forEach(c => (c.banned = data.banned));
this.state.userRes.posts
.filter(c => c.creator_id == data.user.id)
.forEach(c => (c.banned = data.banned));
this.setState(this.state);
}
}
}

View file

@ -64,8 +64,13 @@ export const routes: IRoutePropsWithFetch[] = [
{
path: `/post/:id/comment/:comment_id`,
component: Post,
fetchInitialData: (auth, path) => Post.fetchInitialData(auth, path),
},
{
path: `/post/:id`,
component: Post,
fetchInitialData: (auth, path) => Post.fetchInitialData(auth, path),
},
{ path: `/post/:id`, component: Post },
{
path: `/c/:name/data_type/:data_type/sort/:sort/page/:page`,
component: Community,
@ -84,10 +89,23 @@ export const routes: IRoutePropsWithFetch[] = [
{
path: `/u/:username/view/:view/sort/:sort/page/:page`,
component: User,
fetchInitialData: (auth, path) => User.fetchInitialData(auth, path),
},
{
path: `/user/:id`,
component: User,
fetchInitialData: (auth, path) => User.fetchInitialData(auth, path),
},
{
path: `/u/:username`,
component: User,
fetchInitialData: (auth, path) => User.fetchInitialData(auth, path),
},
{
path: `/inbox`,
component: Inbox,
fetchInitialData: (auth, path) => Inbox.fetchInitialData(auth, path),
},
{ path: `/user/:id`, component: User },
{ path: `/u/:username`, component: User },
{ path: `/inbox`, component: Inbox },
{
path: `/modlog/community/:community_id`,
component: Modlog,

View file

@ -31,7 +31,9 @@ export class UserService {
public login(res: LoginResponse) {
this.setClaims(res.jwt);
IsomorphicCookie.save('jwt', res.jwt, { expires: 365 });
let expires = new Date();
expires.setDate(expires.getDate() + 365);
IsomorphicCookie.save('jwt', res.jwt, { expires });
console.log('jwt cookie set');
}

View file

@ -845,6 +845,18 @@ export function getRecipientIdFromProps(props: any): number {
: 1;
}
export function getIdFromProps(props: any): number {
return Number(props.match.params.id);
}
export function getCommentIdFromProps(props: any): number {
return Number(props.match.params.comment_id);
}
export function getUsernameFromProps(props: any): string {
return props.match.params.username;
}
export function editCommentRes(data: CommentResponse, comments: Comment[]) {
let found = comments.find(c => c.id == data.comment.id);
if (found) {