lemmy-ui/src/shared/components/person/profile.tsx

584 lines
18 KiB
TypeScript
Raw Normal View History

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()}
{this.isCurrentUser && 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);
}
}
}