+import classNames from "classnames";
+import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import { Link } from "inferno-router";
+import { RouteComponentProps } from "inferno-router/dist/Route";
import {
- AddAdminResponse,
+ AddAdmin,
+ AddModToCommunity,
+ BanFromCommunity,
+ BanFromCommunityResponse,
+ BanPerson,
BanPersonResponse,
BlockPerson,
- BlockPersonResponse,
+ CommentId,
+ CommentReplyResponse,
CommentResponse,
+ Community,
+ CommunityModeratorView,
+ CreateComment,
+ CreateCommentLike,
+ CreateCommentReport,
+ CreatePostLike,
+ CreatePostReport,
+ DeleteComment,
+ DeletePost,
+ DistinguishComment,
+ EditComment,
+ EditPost,
+ FeaturePost,
GetPersonDetails,
GetPersonDetailsResponse,
GetSiteResponse,
+ LockPost,
+ MarkCommentReplyAsRead,
+ MarkPersonMentionAsRead,
+ PersonView,
PostResponse,
+ PurgeComment,
+ PurgeItemResponse,
+ PurgePerson,
+ PurgePost,
+ RemoveComment,
+ RemovePost,
+ SaveComment,
+ SavePost,
SortType,
- UserOperation,
+ TransferCommunity,
} 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 { UserService } from "../../services";
+import { FirstLoadService } from "../../services/FirstLoadService";
+import { HttpService, RequestState } from "../../services/HttpService";
import {
- authField,
- createCommentLikeRes,
- createPostLikeFindRes,
- editCommentRes,
- editPostFindRes,
+ QueryParams,
+ RouteDataResponse,
+ canMod,
+ capitalizeFirstLetter,
+ editComment,
+ editPost,
+ editWith,
+ enableDownvotes,
+ enableNsfw,
fetchLimit,
- getUsernameFromProps,
+ futureDaysToUnixTime,
+ getCommentParentId,
+ getPageFromString,
+ getQueryParams,
+ getQueryString,
+ isAdmin,
+ isBanned,
mdToHtml,
+ myAuth,
+ myAuthRequired,
numToSI,
- previewLines,
+ relTags,
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 { PersonDetails } from "./person-details";
import { PersonListing } from "./person-listing";
+type ProfileData = RouteDataResponse<{
+ personResponse: GetPersonDetailsResponse;
+}>;
+
interface ProfileState {
- personRes: GetPersonDetailsResponse;
- userName: string;
- view: PersonDetailsView;
- sort: SortType;
- page: number;
- loading: boolean;
+ personRes: RequestState<GetPersonDetailsResponse>;
personBlocked: boolean;
+ banReason?: string;
+ banExpireDays?: number;
+ showBanDialog: boolean;
+ removeData: boolean;
siteRes: GetSiteResponse;
+ finished: Map<CommentId, boolean | undefined>;
+ isIsomorphic: boolean;
}
interface ProfileProps {
view: PersonDetailsView;
sort: SortType;
page: number;
- person_id: number | null;
- username: string;
}
-interface UrlParams {
- view?: string;
- sort?: SortType;
- page?: number;
+function getProfileQueryParams() {
+ return getQueryParams<ProfileProps>({
+ view: getViewFromProps,
+ page: getPageFromString,
+ sort: getSortTypeFromQuery,
+ });
+}
+
+function getSortTypeFromQuery(sort?: string): SortType {
+ return sort ? (sort as SortType) : "New";
+}
+
+function getViewFromProps(view?: string): PersonDetailsView {
+ return view
+ ? PersonDetailsView[view] ?? PersonDetailsView.Overview
+ : PersonDetailsView.Overview;
}
-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),
+const getCommunitiesListing = (
+ translationKey: NoOptionI18nKeys,
+ communityViews?: { community: Community }[]
+) =>
+ communityViews &&
+ communityViews.length > 0 && (
+ <div className="card border-secondary mb-3">
+ <div className="card-body">
+ <h5>{i18n.t(translationKey)}</h5>
+ <ul className="list-unstyled mb-0">
+ {communityViews.map(({ community }) => (
+ <li key={community.id}>
+ <CommunityLink community={community} />
+ </li>
+ ))}
+ </ul>
+ </div>
+ </div>
+ );
+
+const Moderates = ({ moderates }: { moderates?: CommunityModeratorView[] }) =>
+ getCommunitiesListing("moderates", moderates);
+
+const Follows = () =>
+ getCommunitiesListing("subscribed", UserService.Instance.myUserInfo?.follows);
+
+export class Profile extends Component<
+ RouteComponentProps<{ username: string }>,
+ ProfileState
+> {
+ private isoData = setIsoData<ProfileData>(this.context);
+ state: ProfileState = {
+ personRes: { state: "empty" },
personBlocked: false,
siteRes: this.isoData.site_res,
+ showBanDialog: false,
+ removeData: false,
+ finished: new Map(),
+ isIsomorphic: false,
};
- constructor(props: any, context: any) {
+ constructor(props: RouteComponentProps<{ username: string }>, context: any) {
super(props, context);
- this.state = this.emptyState;
this.handleSortChange = this.handleSortChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+ this.handleBlockPerson = this.handleBlockPerson.bind(this);
+ this.handleUnblockPerson = this.handleUnblockPerson.bind(this);
+
+ this.handleCreateComment = this.handleCreateComment.bind(this);
+ this.handleEditComment = this.handleEditComment.bind(this);
+ this.handleSaveComment = this.handleSaveComment.bind(this);
+ this.handleBlockPersonAlt = this.handleBlockPersonAlt.bind(this);
+ this.handleDeleteComment = this.handleDeleteComment.bind(this);
+ this.handleRemoveComment = this.handleRemoveComment.bind(this);
+ this.handleCommentVote = this.handleCommentVote.bind(this);
+ this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this);
+ this.handleAddAdmin = this.handleAddAdmin.bind(this);
+ this.handlePurgePerson = this.handlePurgePerson.bind(this);
+ this.handlePurgeComment = this.handlePurgeComment.bind(this);
+ this.handleCommentReport = this.handleCommentReport.bind(this);
+ this.handleDistinguishComment = this.handleDistinguishComment.bind(this);
+ this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
+ this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this);
+ this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this);
+ this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this);
+ this.handleBanPerson = this.handleBanPerson.bind(this);
+ this.handlePostVote = this.handlePostVote.bind(this);
+ this.handlePostEdit = this.handlePostEdit.bind(this);
+ this.handlePostReport = this.handlePostReport.bind(this);
+ this.handleLockPost = this.handleLockPost.bind(this);
+ this.handleDeletePost = this.handleDeletePost.bind(this);
+ this.handleRemovePost = this.handleRemovePost.bind(this);
+ this.handleSavePost = this.handleSavePost.bind(this);
+ this.handlePurgePost = this.handlePurgePost.bind(this);
+ this.handleFeaturePost = this.handleFeaturePost.bind(this);
// 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();
+ if (FirstLoadService.isFirstLoad) {
+ this.state = {
+ ...this.state,
+ personRes: this.isoData.routeData.personResponse,
+ isIsomorphic: true,
+ };
}
+ }
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await this.fetchUserData();
+ }
setupTippy();
- this.setPersonBlock();
}
- 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));
+ componentWillUnmount() {
+ saveScrollPosition(this.context);
}
- get isCurrentUser() {
- return (
- UserService.Instance.myUserInfo?.local_user_view.person.id ==
- this.state.personRes.person_view.person.id
- );
- }
+ async fetchUserData() {
+ const { page, sort, view } = getProfileQueryParams();
- setPersonBlock() {
- this.state.personBlocked = UserService.Instance.myUserInfo.person_blocks
- .map(a => a.target.id)
- .includes(this.state.personRes?.person_view.person.id);
+ this.setState({ personRes: { state: "empty" } });
+ this.setState({
+ personRes: await HttpService.client.getPersonDetails({
+ username: this.props.match.params.username,
+ sort,
+ saved_only: view === PersonDetailsView.Saved,
+ page,
+ limit: fetchLimit,
+ auth: myAuth(),
+ }),
+ });
+ restoreScrollPosition(this.context);
+ this.setPersonBlock();
}
- static getViewFromProps(view: string): PersonDetailsView {
- return view ? PersonDetailsView[view] : PersonDetailsView.Overview;
+ get amCurrentUser() {
+ if (this.state.personRes.state === "success") {
+ return (
+ UserService.Instance.myUserInfo?.local_user_view.person.id ===
+ this.state.personRes.data.person_view.person.id
+ );
+ } else {
+ return false;
+ }
}
- static getSortTypeFromProps(sort: string): SortType {
- return sort ? routeSortTypeToEnum(sort) : SortType.New;
- }
+ setPersonBlock() {
+ const mui = UserService.Instance.myUserInfo;
+ const res = this.state.personRes;
- static getPageFromProps(page: number): number {
- return page ? Number(page) : 1;
+ if (mui && res.state === "success") {
+ this.setState({
+ personBlocked: mui.person_blocks.some(
+ ({ target: { id } }) => id === res.data.person_view.person.id
+ ),
+ });
+ }
}
- 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);
- }
+ static async fetchInitialData({
+ client,
+ path,
+ query: { page, sort, view: urlView },
+ auth,
+ }: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<ProfileData> {
+ const pathSplit = path.split("/");
- let view = this.getViewFromProps(pathSplit[4]);
- let sort = this.getSortTypeFromProps(pathSplit[6]);
- let page = this.getPageFromProps(Number(pathSplit[8]));
+ const username = pathSplit[2];
+ const view = getViewFromProps(urlView);
- let form: GetPersonDetails = {
- sort,
+ const form: GetPersonDetails = {
+ username: username,
+ sort: getSortTypeFromQuery(sort),
saved_only: view === PersonDetailsView.Saved,
- page,
+ page: getPageFromString(page),
limit: fetchLimit,
+ auth,
};
- 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,
+ personResponse: await client.getPersonDetails(form),
};
}
- 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}`;
+ const siteName = this.state.siteRes.site_view.site.name;
+ const res = this.state.personRes;
+ return res.state == "success"
+ ? `@${res.data.person_view.person.name} - ${siteName}`
+ : siteName;
}
- 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 ? (
+ renderPersonRes() {
+ switch (this.state.personRes.state) {
+ case "loading":
+ return (
<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()}
+ );
+ case "success": {
+ const siteRes = this.state.siteRes;
+ const personRes = this.state.personRes.data;
+ const { page, sort, view } = getProfileQueryParams();
+
+ return (
+ <div className="row">
+ <div className="col-12 col-md-8">
+ <HtmlTags
+ title={this.documentTitle}
+ path={this.context.router.route.match.url}
+ description={personRes.person_view.person.bio}
+ image={personRes.person_view.person.avatar}
+ />
+
+ {this.userInfo(personRes.person_view)}
+
+ <hr />
+
+ {this.selects}
+
<PersonDetails
- personRes={this.state.personRes}
- admins={this.state.siteRes.admins}
- sort={this.state.sort}
- page={this.state.page}
+ personRes={personRes}
+ admins={siteRes.admins}
+ sort={sort}
+ page={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}
+ finished={this.state.finished}
+ enableDownvotes={enableDownvotes(siteRes)}
+ enableNsfw={enableNsfw(siteRes)}
+ view={view}
onPageChange={this.handlePageChange}
+ allLanguages={siteRes.all_languages}
+ siteLanguages={siteRes.discussion_languages}
+ // TODO all the forms here
+ onSaveComment={this.handleSaveComment}
+ onBlockPerson={this.handleBlockPersonAlt}
+ onDeleteComment={this.handleDeleteComment}
+ onRemoveComment={this.handleRemoveComment}
+ onCommentVote={this.handleCommentVote}
+ onCommentReport={this.handleCommentReport}
+ onDistinguishComment={this.handleDistinguishComment}
+ onAddModToCommunity={this.handleAddModToCommunity}
+ onAddAdmin={this.handleAddAdmin}
+ onTransferCommunity={this.handleTransferCommunity}
+ onPurgeComment={this.handlePurgeComment}
+ onPurgePerson={this.handlePurgePerson}
+ onCommentReplyRead={this.handleCommentReplyRead}
+ onPersonMentionRead={this.handlePersonMentionRead}
+ onBanPersonFromCommunity={this.handleBanFromCommunity}
+ onBanPerson={this.handleBanPerson}
+ onCreateComment={this.handleCreateComment}
+ onEditComment={this.handleEditComment}
+ onPostEdit={this.handlePostEdit}
+ onPostVote={this.handlePostVote}
+ onPostReport={this.handlePostReport}
+ onLockPost={this.handleLockPost}
+ onDeletePost={this.handleDeletePost}
+ onRemovePost={this.handleRemovePost}
+ onSavePost={this.handleSavePost}
+ onPurgePost={this.handlePurgePost}
+ onFeaturePost={this.handleFeaturePost}
/>
</div>
- {!this.state.loading && (
- <div class="col-12 col-md-4">
- {this.moderates()}
- {this.isCurrentUser && this.follows()}
- </div>
- )}
+ <div className="col-12 col-md-4">
+ <Moderates moderates={personRes.moderates} />
+ {this.amCurrentUser && <Follows />}
+ </div>
</div>
- )}
+ );
+ }
+ }
+ }
+
+ render() {
+ return <div className="container-lg">{this.renderPersonRes()}</div>;
+ }
+
+ get viewRadios() {
+ return (
+ <div className="btn-group btn-group-toggle flex-wrap mb-2">
+ {this.getRadio(PersonDetailsView.Overview)}
+ {this.getRadio(PersonDetailsView.Comments)}
+ {this.getRadio(PersonDetailsView.Posts)}
+ {this.amCurrentUser && this.getRadio(PersonDetailsView.Saved)}
</div>
);
}
- viewRadios() {
+ getRadio(view: PersonDetailsView) {
+ const { view: urlView } = getProfileQueryParams();
+ const active = view === urlView;
+
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>
+ <label
+ className={classNames("btn btn-outline-secondary pointer", {
+ active,
+ })}
+ >
+ <input
+ type="radio"
+ value={view}
+ checked={active}
+ onChange={linkEvent(this, this.handleViewChange)}
+ />
+ {i18n.t(view.toLowerCase() as NoOptionI18nKeys)}
+ </label>
);
}
- selects() {
+ get selects() {
+ const { sort } = getProfileQueryParams();
+ const { username } = this.props.match.params;
+
+ const profileRss = `/feeds/u/${username}.xml?sort=${sort}`;
+
return (
<div className="mb-2">
- <span class="mr-3">{this.viewRadios()}</span>
+ <span className="mr-3">{this.viewRadios}</span>
<SortSelect
- sort={this.state.sort}
+ sort={sort}
onChange={this.handleSortChange}
hideHot
hideMostComments
/>
- <a
- href={`/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`}
- rel="noopener"
- title="RSS"
- >
+ <a href={profileRss} rel={relTags} title="RSS">
<Icon icon="rss" classes="text-muted small mx-2" />
</a>
+ <link rel="alternate" type="application/atom+xml" href={profileRss} />
</div>
);
}
- handleBlockPerson(personId: number) {
- if (personId != 0) {
- let blockUserForm: BlockPerson = {
- person_id: personId,
- block: true,
- auth: authField(),
- };
- WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
- }
- }
- handleUnblockPerson(recipientId: number) {
- let blockUserForm: BlockPerson = {
- person_id: recipientId,
- block: false,
- auth: authField(),
- };
- WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
- }
- userInfo() {
- let pv = this.state.personRes?.person_view;
+ userInfo(pv: PersonView) {
+ const {
+ personBlocked,
+ siteRes: { admins },
+ showBanDialog,
+ } = this.state;
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>
+ pv && (
+ <div>
+ {!isBanned(pv.person) && (
+ <BannerIconHeader
+ banner={pv.person.banner}
+ icon={pv.person.avatar}
+ />
+ )}
+ <div className="mb-3">
+ <div className="">
+ <div className="mb-0 d-flex flex-wrap">
+ <div>
+ {pv.person.display_name && (
+ <h5 className="mb-0">{pv.person.display_name}</h5>
)}
- {pv.person.admin && (
- <li className="list-inline-item badge badge-light">
- {i18n.t("admin")}
+ <ul className="list-inline mb-2">
+ <li className="list-inline-item">
+ <PersonListing
+ person={pv.person}
+ realLink
+ useApubName
+ muted
+ hideAvatar
+ />
</li>
- )}
- {pv.person.bot_account && (
- <li className="list-inline-item badge badge-light">
- {i18n.t("bot_account").toLowerCase()}
- </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 mr-2"}
- to={`/create_private_message/recipient/${pv.person.id}`}
- >
- {i18n.t("send_message")}
- </Link>
- {this.state.personBlocked ? (
+ {isBanned(pv.person) && (
+ <li className="list-inline-item badge badge-danger">
+ {i18n.t("banned")}
+ </li>
+ )}
+ {pv.person.deleted && (
+ <li className="list-inline-item badge badge-danger">
+ {i18n.t("deleted")}
+ </li>
+ )}
+ {pv.person.admin && (
+ <li className="list-inline-item badge badge-light">
+ {i18n.t("admin")}
+ </li>
+ )}
+ {pv.person.bot_account && (
+ <li className="list-inline-item badge badge-light">
+ {i18n.t("bot_account").toLowerCase()}
+ </li>
+ )}
+ </ul>
+ </div>
+ {this.banDialog(pv)}
+ <div className="flex-grow-1 unselectable pointer mx-2"></div>
+ {!this.amCurrentUser && UserService.Instance.myUserInfo && (
+ <>
+ <a
+ className={`d-flex align-self-start btn btn-secondary mr-2 ${
+ !pv.person.matrix_user_id && "invisible"
+ }`}
+ rel={relTags}
+ 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 mr-2"
+ }
+ to={`/create_private_message/${pv.person.id}`}
+ >
+ {i18n.t("send_message")}
+ </Link>
+ {personBlocked ? (
+ <button
+ className={
+ "d-flex align-self-start btn btn-secondary mr-2"
+ }
+ onClick={linkEvent(
+ pv.person.id,
+ this.handleUnblockPerson
+ )}
+ >
+ {i18n.t("unblock_user")}
+ </button>
+ ) : (
+ <button
+ className={
+ "d-flex align-self-start btn btn-secondary mr-2"
+ }
+ onClick={linkEvent(
+ pv.person.id,
+ this.handleBlockPerson
+ )}
+ >
+ {i18n.t("block_user")}
+ </button>
+ )}
+ </>
+ )}
+
+ {canMod(pv.person.id, undefined, admins) &&
+ !isAdmin(pv.person.id, admins) &&
+ !showBanDialog &&
+ (!isBanned(pv.person) ? (
<button
- className={"d-flex align-self-start btn btn-secondary"}
- onClick={linkEvent(
- pv.person.id,
- this.handleUnblockPerson
- )}
+ className={
+ "d-flex align-self-start btn btn-secondary mr-2"
+ }
+ onClick={linkEvent(this, this.handleModBanShow)}
+ aria-label={i18n.t("ban")}
>
- {i18n.t("unblock_user")}
+ {capitalizeFirstLetter(i18n.t("ban"))}
</button>
) : (
<button
- className={"d-flex align-self-start btn btn-secondary"}
- onClick={linkEvent(pv.person.id, this.handleBlockPerson)}
+ className={
+ "d-flex align-self-start btn btn-secondary mr-2"
+ }
+ onClick={linkEvent(this, this.handleModBanSubmit)}
+ aria-label={i18n.t("unban")}
>
- {i18n.t("block_user")}
+ {capitalizeFirstLetter(i18n.t("unban"))}
</button>
- )}
- </>
+ ))}
+ </div>
+ {pv.person.bio && (
+ <div className="d-flex align-items-center mb-2">
+ <div
+ className="md-div"
+ dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
+ />
+ </div>
)}
- </div>
- {pv.person.bio && (
- <div className="d-flex align-items-center mb-2">
- <div
- className="md-div"
- dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
+ <div>
+ <ul className="list-inline mb-2">
+ <li className="list-inline-item badge badge-light">
+ {i18n.t("number_of_posts", {
+ count: Number(pv.counts.post_count),
+ formattedCount: numToSI(pv.counts.post_count),
+ })}
+ </li>
+ <li className="list-inline-item badge badge-light">
+ {i18n.t("number_of_comments", {
+ count: Number(pv.counts.comment_count),
+ formattedCount: numToSI(pv.counts.comment_count),
+ })}
+ </li>
+ </ul>
+ </div>
+ <div className="text-muted">
+ {i18n.t("joined")}{" "}
+ <MomentTime
+ published={pv.person.published}
+ showAgo
+ ignoreUpdated
/>
</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,
- formattedCount: numToSI(pv.counts.post_count),
- })}
- </li>
- <li className="list-inline-item badge badge-light">
- {i18n.t("number_of_comments", {
- count: pv.counts.comment_count,
- formattedCount: numToSI(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 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>
+ {!UserService.Instance.myUserInfo && (
+ <div className="alert alert-info" role="alert">
+ {i18n.t("profile_not_logged_in_alert")}
+ </div>
+ )}
</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>
- );
- }
+ banDialog(pv: PersonView) {
+ const { showBanDialog } = this.state;
- 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>
+ showBanDialog && (
+ <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
+ <div className="form-group row col-12">
+ <label className="col-form-label" htmlFor="profile-ban-reason">
+ {i18n.t("reason")}
+ </label>
+ <input
+ type="text"
+ id="profile-ban-reason"
+ className="form-control mr-2"
+ placeholder={i18n.t("reason")}
+ value={this.state.banReason}
+ onInput={linkEvent(this, this.handleModBanReasonChange)}
+ />
+ <label className="col-form-label" htmlFor={`mod-ban-expires`}>
+ {i18n.t("expires")}
+ </label>
+ <input
+ type="number"
+ id={`mod-ban-expires`}
+ className="form-control mr-2"
+ placeholder={i18n.t("number_of_days")}
+ value={this.state.banExpireDays}
+ onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
+ />
+ <div className="form-group">
+ <div className="form-check">
+ <input
+ className="form-check-input"
+ id="mod-ban-remove-data"
+ type="checkbox"
+ checked={this.state.removeData}
+ onChange={linkEvent(this, this.handleModRemoveDataChange)}
+ />
+ <label
+ className="form-check-label"
+ htmlFor="mod-ban-remove-data"
+ title={i18n.t("remove_content_more")}
+ >
+ {i18n.t("remove_content")}
+ </label>
+ </div>
</div>
</div>
- )}
- </div>
+ {/* TODO hold off on expires until later */}
+ {/* <div class="form-group row"> */}
+ {/* <label class="col-form-label">Expires</label> */}
+ {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
+ {/* </div> */}
+ <div className="form-group row">
+ <button
+ type="reset"
+ className="btn btn-secondary mr-2"
+ aria-label={i18n.t("cancel")}
+ onClick={linkEvent(this, this.handleModBanSubmitCancel)}
+ >
+ {i18n.t("cancel")}
+ </button>
+ <button
+ type="submit"
+ className="btn btn-secondary"
+ aria-label={i18n.t("ban")}
+ >
+ {i18n.t("ban")} {pv.person.name}
+ </button>
+ </div>
+ </form>
+ )
);
}
- 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;
+ async updateUrl({ page, sort, view }: Partial<ProfileProps>) {
+ const {
+ page: urlPage,
+ sort: urlSort,
+ view: urlView,
+ } = getProfileQueryParams();
- let typeView = `/u/${this.state.userName}`;
+ const queryParams: QueryParams<ProfileProps> = {
+ page: (page ?? urlPage).toString(),
+ sort: sort ?? urlSort,
+ view: view ?? urlView,
+ };
- this.props.history.push(
- `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
- );
- this.state.loading = true;
- this.setState(this.state);
- this.fetchUserData();
+ const { username } = this.props.match.params;
+
+ this.props.history.push(`/u/${username}${getQueryString(queryParams)}`);
+ await this.fetchUserData();
}
handlePageChange(page: number) {
this.updateUrl({ page });
}
- handleSortChange(val: SortType) {
- this.updateUrl({ sort: val, page: 1 });
+ handleSortChange(sort: SortType) {
+ this.updateUrl({ sort, page: 1 });
}
handleViewChange(i: Profile, event: any) {
i.updateUrl({
- view: PersonDetailsView[Number(event.target.value)],
+ view: PersonDetailsView[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.setPersonBlock();
- 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"));
+ handleModBanShow(i: Profile) {
+ i.setState({ showBanDialog: true });
+ }
+
+ handleModBanReasonChange(i: Profile, event: any) {
+ i.setState({ banReason: event.target.value });
+ }
+
+ handleModBanExpireDaysChange(i: Profile, event: any) {
+ i.setState({ banExpireDays: event.target.value });
+ }
+
+ handleModRemoveDataChange(i: Profile, event: any) {
+ i.setState({ removeData: event.target.checked });
+ }
+
+ handleModBanSubmitCancel(i: Profile) {
+ i.setState({ showBanDialog: false });
+ }
+
+ async handleModBanSubmit(i: Profile, event: any) {
+ event.preventDefault();
+ const { removeData, banReason, banExpireDays } = i.state;
+
+ const personRes = i.state.personRes;
+
+ if (personRes.state == "success") {
+ const person = personRes.data.person_view.person;
+ const ban = !person.banned;
+
+ // If its an unban, restore all their data
+ if (!ban) {
+ i.setState({ removeData: false });
}
- } 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);
- this.setPersonBlock();
- this.setState(this.state);
+
+ const res = await HttpService.client.banPerson({
+ person_id: person.id,
+ ban,
+ remove_data: removeData,
+ reason: banReason,
+ expires: futureDaysToUnixTime(banExpireDays),
+ auth: myAuthRequired(),
+ });
+ // TODO
+ this.updateBan(res);
+ i.setState({ showBanDialog: false });
}
}
+
+ async toggleBlockPerson(recipientId: number, block: boolean) {
+ const res = await HttpService.client.blockPerson({
+ person_id: recipientId,
+ block,
+ auth: myAuthRequired(),
+ });
+ if (res.state == "success") {
+ updatePersonBlock(res.data);
+ }
+ }
+
+ handleUnblockPerson(personId: number) {
+ this.toggleBlockPerson(personId, false);
+ }
+
+ handleBlockPerson(personId: number) {
+ this.toggleBlockPerson(personId, true);
+ }
+
+ async handleAddModToCommunity(form: AddModToCommunity) {
+ // TODO not sure what to do here
+ await HttpService.client.addModToCommunity(form);
+ }
+
+ async handlePurgePerson(form: PurgePerson) {
+ const purgePersonRes = await HttpService.client.purgePerson(form);
+ this.purgeItem(purgePersonRes);
+ }
+
+ async handlePurgeComment(form: PurgeComment) {
+ const purgeCommentRes = await HttpService.client.purgeComment(form);
+ this.purgeItem(purgeCommentRes);
+ }
+
+ async handlePurgePost(form: PurgePost) {
+ const purgeRes = await HttpService.client.purgePost(form);
+ this.purgeItem(purgeRes);
+ }
+
+ async handleBlockPersonAlt(form: BlockPerson) {
+ const blockPersonRes = await HttpService.client.blockPerson(form);
+ if (blockPersonRes.state === "success") {
+ updatePersonBlock(blockPersonRes.data);
+ }
+ }
+
+ async handleCreateComment(form: CreateComment) {
+ const createCommentRes = await HttpService.client.createComment(form);
+ this.createAndUpdateComments(createCommentRes);
+
+ return createCommentRes;
+ }
+
+ async handleEditComment(form: EditComment) {
+ const editCommentRes = await HttpService.client.editComment(form);
+ this.findAndUpdateComment(editCommentRes);
+
+ return editCommentRes;
+ }
+
+ async handleDeleteComment(form: DeleteComment) {
+ const deleteCommentRes = await HttpService.client.deleteComment(form);
+ this.findAndUpdateComment(deleteCommentRes);
+ }
+
+ async handleDeletePost(form: DeletePost) {
+ const deleteRes = await HttpService.client.deletePost(form);
+ this.findAndUpdatePost(deleteRes);
+ }
+
+ async handleRemovePost(form: RemovePost) {
+ const removeRes = await HttpService.client.removePost(form);
+ this.findAndUpdatePost(removeRes);
+ }
+
+ async handleRemoveComment(form: RemoveComment) {
+ const removeCommentRes = await HttpService.client.removeComment(form);
+ this.findAndUpdateComment(removeCommentRes);
+ }
+
+ async handleSaveComment(form: SaveComment) {
+ const saveCommentRes = await HttpService.client.saveComment(form);
+ this.findAndUpdateComment(saveCommentRes);
+ }
+
+ async handleSavePost(form: SavePost) {
+ const saveRes = await HttpService.client.savePost(form);
+ this.findAndUpdatePost(saveRes);
+ }
+
+ async handleFeaturePost(form: FeaturePost) {
+ const featureRes = await HttpService.client.featurePost(form);
+ this.findAndUpdatePost(featureRes);
+ }
+
+ async handleCommentVote(form: CreateCommentLike) {
+ const voteRes = await HttpService.client.likeComment(form);
+ this.findAndUpdateComment(voteRes);
+ }
+
+ async handlePostVote(form: CreatePostLike) {
+ const voteRes = await HttpService.client.likePost(form);
+ this.findAndUpdatePost(voteRes);
+ }
+
+ async handlePostEdit(form: EditPost) {
+ const res = await HttpService.client.editPost(form);
+ this.findAndUpdatePost(res);
+ }
+
+ async handleCommentReport(form: CreateCommentReport) {
+ const reportRes = await HttpService.client.createCommentReport(form);
+ if (reportRes.state === "success") {
+ toast(i18n.t("report_created"));
+ }
+ }
+
+ async handlePostReport(form: CreatePostReport) {
+ const reportRes = await HttpService.client.createPostReport(form);
+ if (reportRes.state === "success") {
+ toast(i18n.t("report_created"));
+ }
+ }
+
+ async handleLockPost(form: LockPost) {
+ const lockRes = await HttpService.client.lockPost(form);
+ this.findAndUpdatePost(lockRes);
+ }
+
+ async handleDistinguishComment(form: DistinguishComment) {
+ const distinguishRes = await HttpService.client.distinguishComment(form);
+ this.findAndUpdateComment(distinguishRes);
+ }
+
+ async handleAddAdmin(form: AddAdmin) {
+ const addAdminRes = await HttpService.client.addAdmin(form);
+
+ if (addAdminRes.state == "success") {
+ this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
+ }
+ }
+
+ async handleTransferCommunity(form: TransferCommunity) {
+ await HttpService.client.transferCommunity(form);
+ toast(i18n.t("transfer_community"));
+ }
+
+ async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
+ const readRes = await HttpService.client.markCommentReplyAsRead(form);
+ this.findAndUpdateCommentReply(readRes);
+ }
+
+ async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
+ // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it.
+ await HttpService.client.markPersonMentionAsRead(form);
+ }
+
+ async handleBanFromCommunity(form: BanFromCommunity) {
+ const banRes = await HttpService.client.banFromCommunity(form);
+ this.updateBanFromCommunity(banRes);
+ }
+
+ async handleBanPerson(form: BanPerson) {
+ const banRes = await HttpService.client.banPerson(form);
+ this.updateBan(banRes);
+ }
+
+ updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
+ // Maybe not necessary
+ if (banRes.state === "success") {
+ this.setState(s => {
+ if (s.personRes.state == "success") {
+ s.personRes.data.posts
+ .filter(c => c.creator.id === banRes.data.person_view.person.id)
+ .forEach(
+ c => (c.creator_banned_from_community = banRes.data.banned)
+ );
+
+ s.personRes.data.comments
+ .filter(c => c.creator.id === banRes.data.person_view.person.id)
+ .forEach(
+ c => (c.creator_banned_from_community = banRes.data.banned)
+ );
+ }
+ return s;
+ });
+ }
+ }
+
+ updateBan(banRes: RequestState<BanPersonResponse>) {
+ // Maybe not necessary
+ if (banRes.state == "success") {
+ this.setState(s => {
+ if (s.personRes.state == "success") {
+ s.personRes.data.posts
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(c => (c.creator.banned = banRes.data.banned));
+ s.personRes.data.comments
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(c => (c.creator.banned = banRes.data.banned));
+ }
+ return s;
+ });
+ }
+ }
+
+ purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
+ if (purgeRes.state == "success") {
+ toast(i18n.t("purge_success"));
+ this.context.router.history.push(`/`);
+ }
+ }
+
+ findAndUpdateComment(res: RequestState<CommentResponse>) {
+ this.setState(s => {
+ if (s.personRes.state == "success" && res.state == "success") {
+ s.personRes.data.comments = editComment(
+ res.data.comment_view,
+ s.personRes.data.comments
+ );
+ s.finished.set(res.data.comment_view.comment.id, true);
+ }
+ return s;
+ });
+ }
+
+ createAndUpdateComments(res: RequestState<CommentResponse>) {
+ this.setState(s => {
+ if (s.personRes.state == "success" && res.state == "success") {
+ s.personRes.data.comments.unshift(res.data.comment_view);
+ // Set finished for the parent
+ s.finished.set(
+ getCommentParentId(res.data.comment_view.comment) ?? 0,
+ true
+ );
+ }
+ return s;
+ });
+ }
+
+ findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
+ this.setState(s => {
+ if (s.personRes.state == "success" && res.state == "success") {
+ s.personRes.data.comments = editWith(
+ res.data.comment_reply_view,
+ s.personRes.data.comments
+ );
+ }
+ return s;
+ });
+ }
+
+ findAndUpdatePost(res: RequestState<PostResponse>) {
+ this.setState(s => {
+ if (s.personRes.state == "success" && res.state == "success") {
+ s.personRes.data.posts = editPost(
+ res.data.post_view,
+ s.personRes.data.posts
+ );
+ }
+ return s;
+ });
+ }
}