mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-02 10:39:56 +00:00
b27d982a7b
* Extracting user settings and profile page. - Auto-collapsing dropdown and navbar on link clicks. - Fixes #180 * Adding User and Community blocking. Fixes #295 - Added a new settings page. - Switched to myUserInfo. - Removing GetFollowedCommunities endpoint * Fixing blocks
583 lines
18 KiB
TypeScript
583 lines
18 KiB
TypeScript
import { Component, linkEvent } from "inferno";
|
|
import { Link } from "inferno-router";
|
|
import {
|
|
AddAdminResponse,
|
|
BanPersonResponse,
|
|
BlockPersonResponse,
|
|
CommentResponse,
|
|
GetPersonDetails,
|
|
GetPersonDetailsResponse,
|
|
GetSiteResponse,
|
|
PostResponse,
|
|
SortType,
|
|
UserOperation,
|
|
} from "lemmy-js-client";
|
|
import moment from "moment";
|
|
import { Subscription } from "rxjs";
|
|
import { i18n } from "../../i18next";
|
|
import { InitialFetchRequest, PersonDetailsView } from "../../interfaces";
|
|
import { UserService, WebSocketService } from "../../services";
|
|
import {
|
|
authField,
|
|
createCommentLikeRes,
|
|
createPostLikeFindRes,
|
|
editCommentRes,
|
|
editPostFindRes,
|
|
fetchLimit,
|
|
getUsernameFromProps,
|
|
mdToHtml,
|
|
previewLines,
|
|
restoreScrollPosition,
|
|
routeSortTypeToEnum,
|
|
saveCommentRes,
|
|
saveScrollPosition,
|
|
setIsoData,
|
|
setOptionalAuth,
|
|
setupTippy,
|
|
toast,
|
|
updatePersonBlock,
|
|
wsClient,
|
|
wsJsonToRes,
|
|
wsSubscribe,
|
|
wsUserOp,
|
|
} from "../../utils";
|
|
import { BannerIconHeader } from "../common/banner-icon-header";
|
|
import { HtmlTags } from "../common/html-tags";
|
|
import { Icon, Spinner } from "../common/icon";
|
|
import { MomentTime } from "../common/moment-time";
|
|
import { SortSelect } from "../common/sort-select";
|
|
import { CommunityLink } from "../community/community-link";
|
|
import { PersonDetails } from "./person-details";
|
|
import { PersonListing } from "./person-listing";
|
|
|
|
interface ProfileState {
|
|
personRes: GetPersonDetailsResponse;
|
|
userName: string;
|
|
view: PersonDetailsView;
|
|
sort: SortType;
|
|
page: number;
|
|
loading: boolean;
|
|
siteRes: GetSiteResponse;
|
|
}
|
|
|
|
interface ProfileProps {
|
|
view: PersonDetailsView;
|
|
sort: SortType;
|
|
page: number;
|
|
person_id: number | null;
|
|
username: string;
|
|
}
|
|
|
|
interface UrlParams {
|
|
view?: string;
|
|
sort?: SortType;
|
|
page?: number;
|
|
}
|
|
|
|
export class Profile extends Component<any, ProfileState> {
|
|
private isoData = setIsoData(this.context);
|
|
private subscription: Subscription;
|
|
private emptyState: ProfileState = {
|
|
personRes: undefined,
|
|
userName: getUsernameFromProps(this.props),
|
|
loading: true,
|
|
view: Profile.getViewFromProps(this.props.match.view),
|
|
sort: Profile.getSortTypeFromProps(this.props.match.sort),
|
|
page: Profile.getPageFromProps(this.props.match.page),
|
|
siteRes: this.isoData.site_res,
|
|
};
|
|
|
|
constructor(props: any, context: any) {
|
|
super(props, context);
|
|
|
|
this.state = this.emptyState;
|
|
this.handleSortChange = this.handleSortChange.bind(this);
|
|
|
|
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.personRes = this.isoData.routeData[0];
|
|
this.state.loading = false;
|
|
} else {
|
|
this.fetchUserData();
|
|
}
|
|
|
|
setupTippy();
|
|
}
|
|
|
|
fetchUserData() {
|
|
let form: GetPersonDetails = {
|
|
username: this.state.userName,
|
|
sort: this.state.sort,
|
|
saved_only: this.state.view === PersonDetailsView.Saved,
|
|
page: this.state.page,
|
|
limit: fetchLimit,
|
|
auth: authField(false),
|
|
};
|
|
WebSocketService.Instance.send(wsClient.getPersonDetails(form));
|
|
}
|
|
|
|
get isCurrentUser() {
|
|
return (
|
|
UserService.Instance.myUserInfo?.local_user_view.person.id ==
|
|
this.state.personRes.person_view.person.id
|
|
);
|
|
}
|
|
|
|
static getViewFromProps(view: string): PersonDetailsView {
|
|
return view ? PersonDetailsView[view] : PersonDetailsView.Overview;
|
|
}
|
|
|
|
static getSortTypeFromProps(sort: string): SortType {
|
|
return sort ? routeSortTypeToEnum(sort) : SortType.New;
|
|
}
|
|
|
|
static getPageFromProps(page: number): number {
|
|
return page ? Number(page) : 1;
|
|
}
|
|
|
|
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
|
|
let pathSplit = req.path.split("/");
|
|
let promises: Promise<any>[] = [];
|
|
|
|
// It can be /u/me, or /username/1
|
|
let idOrName = pathSplit[2];
|
|
let person_id: number;
|
|
let username: string;
|
|
if (isNaN(Number(idOrName))) {
|
|
username = idOrName;
|
|
} else {
|
|
person_id = Number(idOrName);
|
|
}
|
|
|
|
let view = this.getViewFromProps(pathSplit[4]);
|
|
let sort = this.getSortTypeFromProps(pathSplit[6]);
|
|
let page = this.getPageFromProps(Number(pathSplit[8]));
|
|
|
|
let form: GetPersonDetails = {
|
|
sort,
|
|
saved_only: view === PersonDetailsView.Saved,
|
|
page,
|
|
limit: fetchLimit,
|
|
};
|
|
setOptionalAuth(form, req.auth);
|
|
this.setIdOrName(form, person_id, username);
|
|
promises.push(req.client.getPersonDetails(form));
|
|
return promises;
|
|
}
|
|
|
|
static setIdOrName(obj: any, id: number, name_: string) {
|
|
if (id) {
|
|
obj.person_id = id;
|
|
} else {
|
|
obj.username = name_;
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.subscription.unsubscribe();
|
|
saveScrollPosition(this.context);
|
|
}
|
|
|
|
static getDerivedStateFromProps(props: any): ProfileProps {
|
|
return {
|
|
view: this.getViewFromProps(props.match.params.view),
|
|
sort: this.getSortTypeFromProps(props.match.params.sort),
|
|
page: this.getPageFromProps(props.match.params.page),
|
|
person_id: Number(props.match.params.id) || null,
|
|
username: props.match.params.username,
|
|
};
|
|
}
|
|
|
|
componentDidUpdate(lastProps: any) {
|
|
// Necessary if you are on a post and you click another post (same route)
|
|
if (
|
|
lastProps.location.pathname.split("/")[2] !==
|
|
lastProps.history.location.pathname.split("/")[2]
|
|
) {
|
|
// Couldnt get a refresh working. This does for now.
|
|
location.reload();
|
|
}
|
|
}
|
|
|
|
get documentTitle(): string {
|
|
return `@${this.state.personRes.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`;
|
|
}
|
|
|
|
get bioTag(): string {
|
|
return this.state.personRes.person_view.person.bio
|
|
? previewLines(this.state.personRes.person_view.person.bio)
|
|
: undefined;
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<div class="container">
|
|
{this.state.loading ? (
|
|
<h5>
|
|
<Spinner large />
|
|
</h5>
|
|
) : (
|
|
<div class="row">
|
|
<div class="col-12 col-md-8">
|
|
<>
|
|
<HtmlTags
|
|
title={this.documentTitle}
|
|
path={this.context.router.route.match.url}
|
|
description={this.bioTag}
|
|
image={this.state.personRes.person_view.person.avatar}
|
|
/>
|
|
{this.userInfo()}
|
|
<hr />
|
|
</>
|
|
{!this.state.loading && this.selects()}
|
|
<PersonDetails
|
|
personRes={this.state.personRes}
|
|
admins={this.state.siteRes.admins}
|
|
sort={this.state.sort}
|
|
page={this.state.page}
|
|
limit={fetchLimit}
|
|
enableDownvotes={
|
|
this.state.siteRes.site_view.site.enable_downvotes
|
|
}
|
|
enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
|
|
view={this.state.view}
|
|
onPageChange={this.handlePageChange}
|
|
/>
|
|
</div>
|
|
|
|
{!this.state.loading && (
|
|
<div class="col-12 col-md-4">
|
|
{this.moderates()}
|
|
{UserService.Instance.myUserInfo && this.follows()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
viewRadios() {
|
|
return (
|
|
<div class="btn-group btn-group-toggle flex-wrap mb-2">
|
|
<label
|
|
className={`btn btn-outline-secondary pointer
|
|
${this.state.view == PersonDetailsView.Overview && "active"}
|
|
`}
|
|
>
|
|
<input
|
|
type="radio"
|
|
value={PersonDetailsView.Overview}
|
|
checked={this.state.view === PersonDetailsView.Overview}
|
|
onChange={linkEvent(this, this.handleViewChange)}
|
|
/>
|
|
{i18n.t("overview")}
|
|
</label>
|
|
<label
|
|
className={`btn btn-outline-secondary pointer
|
|
${this.state.view == PersonDetailsView.Comments && "active"}
|
|
`}
|
|
>
|
|
<input
|
|
type="radio"
|
|
value={PersonDetailsView.Comments}
|
|
checked={this.state.view == PersonDetailsView.Comments}
|
|
onChange={linkEvent(this, this.handleViewChange)}
|
|
/>
|
|
{i18n.t("comments")}
|
|
</label>
|
|
<label
|
|
className={`btn btn-outline-secondary pointer
|
|
${this.state.view == PersonDetailsView.Posts && "active"}
|
|
`}
|
|
>
|
|
<input
|
|
type="radio"
|
|
value={PersonDetailsView.Posts}
|
|
checked={this.state.view == PersonDetailsView.Posts}
|
|
onChange={linkEvent(this, this.handleViewChange)}
|
|
/>
|
|
{i18n.t("posts")}
|
|
</label>
|
|
<label
|
|
className={`btn btn-outline-secondary pointer
|
|
${this.state.view == PersonDetailsView.Saved && "active"}
|
|
`}
|
|
>
|
|
<input
|
|
type="radio"
|
|
value={PersonDetailsView.Saved}
|
|
checked={this.state.view == PersonDetailsView.Saved}
|
|
onChange={linkEvent(this, this.handleViewChange)}
|
|
/>
|
|
{i18n.t("saved")}
|
|
</label>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
selects() {
|
|
return (
|
|
<div className="mb-2">
|
|
<span class="mr-3">{this.viewRadios()}</span>
|
|
<SortSelect
|
|
sort={this.state.sort}
|
|
onChange={this.handleSortChange}
|
|
hideHot
|
|
hideMostComments
|
|
/>
|
|
<a
|
|
href={`/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`}
|
|
rel="noopener"
|
|
title="RSS"
|
|
>
|
|
<Icon icon="rss" classes="text-muted small mx-2" />
|
|
</a>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
userInfo() {
|
|
let pv = this.state.personRes?.person_view;
|
|
|
|
return (
|
|
<div>
|
|
<BannerIconHeader banner={pv.person.banner} icon={pv.person.avatar} />
|
|
<div class="mb-3">
|
|
<div class="">
|
|
<div class="mb-0 d-flex flex-wrap">
|
|
<div>
|
|
{pv.person.display_name && (
|
|
<h5 class="mb-0">{pv.person.display_name}</h5>
|
|
)}
|
|
<ul class="list-inline mb-2">
|
|
<li className="list-inline-item">
|
|
<PersonListing
|
|
person={pv.person}
|
|
realLink
|
|
useApubName
|
|
muted
|
|
hideAvatar
|
|
/>
|
|
</li>
|
|
{pv.person.banned && (
|
|
<li className="list-inline-item badge badge-danger">
|
|
{i18n.t("banned")}
|
|
</li>
|
|
)}
|
|
</ul>
|
|
</div>
|
|
<div className="flex-grow-1 unselectable pointer mx-2"></div>
|
|
{!this.isCurrentUser && (
|
|
<>
|
|
<a
|
|
className={`d-flex align-self-start btn btn-secondary mr-2 ${
|
|
!pv.person.matrix_user_id && "invisible"
|
|
}`}
|
|
rel="noopener"
|
|
href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
|
|
>
|
|
{i18n.t("send_secure_message")}
|
|
</a>
|
|
<Link
|
|
className={"d-flex align-self-start btn btn-secondary"}
|
|
to={`/create_private_message/recipient/${pv.person.id}`}
|
|
>
|
|
{i18n.t("send_message")}
|
|
</Link>
|
|
</>
|
|
)}
|
|
</div>
|
|
{pv.person.bio && (
|
|
<div className="d-flex align-items-center mb-2">
|
|
<div
|
|
className="md-div"
|
|
dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<ul class="list-inline mb-2">
|
|
<li className="list-inline-item badge badge-light">
|
|
{i18n.t("number_of_posts", { count: pv.counts.post_count })}
|
|
</li>
|
|
<li className="list-inline-item badge badge-light">
|
|
{i18n.t("number_of_comments", {
|
|
count: pv.counts.comment_count,
|
|
})}
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="text-muted">
|
|
{i18n.t("joined")}{" "}
|
|
<MomentTime data={pv.person} showAgo ignoreUpdated />
|
|
</div>
|
|
<div className="d-flex align-items-center text-muted mb-2">
|
|
<Icon icon="cake" />
|
|
<span className="ml-2">
|
|
{i18n.t("cake_day_title")}{" "}
|
|
{moment.utc(pv.person.published).local().format("MMM DD, YYYY")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
moderates() {
|
|
return (
|
|
<div>
|
|
{this.state.personRes.moderates.length > 0 && (
|
|
<div class="card border-secondary mb-3">
|
|
<div class="card-body">
|
|
<h5>{i18n.t("moderates")}</h5>
|
|
<ul class="list-unstyled mb-0">
|
|
{this.state.personRes.moderates.map(cmv => (
|
|
<li>
|
|
<CommunityLink community={cmv.community} />
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
follows() {
|
|
let follows = UserService.Instance.myUserInfo.follows;
|
|
return (
|
|
<div>
|
|
{follows.length > 0 && (
|
|
<div class="card border-secondary mb-3">
|
|
<div class="card-body">
|
|
<h5>{i18n.t("subscribed")}</h5>
|
|
<ul class="list-unstyled mb-0">
|
|
{follows.map(cfv => (
|
|
<li>
|
|
<CommunityLink community={cfv.community} />
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
updateUrl(paramUpdates: UrlParams) {
|
|
const page = paramUpdates.page || this.state.page;
|
|
const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
|
|
const sortStr = paramUpdates.sort || this.state.sort;
|
|
|
|
let typeView = `/u/${this.state.userName}`;
|
|
|
|
this.props.history.push(
|
|
`${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
|
|
);
|
|
this.state.loading = true;
|
|
this.setState(this.state);
|
|
this.fetchUserData();
|
|
}
|
|
|
|
handlePageChange(page: number) {
|
|
this.updateUrl({ page });
|
|
}
|
|
|
|
handleSortChange(val: SortType) {
|
|
this.updateUrl({ sort: val, page: 1 });
|
|
}
|
|
|
|
handleViewChange(i: Profile, event: any) {
|
|
i.updateUrl({
|
|
view: PersonDetailsView[Number(event.target.value)],
|
|
page: 1,
|
|
});
|
|
}
|
|
|
|
parseMessage(msg: any) {
|
|
let op = wsUserOp(msg);
|
|
console.log(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 (op == UserOperation.GetPersonDetails) {
|
|
// Since the PersonDetails 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
|
|
let data = wsJsonToRes<GetPersonDetailsResponse>(msg).data;
|
|
this.state.personRes = data;
|
|
console.log(data);
|
|
this.state.loading = false;
|
|
this.setState(this.state);
|
|
restoreScrollPosition(this.context);
|
|
} else if (op == UserOperation.AddAdmin) {
|
|
let data = wsJsonToRes<AddAdminResponse>(msg).data;
|
|
this.state.siteRes.admins = data.admins;
|
|
this.setState(this.state);
|
|
} else if (op == UserOperation.CreateCommentLike) {
|
|
let data = wsJsonToRes<CommentResponse>(msg).data;
|
|
createCommentLikeRes(data.comment_view, this.state.personRes.comments);
|
|
this.setState(this.state);
|
|
} else if (
|
|
op == UserOperation.EditComment ||
|
|
op == UserOperation.DeleteComment ||
|
|
op == UserOperation.RemoveComment
|
|
) {
|
|
let data = wsJsonToRes<CommentResponse>(msg).data;
|
|
editCommentRes(data.comment_view, this.state.personRes.comments);
|
|
this.setState(this.state);
|
|
} else if (op == UserOperation.CreateComment) {
|
|
let data = wsJsonToRes<CommentResponse>(msg).data;
|
|
if (
|
|
UserService.Instance.myUserInfo &&
|
|
data.comment_view.creator.id ==
|
|
UserService.Instance.myUserInfo.local_user_view.person.id
|
|
) {
|
|
toast(i18n.t("reply_sent"));
|
|
}
|
|
} else if (op == UserOperation.SaveComment) {
|
|
let data = wsJsonToRes<CommentResponse>(msg).data;
|
|
saveCommentRes(data.comment_view, this.state.personRes.comments);
|
|
this.setState(this.state);
|
|
} else if (
|
|
op == UserOperation.EditPost ||
|
|
op == UserOperation.DeletePost ||
|
|
op == UserOperation.RemovePost ||
|
|
op == UserOperation.LockPost ||
|
|
op == UserOperation.StickyPost ||
|
|
op == UserOperation.SavePost
|
|
) {
|
|
let data = wsJsonToRes<PostResponse>(msg).data;
|
|
editPostFindRes(data.post_view, this.state.personRes.posts);
|
|
this.setState(this.state);
|
|
} else if (op == UserOperation.CreatePostLike) {
|
|
let data = wsJsonToRes<PostResponse>(msg).data;
|
|
createPostLikeFindRes(data.post_view, this.state.personRes.posts);
|
|
this.setState(this.state);
|
|
} else if (op == UserOperation.BanPerson) {
|
|
let data = wsJsonToRes<BanPersonResponse>(msg).data;
|
|
this.state.personRes.comments
|
|
.filter(c => c.creator.id == data.person_view.person.id)
|
|
.forEach(c => (c.creator.banned = data.banned));
|
|
this.state.personRes.posts
|
|
.filter(c => c.creator.id == data.person_view.person.id)
|
|
.forEach(c => (c.creator.banned = data.banned));
|
|
this.setState(this.state);
|
|
} else if (op == UserOperation.BlockPerson) {
|
|
let data = wsJsonToRes<BlockPersonResponse>(msg).data;
|
|
updatePersonBlock(data);
|
|
}
|
|
}
|
|
}
|