import { editComment, editPost, editWith, enableDownvotes, enableNsfw, getCommentParentId, myAuth, myAuthRequired, setIsoData, updatePersonBlock, } from "@utils/app"; import { restoreScrollPosition, saveScrollPosition } from "@utils/browser"; import { capitalizeFirstLetter, futureDaysToUnixTime, getPageFromString, getQueryParams, getQueryString, numToSI, } from "@utils/helpers"; import { canMod, isAdmin, isBanned } from "@utils/roles"; import type { QueryParams } from "@utils/types"; import { RouteDataResponse } from "@utils/types"; import classNames from "classnames"; import format from "date-fns/format"; import parseISO from "date-fns/parseISO"; import { NoOptionI18nKeys } from "i18next"; import { Component, linkEvent } from "inferno"; import { Link } from "inferno-router"; import { RouteComponentProps } from "inferno-router/dist/Route"; import { AddAdmin, AddModToCommunity, BanFromCommunity, BanFromCommunityResponse, BanPerson, BanPersonResponse, BlockPerson, 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, TransferCommunity, } from "lemmy-js-client"; import { fetchLimit, relTags } from "../../config"; import { InitialFetchRequest, PersonDetailsView } from "../../interfaces"; import { mdToHtml } from "../../markdown"; import { FirstLoadService, I18NextService, UserService } from "../../services"; import { HttpService, RequestState } from "../../services/HttpService"; import { setupTippy } from "../../tippy"; import { toast } from "../../toast"; 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 { UserBadges } from "../common/user-badges"; import { CommunityLink } from "../community/community-link"; import { PersonDetails } from "./person-details"; import { PersonListing } from "./person-listing"; type ProfileData = RouteDataResponse<{ personResponse: GetPersonDetailsResponse; }>; interface ProfileState { personRes: RequestState; personBlocked: boolean; banReason?: string; banExpireDays?: number; showBanDialog: boolean; removeData: boolean; siteRes: GetSiteResponse; finished: Map; isIsomorphic: boolean; } interface ProfileProps { view: PersonDetailsView; sort: SortType; page: number; } function getProfileQueryParams() { return getQueryParams({ 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; } const getCommunitiesListing = ( translationKey: NoOptionI18nKeys, communityViews?: { community: Community }[] ) => communityViews && communityViews.length > 0 && (
{I18NextService.i18n.t(translationKey)}
    {communityViews.map(({ community }) => (
  • ))}
); 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); state: ProfileState = { personRes: { state: "empty" }, personBlocked: false, siteRes: this.isoData.site_res, showBanDialog: false, removeData: false, finished: new Map(), isIsomorphic: false, }; constructor(props: RouteComponentProps<{ username: string }>, context: any) { super(props, context); this.handleSortChange = this.handleSortChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this); 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); this.handleModBanSubmit = this.handleModBanSubmit.bind(this); // Only fetch the data if coming from another route if (FirstLoadService.isFirstLoad) { this.state = { ...this.state, personRes: this.isoData.routeData.personResponse, isIsomorphic: true, }; } } async componentDidMount() { if (!this.state.isIsomorphic) { await this.fetchUserData(); } setupTippy(); } componentWillUnmount() { saveScrollPosition(this.context); } async fetchUserData() { const { page, sort, view } = getProfileQueryParams(); 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(); } 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; } } 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 async fetchInitialData({ client, path, query: { page, sort, view: urlView }, auth, }: InitialFetchRequest>): Promise { const pathSplit = path.split("/"); const username = pathSplit[2]; const view = getViewFromProps(urlView); const form: GetPersonDetails = { username: username, sort: getSortTypeFromQuery(sort), saved_only: view === PersonDetailsView.Saved, page: getPageFromString(page), limit: fetchLimit, auth, }; return { personResponse: await client.getPersonDetails(form), }; } 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; } renderPersonRes() { switch (this.state.personRes.state) { case "loading": return (
); case "success": { const siteRes = this.state.siteRes; const personRes = this.state.personRes.data; const { page, sort, view } = getProfileQueryParams(); return (
{this.userInfo(personRes.person_view)}
{this.selects}
{this.amCurrentUser && }
); } } } render() { return (
{this.renderPersonRes()}
); } get viewRadios() { return (
{this.getRadio(PersonDetailsView.Overview)} {this.getRadio(PersonDetailsView.Comments)} {this.getRadio(PersonDetailsView.Posts)} {this.amCurrentUser && this.getRadio(PersonDetailsView.Saved)}
); } getRadio(view: PersonDetailsView) { const { view: urlView } = getProfileQueryParams(); const active = view === urlView; return ( ); } get selects() { const { sort } = getProfileQueryParams(); const { username } = this.props.match.params; const profileRss = `/feeds/u/${username}.xml?sort=${sort}`; return (
{this.viewRadios}
); } userInfo(pv: PersonView) { const { personBlocked, siteRes: { admins }, showBanDialog, } = this.state; return ( pv && (
{!isBanned(pv.person) && ( )}
{pv.person.display_name && (
{pv.person.display_name}
)}
{this.banDialog(pv)}
{!this.amCurrentUser && UserService.Instance.myUserInfo && ( <> {I18NextService.i18n.t("send_secure_message")} {I18NextService.i18n.t("send_message")} {personBlocked ? ( ) : ( )} )} {canMod(pv.person.id, undefined, admins) && !isAdmin(pv.person.id, admins) && !showBanDialog && (!isBanned(pv.person) ? ( ) : ( ))}
{pv.person.bio && (
)}
  • {I18NextService.i18n.t("number_of_posts", { count: Number(pv.counts.post_count), formattedCount: numToSI(pv.counts.post_count), })}
  • {I18NextService.i18n.t("number_of_comments", { count: Number(pv.counts.comment_count), formattedCount: numToSI(pv.counts.comment_count), })}
{I18NextService.i18n.t("joined")}{" "}
{I18NextService.i18n.t("cake_day_title")}{" "} {format(parseISO(pv.person.published), "PPP")}
{!UserService.Instance.myUserInfo && (
{I18NextService.i18n.t("profile_not_logged_in_alert")}
)}
) ); } banDialog(pv: PersonView) { const { showBanDialog } = this.state; return ( showBanDialog && (
{/* TODO hold off on expires until later */} {/*
*/} {/* */} {/* */} {/*
*/}
) ); } async updateUrl({ page, sort, view }: Partial) { const { page: urlPage, sort: urlSort, view: urlView, } = getProfileQueryParams(); const queryParams: QueryParams = { page: (page ?? urlPage).toString(), sort: sort ?? urlSort, view: view ?? urlView, }; const { username } = this.props.match.params; this.props.history.push(`/u/${username}${getQueryString(queryParams)}`); await this.fetchUserData(); } handlePageChange(page: number) { this.updateUrl({ page }); } handleSortChange(sort: SortType) { this.updateUrl({ sort, page: 1 }); } handleViewChange(i: Profile, event: any) { i.updateUrl({ view: PersonDetailsView[event.target.value], page: 1, }); } 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 }); } 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(I18NextService.i18n.t("report_created")); } } async handlePostReport(form: CreatePostReport) { const reportRes = await HttpService.client.createPostReport(form); if (reportRes.state === "success") { toast(I18NextService.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(I18NextService.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) { // 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) { // 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)); s.personRes.data.person_view.person.banned = banRes.data.banned; } return s; }); } } purgeItem(purgeRes: RequestState) { if (purgeRes.state == "success") { toast(I18NextService.i18n.t("purge_success")); this.context.router.history.push(`/`); } } findAndUpdateComment(res: RequestState) { 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) { 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) { 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) { 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; }); } }