+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,
- wsJsonToRes,
- wsUserOp,
+ 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 {
+ QueryParams,
canMod,
capitalizeFirstLetter,
- createCommentLikeRes,
- createPostLikeFindRes,
- editCommentRes,
- editPostFindRes,
+ editComment,
+ editPost,
+ editWith,
enableDownvotes,
enableNsfw,
fetchLimit,
futureDaysToUnixTime,
- getUsernameFromProps,
+ getCommentParentId,
+ getPageFromString,
+ getQueryParams,
+ getQueryString,
isAdmin,
isBanned,
mdToHtml,
myAuth,
+ myAuthRequired,
numToSI,
relTags,
restoreScrollPosition,
- routeSortTypeToEnum,
- saveCommentRes,
saveScrollPosition,
setIsoData,
setupTippy,
toast,
updatePersonBlock,
- wsClient,
- wsSubscribe,
} from "../../utils";
import { BannerIconHeader } from "../common/banner-icon-header";
import { HtmlTags } from "../common/html-tags";
import { PersonListing } from "./person-listing";
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;
- 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> {
+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(this.context);
- private subscription?: Subscription;
state: ProfileState = {
- 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),
+ 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.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) {
+ if (FirstLoadService.isFirstLoad) {
this.state = {
...this.state,
- personRes: this.isoData.routeData[0] as GetPersonDetailsResponse,
- loading: false,
+ personRes: this.isoData.routeData[0],
+ isIsomorphic: true,
};
- } else {
- this.fetchUserData();
}
}
- 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: myAuth(false),
- };
- WebSocketService.Instance.send(wsClient.getPersonDetails(form));
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await this.fetchUserData();
+ }
+ setupTippy();
}
- get amCurrentUser() {
- return (
- UserService.Instance.myUserInfo?.local_user_view.person.id ==
- this.state.personRes?.person_view.person.id
- );
+ componentWillUnmount() {
+ saveScrollPosition(this.context);
}
- setPersonBlock() {
- let mui = UserService.Instance.myUserInfo;
- let res = this.state.personRes;
- if (mui && res) {
- this.setState({
- personBlocked: mui.person_blocks
- .map(a => a.target.id)
- .includes(res.person_view.person.id),
- });
- }
- }
+ async fetchUserData() {
+ const { page, sort, view } = getProfileQueryParams();
- static getViewFromProps(view: string): PersonDetailsView {
- return view ? PersonDetailsView[view] : PersonDetailsView.Overview;
+ 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 getSortTypeFromProps(sort: string): SortType {
- return sort ? routeSortTypeToEnum(sort) : SortType.New;
+ 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 getPageFromProps(page: number): number {
- return page ? Number(page) : 1;
+ setPersonBlock() {
+ const mui = UserService.Instance.myUserInfo;
+ const res = this.state.personRes;
+
+ 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("/");
+ static fetchInitialData({
+ client,
+ path,
+ query: { page, sort, view: urlView },
+ auth,
+ }: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<
+ RequestState<any>
+ >[] {
+ const pathSplit = path.split("/");
- let username = pathSplit[2];
- 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 = {
+ const form: GetPersonDetails = {
username: username,
- sort,
+ sort: getSortTypeFromQuery(sort),
saved_only: view === PersonDetailsView.Saved,
- page,
+ page: getPageFromString(page),
limit: fetchLimit,
- auth: req.auth,
+ auth,
};
- return [req.client.getPersonDetails(form)];
- }
- componentDidMount() {
- this.setPersonBlock();
- setupTippy();
+ return [client.getPersonDetails(form)];
}
- componentWillUnmount() {
- this.subscription?.unsubscribe();
- saveScrollPosition(this.context);
+ get documentTitle(): string {
+ 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;
}
- 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),
- username: props.match.params.username,
- };
- }
+ renderPersonRes() {
+ switch (this.state.personRes.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ 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}
+ />
- 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();
+ {this.userInfo(personRes.person_view)}
+
+ <hr />
+
+ {this.selects}
+
+ <PersonDetails
+ personRes={personRes}
+ admins={siteRes.admins}
+ sort={sort}
+ page={page}
+ limit={fetchLimit}
+ 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>
+
+ <div className="col-12 col-md-4">
+ <Moderates moderates={personRes.moderates} />
+ {this.amCurrentUser && <Follows />}
+ </div>
+ </div>
+ );
+ }
}
}
- get documentTitle(): string {
- let res = this.state.personRes;
- return res
- ? `@${res.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`
- : "";
+ render() {
+ return <div className="container-lg">{this.renderPersonRes()}</div>;
}
- render() {
- let res = this.state.personRes;
+ get viewRadios() {
return (
- <div className="container-lg">
- {this.state.loading ? (
- <h5>
- <Spinner large />
- </h5>
- ) : (
- res && (
- <div className="row">
- <div className="col-12 col-md-8">
- <>
- <HtmlTags
- title={this.documentTitle}
- path={this.context.router.route.match.url}
- description={res.person_view.person.bio}
- image={res.person_view.person.avatar}
- />
- {this.userInfo()}
- <hr />
- </>
- {!this.state.loading && this.selects()}
- <PersonDetails
- personRes={res}
- admins={this.state.siteRes.admins}
- sort={this.state.sort}
- page={this.state.page}
- limit={fetchLimit}
- enableDownvotes={enableDownvotes(this.state.siteRes)}
- enableNsfw={enableNsfw(this.state.siteRes)}
- view={this.state.view}
- onPageChange={this.handlePageChange}
- allLanguages={this.state.siteRes.all_languages}
- siteLanguages={this.state.siteRes.discussion_languages}
- />
- </div>
-
- {!this.state.loading && (
- <div className="col-12 col-md-4">
- {this.moderates()}
- {this.amCurrentUser && this.follows()}
- </div>
- )}
- </div>
- )
- )}
+ <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 className="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() {
- let profileRss = `/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`;
+ 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 className="mr-3">{this.viewRadios()}</span>
+ <span className="mr-3">{this.viewRadios}</span>
<SortSelect
- sort={this.state.sort}
+ sort={sort}
onChange={this.handleSortChange}
hideHot
hideMostComments
</div>
);
}
- handleBlockPerson(personId: number) {
- let auth = myAuth();
- if (auth) {
- if (personId != 0) {
- let blockUserForm: BlockPerson = {
- person_id: personId,
- block: true,
- auth,
- };
- WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
- }
- }
- }
- handleUnblockPerson(recipientId: number) {
- let auth = myAuth();
- if (auth) {
- let blockUserForm: BlockPerson = {
- person_id: recipientId,
- block: false,
- auth,
- };
- 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 (
pv && (
<div>
)}
</ul>
</div>
- {this.banDialog()}
+ {this.banDialog(pv)}
<div className="flex-grow-1 unselectable pointer mx-2"></div>
{!this.amCurrentUser && UserService.Instance.myUserInfo && (
<>
className={
"d-flex align-self-start btn btn-secondary mr-2"
}
- to={`/create_private_message/recipient/${pv.person.id}`}
+ to={`/create_private_message/${pv.person.id}`}
>
{i18n.t("send_message")}
</Link>
- {this.state.personBlocked ? (
+ {personBlocked ? (
<button
className={
"d-flex align-self-start btn btn-secondary mr-2"
</>
)}
- {canMod(pv.person.id, undefined, this.state.siteRes.admins) &&
- !isAdmin(pv.person.id, this.state.siteRes.admins) &&
- !this.state.showBanDialog &&
+ {canMod(pv.person.id, undefined, admins) &&
+ !isAdmin(pv.person.id, admins) &&
+ !showBanDialog &&
(!isBanned(pv.person) ? (
<button
className={
<ul className="list-inline mb-2">
<li className="list-inline-item badge badge-light">
{i18n.t("number_of_posts", {
- count: pv.counts.post_count,
+ 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: pv.counts.comment_count,
+ count: Number(pv.counts.comment_count),
formattedCount: numToSI(pv.counts.comment_count),
})}
</li>
.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>
);
}
- banDialog() {
- let pv = this.state.personRes?.person_view;
+ banDialog(pv: PersonView) {
+ const { showBanDialog } = this.state;
+
return (
- pv && (
- <>
- {this.state.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>
+ 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
- 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)}
+ className="form-check-input"
+ id="mod-ban-remove-data"
+ type="checkbox"
+ checked={this.state.removeData}
+ onChange={linkEvent(this, this.handleModRemoveDataChange)}
/>
- <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>
- {/* 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")}
+ <label
+ className="form-check-label"
+ htmlFor="mod-ban-remove-data"
+ title={i18n.t("remove_content_more")}
>
- {i18n.t("ban")} {pv.person.name}
- </button>
+ {i18n.t("remove_content")}
+ </label>
</div>
- </form>
- )}
- </>
- )
- );
- }
-
- moderates() {
- let moderates = this.state.personRes?.moderates;
- return (
- moderates &&
- moderates.length > 0 && (
- <div className="card border-secondary mb-3">
- <div className="card-body">
- <h5>{i18n.t("moderates")}</h5>
- <ul className="list-unstyled mb-0">
- {moderates.map(cmv => (
- <li key={cmv.community.id}>
- <CommunityLink community={cmv.community} />
- </li>
- ))}
- </ul>
+ </div>
</div>
- </div>
- )
- );
- }
-
- follows() {
- let follows = UserService.Instance.myUserInfo?.follows;
- return (
- follows &&
- follows.length > 0 && (
- <div className="card border-secondary mb-3">
- <div className="card-body">
- <h5>{i18n.t("subscribed")}</h5>
- <ul className="list-unstyled mb-0">
- {follows.map(cfv => (
- <li key={cfv.community.id}>
- <CommunityLink community={cfv.community} />
- </li>
- ))}
- </ul>
+ {/* 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>
- </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.setState({ loading: true });
- 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: page });
+ 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,
});
}
i.setState({ removeData: event.target.checked });
}
- handleModBanSubmitCancel(i: Profile, event?: any) {
- event.preventDefault();
+ handleModBanSubmitCancel(i: Profile) {
i.setState({ showBanDialog: false });
}
- handleModBanSubmit(i: Profile, event?: any) {
- if (event) event.preventDefault();
- let person = i.state.personRes?.person_view.person;
- let auth = myAuth();
- if (person && auth) {
+ 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
- let ban = !person.banned;
- if (ban == false) {
+ if (!ban) {
i.setState({ removeData: false });
}
- let form: BanPerson = {
+
+ const res = await HttpService.client.banPerson({
person_id: person.id,
ban,
- remove_data: i.state.removeData,
- reason: i.state.banReason,
- expires: futureDaysToUnixTime(i.state.banExpireDays),
- auth,
- };
- WebSocketService.Instance.send(wsClient.banPerson(form));
-
+ remove_data: removeData,
+ reason: banReason,
+ expires: futureDaysToUnixTime(banExpireDays),
+ auth: myAuthRequired(),
+ });
+ // TODO
+ this.updateBan(res);
i.setState({ showBanDialog: false });
}
}
- 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("/");
+ 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;
- } 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);
- this.setState({ personRes: data, loading: false });
- this.setPersonBlock();
- restoreScrollPosition(this.context);
- } else if (op == UserOperation.AddAdmin) {
- let data = wsJsonToRes<AddAdminResponse>(msg);
- this.setState(s => ((s.siteRes.admins = data.admins), s));
- } else if (op == UserOperation.CreateCommentLike) {
- let data = wsJsonToRes<CommentResponse>(msg);
- 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);
- editCommentRes(data.comment_view, this.state.personRes?.comments);
- this.setState(this.state);
- } else if (op == UserOperation.CreateComment) {
- let data = wsJsonToRes<CommentResponse>(msg);
- let mui = UserService.Instance.myUserInfo;
- if (data.comment_view.creator.id == mui?.local_user_view.person.id) {
- toast(i18n.t("reply_sent"));
+ 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
+ );
}
- } else if (op == UserOperation.SaveComment) {
- let data = wsJsonToRes<CommentResponse>(msg);
- 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.FeaturePost ||
- op == UserOperation.SavePost
- ) {
- let data = wsJsonToRes<PostResponse>(msg);
- editPostFindRes(data.post_view, this.state.personRes?.posts);
- this.setState(this.state);
- } else if (op == UserOperation.CreatePostLike) {
- let data = wsJsonToRes<PostResponse>(msg);
- createPostLikeFindRes(data.post_view, this.state.personRes?.posts);
- this.setState(this.state);
- } else if (op == UserOperation.BanPerson) {
- let data = wsJsonToRes<BanPersonResponse>(msg);
- let res = this.state.personRes;
- res?.comments
- .filter(c => c.creator.id == data.person_view.person.id)
- .forEach(c => (c.creator.banned = data.banned));
- res?.posts
- .filter(c => c.creator.id == data.person_view.person.id)
- .forEach(c => (c.creator.banned = data.banned));
- let pv = res?.person_view;
-
- if (pv?.person.id == data.person_view.person.id) {
- pv.person.banned = data.banned;
+ 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
+ );
}
- this.setState(this.state);
- } else if (op == UserOperation.BlockPerson) {
- let data = wsJsonToRes<BlockPersonResponse>(msg);
- updatePersonBlock(data);
- this.setPersonBlock();
- this.setState(this.state);
- } else if (
- op == UserOperation.PurgePerson ||
- op == UserOperation.PurgePost ||
- op == UserOperation.PurgeComment ||
- op == UserOperation.PurgeCommunity
- ) {
- let data = wsJsonToRes<PurgeItemResponse>(msg);
- if (data.success) {
- toast(i18n.t("purge_success"));
- this.context.router.history.push(`/`);
+ 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;
+ });
}
}