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, BanPerson, BanPersonResponse, BlockPerson, BlockPersonResponse, CommentResponse, Community, CommunityModeratorView, GetPersonDetails, GetPersonDetailsResponse, GetSiteResponse, PostResponse, PurgeItemResponse, SortType, UserOperation, wsJsonToRes, wsUserOp, } 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 { QueryParams, canMod, capitalizeFirstLetter, createCommentLikeRes, createPostLikeFindRes, editCommentRes, editPostFindRes, enableDownvotes, enableNsfw, fetchLimit, futureDaysToUnixTime, getPageFromString, getQueryParams, getQueryString, isAdmin, isBanned, mdToHtml, myAuth, numToSI, relTags, restoreScrollPosition, saveCommentRes, saveScrollPosition, setIsoData, setupTippy, toast, updatePersonBlock, wsClient, wsSubscribe, } from "../../utils"; import { BannerIconHeader } from "../common/banner-icon-header"; import { HtmlTags } from "../common/html-tags"; import { Icon, Spinner } from "../common/icon"; import { MomentTime } from "../common/moment-time"; import { SortSelect } from "../common/sort-select"; import { CommunityLink } from "../community/community-link"; import { PersonDetails } from "./person-details"; import { PersonListing } from "./person-listing"; interface ProfileState { personRes?: GetPersonDetailsResponse; loading: boolean; personBlocked: boolean; banReason?: string; banExpireDays?: number; showBanDialog: boolean; removeData: boolean; siteRes: GetSiteResponse; } 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; } function toggleBlockPerson(recipientId: number, block: boolean) { const auth = myAuth(); if (auth) { const blockUserForm: BlockPerson = { person_id: recipientId, block, auth, }; WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm)); } } const handleUnblockPerson = (personId: number) => toggleBlockPerson(personId, false); const handleBlockPerson = (personId: number) => toggleBlockPerson(personId, true); const getCommunitiesListing = ( translationKey: NoOptionI18nKeys, communityViews?: { community: Community }[] ) => communityViews && communityViews.length > 0 && (
{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); private subscription?: Subscription; state: ProfileState = { loading: true, personBlocked: false, siteRes: this.isoData.site_res, showBanDialog: false, removeData: false, }; 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); // Only fetch the data if coming from another route if (this.isoData.path === this.context.router.route.match.url) { this.state = { ...this.state, personRes: this.isoData.routeData[0] as GetPersonDetailsResponse, loading: false, }; } else { this.fetchUserData(); } } fetchUserData() { const { page, sort, view } = getProfileQueryParams(); const form: GetPersonDetails = { username: this.props.match.params.username, sort, saved_only: view === PersonDetailsView.Saved, page, limit: fetchLimit, auth: myAuth(false), }; WebSocketService.Instance.send(wsClient.getPersonDetails(form)); } get amCurrentUser() { return ( UserService.Instance.myUserInfo?.local_user_view.person.id === this.state.personRes?.person_view.person.id ); } setPersonBlock() { const mui = UserService.Instance.myUserInfo; const res = this.state.personRes; if (mui && res) { this.setState({ personBlocked: mui.person_blocks.some( ({ target: { id } }) => id === res.person_view.person.id ), }); } } static 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 [client.getPersonDetails(form)]; } componentDidMount() { this.setPersonBlock(); setupTippy(); } componentWillUnmount() { this.subscription?.unsubscribe(); saveScrollPosition(this.context); } get documentTitle(): string { const res = this.state.personRes; return res ? `@${res.person_view.person.name} - ${this.state.siteRes.site_view.site.name}` : ""; } render() { const { personRes, loading, siteRes } = this.state; const { page, sort, view } = getProfileQueryParams(); return (
{loading ? (
) : ( personRes && (
{this.userInfo}
{this.selects}
{this.amCurrentUser && }
) )}
); } 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}
); } get userInfo() { const pv = this.state.personRes?.person_view; const { personBlocked, siteRes: { admins }, showBanDialog, } = this.state; return ( pv && (
{!isBanned(pv.person) && ( )}
{pv.person.display_name && (
{pv.person.display_name}
)}
  • {isBanned(pv.person) && (
  • {i18n.t("banned")}
  • )} {pv.person.deleted && (
  • {i18n.t("deleted")}
  • )} {pv.person.admin && (
  • {i18n.t("admin")}
  • )} {pv.person.bot_account && (
  • {i18n.t("bot_account").toLowerCase()}
  • )}
{this.banDialog}
{!this.amCurrentUser && UserService.Instance.myUserInfo && ( <> {i18n.t("send_secure_message")} {i18n.t("send_message")} {personBlocked ? ( ) : ( )} )} {canMod(pv.person.id, undefined, admins) && !isAdmin(pv.person.id, admins) && !showBanDialog && (!isBanned(pv.person) ? ( ) : ( ))}
{pv.person.bio && (
)}
  • {i18n.t("number_of_posts", { count: Number(pv.counts.post_count), formattedCount: numToSI(pv.counts.post_count), })}
  • {i18n.t("number_of_comments", { count: Number(pv.counts.comment_count), formattedCount: numToSI(pv.counts.comment_count), })}
{i18n.t("joined")}{" "}
{i18n.t("cake_day_title")}{" "} {moment .utc(pv.person.published) .local() .format("MMM DD, YYYY")}
{!UserService.Instance.myUserInfo && (
{i18n.t("profile_not_logged_in_alert")}
)}
) ); } get banDialog() { const pv = this.state.personRes?.person_view; const { showBanDialog } = this.state; return ( pv && ( <> {showBanDialog && (
{/* TODO hold off on expires until later */} {/*
*/} {/* */} {/* */} {/*
*/}
)} ) ); } 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)}`); this.setState({ loading: true }); 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, event?: any) { event.preventDefault(); i.setState({ showBanDialog: false }); } handleModBanSubmit(i: Profile, event?: any) { if (event) event.preventDefault(); const { personRes, removeData, banReason, banExpireDays } = i.state; const person = personRes?.person_view.person; const auth = myAuth(); if (person && auth) { const ban = !person.banned; // If its an unban, restore all their data if (!ban) { i.setState({ removeData: false }); } const form: BanPerson = { person_id: person.id, ban, remove_data: removeData, reason: banReason, expires: futureDaysToUnixTime(banExpireDays), auth, }; WebSocketService.Instance.send(wsClient.banPerson(form)); i.setState({ showBanDialog: false }); } } parseMessage(msg: any) { const 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("/"); } } else if (msg.reconnect) { this.fetchUserData(); } else { switch (op) { case 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 const data = wsJsonToRes(msg); this.setState({ personRes: data, loading: false }); this.setPersonBlock(); restoreScrollPosition(this.context); break; } case UserOperation.AddAdmin: { const { admins } = wsJsonToRes(msg); this.setState(s => ((s.siteRes.admins = admins), s)); break; } case UserOperation.CreateCommentLike: { const { comment_view } = wsJsonToRes(msg); createCommentLikeRes(comment_view, this.state.personRes?.comments); this.setState(this.state); break; } case UserOperation.EditComment: case UserOperation.DeleteComment: case UserOperation.RemoveComment: { const { comment_view } = wsJsonToRes(msg); editCommentRes(comment_view, this.state.personRes?.comments); this.setState(this.state); break; } case UserOperation.CreateComment: { const { comment_view: { creator: { id }, }, } = wsJsonToRes(msg); const mui = UserService.Instance.myUserInfo; if (id === mui?.local_user_view.person.id) { toast(i18n.t("reply_sent")); } break; } case UserOperation.SaveComment: { const { comment_view } = wsJsonToRes(msg); saveCommentRes(comment_view, this.state.personRes?.comments); this.setState(this.state); break; } case UserOperation.EditPost: case UserOperation.DeletePost: case UserOperation.RemovePost: case UserOperation.LockPost: case UserOperation.FeaturePost: case UserOperation.SavePost: { const { post_view } = wsJsonToRes(msg); editPostFindRes(post_view, this.state.personRes?.posts); this.setState(this.state); break; } case UserOperation.CreatePostLike: { const { post_view } = wsJsonToRes(msg); createPostLikeFindRes(post_view, this.state.personRes?.posts); this.setState(this.state); break; } case UserOperation.BanPerson: { const data = wsJsonToRes(msg); const 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)); const pv = res?.person_view; if (pv?.person.id === data.person_view.person.id) { pv.person.banned = data.banned; } this.setState(this.state); break; } case UserOperation.BlockPerson: { const data = wsJsonToRes(msg); updatePersonBlock(data); this.setPersonBlock(); break; } case UserOperation.PurgePerson: case UserOperation.PurgePost: case UserOperation.PurgeComment: case UserOperation.PurgeCommunity: { const { success } = wsJsonToRes(msg); if (success) { toast(i18n.t("purge_success")); this.context.router.history.push(`/`); } } } } } }