1 import { Component, linkEvent } from "inferno";
2 import { Link } from "inferno-router";
10 GetPersonDetailsResponse,
15 } from "lemmy-js-client";
16 import moment from "moment";
17 import { Subscription } from "rxjs";
18 import { i18n } from "../../i18next";
19 import { InitialFetchRequest, PersonDetailsView } from "../../interfaces";
20 import { UserService, WebSocketService } from "../../services";
24 createPostLikeFindRes,
33 restoreScrollPosition,
47 import { BannerIconHeader } from "../common/banner-icon-header";
48 import { HtmlTags } from "../common/html-tags";
49 import { Icon, Spinner } from "../common/icon";
50 import { MomentTime } from "../common/moment-time";
51 import { SortSelect } from "../common/sort-select";
52 import { CommunityLink } from "../community/community-link";
53 import { PersonDetails } from "./person-details";
54 import { PersonListing } from "./person-listing";
56 interface ProfileState {
57 personRes: GetPersonDetailsResponse;
59 view: PersonDetailsView;
63 personBlocked: boolean;
64 siteRes: GetSiteResponse;
67 interface ProfileProps {
68 view: PersonDetailsView;
71 person_id: number | null;
81 export class Profile extends Component<any, ProfileState> {
82 private isoData = setIsoData(this.context);
83 private subscription: Subscription;
84 private emptyState: ProfileState = {
86 userName: getUsernameFromProps(this.props),
88 view: Profile.getViewFromProps(this.props.match.view),
89 sort: Profile.getSortTypeFromProps(this.props.match.sort),
90 page: Profile.getPageFromProps(this.props.match.page),
92 siteRes: this.isoData.site_res,
95 constructor(props: any, context: any) {
96 super(props, context);
98 this.state = this.emptyState;
99 this.handleSortChange = this.handleSortChange.bind(this);
100 this.handlePageChange = this.handlePageChange.bind(this);
102 this.parseMessage = this.parseMessage.bind(this);
103 this.subscription = wsSubscribe(this.parseMessage);
105 // Only fetch the data if coming from another route
106 if (this.isoData.path == this.context.router.route.match.url) {
107 this.state.personRes = this.isoData.routeData[0];
108 this.state.loading = false;
110 this.fetchUserData();
113 this.setPersonBlock();
117 let form: GetPersonDetails = {
118 username: this.state.userName,
119 sort: this.state.sort,
120 saved_only: this.state.view === PersonDetailsView.Saved,
121 page: this.state.page,
123 auth: authField(false),
125 WebSocketService.Instance.send(wsClient.getPersonDetails(form));
128 get isCurrentUser() {
130 UserService.Instance.myUserInfo?.local_user_view.person.id ==
131 this.state.personRes.person_view.person.id
136 this.state.personBlocked = UserService.Instance.myUserInfo?.person_blocks
137 .map(a => a.target.id)
138 .includes(this.state.personRes?.person_view.person.id);
141 static getViewFromProps(view: string): PersonDetailsView {
142 return view ? PersonDetailsView[view] : PersonDetailsView.Overview;
145 static getSortTypeFromProps(sort: string): SortType {
146 return sort ? routeSortTypeToEnum(sort) : SortType.New;
149 static getPageFromProps(page: number): number {
150 return page ? Number(page) : 1;
153 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
154 let pathSplit = req.path.split("/");
155 let promises: Promise<any>[] = [];
157 let username = pathSplit[2];
158 let view = this.getViewFromProps(pathSplit[4]);
159 let sort = this.getSortTypeFromProps(pathSplit[6]);
160 let page = this.getPageFromProps(Number(pathSplit[8]));
162 let form: GetPersonDetails = {
164 saved_only: view === PersonDetailsView.Saved,
169 setOptionalAuth(form, req.auth);
170 promises.push(req.client.getPersonDetails(form));
174 componentDidMount() {
178 componentWillUnmount() {
179 this.subscription.unsubscribe();
180 saveScrollPosition(this.context);
183 static getDerivedStateFromProps(props: any): ProfileProps {
185 view: this.getViewFromProps(props.match.params.view),
186 sort: this.getSortTypeFromProps(props.match.params.sort),
187 page: this.getPageFromProps(props.match.params.page),
188 person_id: Number(props.match.params.id) || null,
189 username: props.match.params.username,
193 componentDidUpdate(lastProps: any) {
194 // Necessary if you are on a post and you click another post (same route)
196 lastProps.location.pathname.split("/")[2] !==
197 lastProps.history.location.pathname.split("/")[2]
199 // Couldnt get a refresh working. This does for now.
204 get documentTitle(): string {
205 return `@${this.state.personRes.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`;
208 get bioTag(): string {
209 return this.state.personRes.person_view.person.bio
210 ? previewLines(this.state.personRes.person_view.person.bio)
216 <div class="container">
217 {this.state.loading ? (
223 <div class="col-12 col-md-8">
226 title={this.documentTitle}
227 path={this.context.router.route.match.url}
228 description={this.bioTag}
229 image={this.state.personRes.person_view.person.avatar}
234 {!this.state.loading && this.selects()}
236 personRes={this.state.personRes}
237 admins={this.state.siteRes.admins}
238 sort={this.state.sort}
239 page={this.state.page}
242 this.state.siteRes.site_view.site.enable_downvotes
244 enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
245 view={this.state.view}
246 onPageChange={this.handlePageChange}
250 {!this.state.loading && (
251 <div class="col-12 col-md-4">
253 {this.isCurrentUser && this.follows()}
264 <div class="btn-group btn-group-toggle flex-wrap mb-2">
266 className={`btn btn-outline-secondary pointer
267 ${this.state.view == PersonDetailsView.Overview && "active"}
272 value={PersonDetailsView.Overview}
273 checked={this.state.view === PersonDetailsView.Overview}
274 onChange={linkEvent(this, this.handleViewChange)}
279 className={`btn btn-outline-secondary pointer
280 ${this.state.view == PersonDetailsView.Comments && "active"}
285 value={PersonDetailsView.Comments}
286 checked={this.state.view == PersonDetailsView.Comments}
287 onChange={linkEvent(this, this.handleViewChange)}
292 className={`btn btn-outline-secondary pointer
293 ${this.state.view == PersonDetailsView.Posts && "active"}
298 value={PersonDetailsView.Posts}
299 checked={this.state.view == PersonDetailsView.Posts}
300 onChange={linkEvent(this, this.handleViewChange)}
305 className={`btn btn-outline-secondary pointer
306 ${this.state.view == PersonDetailsView.Saved && "active"}
311 value={PersonDetailsView.Saved}
312 checked={this.state.view == PersonDetailsView.Saved}
313 onChange={linkEvent(this, this.handleViewChange)}
322 let profileRss = `/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`;
325 <div className="mb-2">
326 <span class="mr-3">{this.viewRadios()}</span>
328 sort={this.state.sort}
329 onChange={this.handleSortChange}
333 <a href={profileRss} rel={relTags} title="RSS">
334 <Icon icon="rss" classes="text-muted small mx-2" />
336 <link rel="alternate" type="application/atom+xml" href={profileRss} />
340 handleBlockPerson(personId: number) {
342 let blockUserForm: BlockPerson = {
347 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
350 handleUnblockPerson(recipientId: number) {
351 let blockUserForm: BlockPerson = {
352 person_id: recipientId,
356 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
360 let pv = this.state.personRes?.person_view;
364 <BannerIconHeader banner={pv.person.banner} icon={pv.person.avatar} />
367 <div class="mb-0 d-flex flex-wrap">
369 {pv.person.display_name && (
370 <h5 class="mb-0">{pv.person.display_name}</h5>
372 <ul class="list-inline mb-2">
373 <li className="list-inline-item">
382 {pv.person.banned && (
383 <li className="list-inline-item badge badge-danger">
387 {pv.person.admin && (
388 <li className="list-inline-item badge badge-light">
392 {pv.person.bot_account && (
393 <li className="list-inline-item badge badge-light">
394 {i18n.t("bot_account").toLowerCase()}
399 <div className="flex-grow-1 unselectable pointer mx-2"></div>
400 {!this.isCurrentUser && UserService.Instance.myUserInfo && (
403 className={`d-flex align-self-start btn btn-secondary mr-2 ${
404 !pv.person.matrix_user_id && "invisible"
407 href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
409 {i18n.t("send_secure_message")}
412 className={"d-flex align-self-start btn btn-secondary mr-2"}
413 to={`/create_private_message/recipient/${pv.person.id}`}
415 {i18n.t("send_message")}
417 {this.state.personBlocked ? (
419 className={"d-flex align-self-start btn btn-secondary"}
422 this.handleUnblockPerson
425 {i18n.t("unblock_user")}
429 className={"d-flex align-self-start btn btn-secondary"}
430 onClick={linkEvent(pv.person.id, this.handleBlockPerson)}
432 {i18n.t("block_user")}
439 <div className="d-flex align-items-center mb-2">
442 dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
447 <ul class="list-inline mb-2">
448 <li className="list-inline-item badge badge-light">
449 {i18n.t("number_of_posts", {
450 count: pv.counts.post_count,
451 formattedCount: numToSI(pv.counts.post_count),
454 <li className="list-inline-item badge badge-light">
455 {i18n.t("number_of_comments", {
456 count: pv.counts.comment_count,
457 formattedCount: numToSI(pv.counts.comment_count),
462 <div class="text-muted">
463 {i18n.t("joined")}{" "}
464 <MomentTime data={pv.person} showAgo ignoreUpdated />
466 <div className="d-flex align-items-center text-muted mb-2">
468 <span className="ml-2">
469 {i18n.t("cake_day_title")}{" "}
470 {moment.utc(pv.person.published).local().format("MMM DD, YYYY")}
482 {this.state.personRes.moderates.length > 0 && (
483 <div class="card border-secondary mb-3">
484 <div class="card-body">
485 <h5>{i18n.t("moderates")}</h5>
486 <ul class="list-unstyled mb-0">
487 {this.state.personRes.moderates.map(cmv => (
489 <CommunityLink community={cmv.community} />
501 let follows = UserService.Instance.myUserInfo.follows;
504 {follows.length > 0 && (
505 <div class="card border-secondary mb-3">
506 <div class="card-body">
507 <h5>{i18n.t("subscribed")}</h5>
508 <ul class="list-unstyled mb-0">
509 {follows.map(cfv => (
511 <CommunityLink community={cfv.community} />
522 updateUrl(paramUpdates: UrlParams) {
523 const page = paramUpdates.page || this.state.page;
524 const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
525 const sortStr = paramUpdates.sort || this.state.sort;
527 let typeView = `/u/${this.state.userName}`;
529 this.props.history.push(
530 `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
532 this.state.loading = true;
533 this.setState(this.state);
534 this.fetchUserData();
537 handlePageChange(page: number) {
538 this.updateUrl({ page });
541 handleSortChange(val: SortType) {
542 this.updateUrl({ sort: val, page: 1 });
545 handleViewChange(i: Profile, event: any) {
547 view: PersonDetailsView[Number(event.target.value)],
552 parseMessage(msg: any) {
553 let op = wsUserOp(msg);
556 toast(i18n.t(msg.error), "danger");
557 if (msg.error == "couldnt_find_that_username_or_email") {
558 this.context.router.history.push("/");
561 } else if (msg.reconnect) {
562 this.fetchUserData();
563 } else if (op == UserOperation.GetPersonDetails) {
564 // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
565 // and set the parent state if it is not set or differs
566 // TODO this might need to get abstracted
567 let data = wsJsonToRes<GetPersonDetailsResponse>(msg).data;
568 this.state.personRes = data;
570 this.state.loading = false;
571 this.setPersonBlock();
572 this.setState(this.state);
573 restoreScrollPosition(this.context);
574 } else if (op == UserOperation.AddAdmin) {
575 let data = wsJsonToRes<AddAdminResponse>(msg).data;
576 this.state.siteRes.admins = data.admins;
577 this.setState(this.state);
578 } else if (op == UserOperation.CreateCommentLike) {
579 let data = wsJsonToRes<CommentResponse>(msg).data;
580 createCommentLikeRes(data.comment_view, this.state.personRes.comments);
581 this.setState(this.state);
583 op == UserOperation.EditComment ||
584 op == UserOperation.DeleteComment ||
585 op == UserOperation.RemoveComment
587 let data = wsJsonToRes<CommentResponse>(msg).data;
588 editCommentRes(data.comment_view, this.state.personRes.comments);
589 this.setState(this.state);
590 } else if (op == UserOperation.CreateComment) {
591 let data = wsJsonToRes<CommentResponse>(msg).data;
593 UserService.Instance.myUserInfo &&
594 data.comment_view.creator.id ==
595 UserService.Instance.myUserInfo?.local_user_view.person.id
597 toast(i18n.t("reply_sent"));
599 } else if (op == UserOperation.SaveComment) {
600 let data = wsJsonToRes<CommentResponse>(msg).data;
601 saveCommentRes(data.comment_view, this.state.personRes.comments);
602 this.setState(this.state);
604 op == UserOperation.EditPost ||
605 op == UserOperation.DeletePost ||
606 op == UserOperation.RemovePost ||
607 op == UserOperation.LockPost ||
608 op == UserOperation.StickyPost ||
609 op == UserOperation.SavePost
611 let data = wsJsonToRes<PostResponse>(msg).data;
612 editPostFindRes(data.post_view, this.state.personRes.posts);
613 this.setState(this.state);
614 } else if (op == UserOperation.CreatePostLike) {
615 let data = wsJsonToRes<PostResponse>(msg).data;
616 createPostLikeFindRes(data.post_view, this.state.personRes.posts);
617 this.setState(this.state);
618 } else if (op == UserOperation.BanPerson) {
619 let data = wsJsonToRes<BanPersonResponse>(msg).data;
620 this.state.personRes.comments
621 .filter(c => c.creator.id == data.person_view.person.id)
622 .forEach(c => (c.creator.banned = data.banned));
623 this.state.personRes.posts
624 .filter(c => c.creator.id == data.person_view.person.id)
625 .forEach(c => (c.creator.banned = data.banned));
626 this.setState(this.state);
627 } else if (op == UserOperation.BlockPerson) {
628 let data = wsJsonToRes<BlockPersonResponse>(msg).data;
629 updatePersonBlock(data);
630 this.setPersonBlock();
631 this.setState(this.state);