1 import { Component, linkEvent } from "inferno";
2 import { Link } from "inferno-router";
9 GetPersonDetailsResponse,
14 } from "lemmy-js-client";
15 import moment from "moment";
16 import { Subscription } from "rxjs";
17 import { i18n } from "../../i18next";
18 import { InitialFetchRequest, PersonDetailsView } from "../../interfaces";
19 import { UserService, WebSocketService } from "../../services";
23 createPostLikeFindRes,
30 restoreScrollPosition,
44 import { BannerIconHeader } from "../common/banner-icon-header";
45 import { HtmlTags } from "../common/html-tags";
46 import { Icon, Spinner } from "../common/icon";
47 import { MomentTime } from "../common/moment-time";
48 import { SortSelect } from "../common/sort-select";
49 import { CommunityLink } from "../community/community-link";
50 import { PersonDetails } from "./person-details";
51 import { PersonListing } from "./person-listing";
53 interface ProfileState {
54 personRes: GetPersonDetailsResponse;
56 view: PersonDetailsView;
60 siteRes: GetSiteResponse;
63 interface ProfileProps {
64 view: PersonDetailsView;
67 person_id: number | null;
77 export class Profile extends Component<any, ProfileState> {
78 private isoData = setIsoData(this.context);
79 private subscription: Subscription;
80 private emptyState: ProfileState = {
82 userName: getUsernameFromProps(this.props),
84 view: Profile.getViewFromProps(this.props.match.view),
85 sort: Profile.getSortTypeFromProps(this.props.match.sort),
86 page: Profile.getPageFromProps(this.props.match.page),
87 siteRes: this.isoData.site_res,
90 constructor(props: any, context: any) {
91 super(props, context);
93 this.state = this.emptyState;
94 this.handleSortChange = this.handleSortChange.bind(this);
96 this.parseMessage = this.parseMessage.bind(this);
97 this.subscription = wsSubscribe(this.parseMessage);
99 // Only fetch the data if coming from another route
100 if (this.isoData.path == this.context.router.route.match.url) {
101 this.state.personRes = this.isoData.routeData[0];
102 this.state.loading = false;
104 this.fetchUserData();
111 let form: GetPersonDetails = {
112 username: this.state.userName,
113 sort: this.state.sort,
114 saved_only: this.state.view === PersonDetailsView.Saved,
115 page: this.state.page,
117 auth: authField(false),
119 WebSocketService.Instance.send(wsClient.getPersonDetails(form));
122 get isCurrentUser() {
124 UserService.Instance.myUserInfo?.local_user_view.person.id ==
125 this.state.personRes.person_view.person.id
129 static getViewFromProps(view: string): PersonDetailsView {
130 return view ? PersonDetailsView[view] : PersonDetailsView.Overview;
133 static getSortTypeFromProps(sort: string): SortType {
134 return sort ? routeSortTypeToEnum(sort) : SortType.New;
137 static getPageFromProps(page: number): number {
138 return page ? Number(page) : 1;
141 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
142 let pathSplit = req.path.split("/");
143 let promises: Promise<any>[] = [];
145 // It can be /u/me, or /username/1
146 let idOrName = pathSplit[2];
147 let person_id: number;
148 let username: string;
149 if (isNaN(Number(idOrName))) {
152 person_id = Number(idOrName);
155 let view = this.getViewFromProps(pathSplit[4]);
156 let sort = this.getSortTypeFromProps(pathSplit[6]);
157 let page = this.getPageFromProps(Number(pathSplit[8]));
159 let form: GetPersonDetails = {
161 saved_only: view === PersonDetailsView.Saved,
165 setOptionalAuth(form, req.auth);
166 this.setIdOrName(form, person_id, username);
167 promises.push(req.client.getPersonDetails(form));
171 static setIdOrName(obj: any, id: number, name_: string) {
175 obj.username = name_;
179 componentWillUnmount() {
180 this.subscription.unsubscribe();
181 saveScrollPosition(this.context);
184 static getDerivedStateFromProps(props: any): ProfileProps {
186 view: this.getViewFromProps(props.match.params.view),
187 sort: this.getSortTypeFromProps(props.match.params.sort),
188 page: this.getPageFromProps(props.match.params.page),
189 person_id: Number(props.match.params.id) || null,
190 username: props.match.params.username,
194 componentDidUpdate(lastProps: any) {
195 // Necessary if you are on a post and you click another post (same route)
197 lastProps.location.pathname.split("/")[2] !==
198 lastProps.history.location.pathname.split("/")[2]
200 // Couldnt get a refresh working. This does for now.
205 get documentTitle(): string {
206 return `@${this.state.personRes.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`;
209 get bioTag(): string {
210 return this.state.personRes.person_view.person.bio
211 ? previewLines(this.state.personRes.person_view.person.bio)
217 <div class="container">
218 {this.state.loading ? (
224 <div class="col-12 col-md-8">
227 title={this.documentTitle}
228 path={this.context.router.route.match.url}
229 description={this.bioTag}
230 image={this.state.personRes.person_view.person.avatar}
235 {!this.state.loading && this.selects()}
237 personRes={this.state.personRes}
238 admins={this.state.siteRes.admins}
239 sort={this.state.sort}
240 page={this.state.page}
243 this.state.siteRes.site_view.site.enable_downvotes
245 enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
246 view={this.state.view}
247 onPageChange={this.handlePageChange}
251 {!this.state.loading && (
252 <div class="col-12 col-md-4">
254 {UserService.Instance.myUserInfo && this.follows()}
265 <div class="btn-group btn-group-toggle flex-wrap mb-2">
267 className={`btn btn-outline-secondary pointer
268 ${this.state.view == PersonDetailsView.Overview && "active"}
273 value={PersonDetailsView.Overview}
274 checked={this.state.view === PersonDetailsView.Overview}
275 onChange={linkEvent(this, this.handleViewChange)}
280 className={`btn btn-outline-secondary pointer
281 ${this.state.view == PersonDetailsView.Comments && "active"}
286 value={PersonDetailsView.Comments}
287 checked={this.state.view == PersonDetailsView.Comments}
288 onChange={linkEvent(this, this.handleViewChange)}
293 className={`btn btn-outline-secondary pointer
294 ${this.state.view == PersonDetailsView.Posts && "active"}
299 value={PersonDetailsView.Posts}
300 checked={this.state.view == PersonDetailsView.Posts}
301 onChange={linkEvent(this, this.handleViewChange)}
306 className={`btn btn-outline-secondary pointer
307 ${this.state.view == PersonDetailsView.Saved && "active"}
312 value={PersonDetailsView.Saved}
313 checked={this.state.view == PersonDetailsView.Saved}
314 onChange={linkEvent(this, this.handleViewChange)}
324 <div className="mb-2">
325 <span class="mr-3">{this.viewRadios()}</span>
327 sort={this.state.sort}
328 onChange={this.handleSortChange}
333 href={`/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`}
337 <Icon icon="rss" classes="text-muted small mx-2" />
344 let pv = this.state.personRes?.person_view;
348 <BannerIconHeader banner={pv.person.banner} icon={pv.person.avatar} />
351 <div class="mb-0 d-flex flex-wrap">
353 {pv.person.display_name && (
354 <h5 class="mb-0">{pv.person.display_name}</h5>
356 <ul class="list-inline mb-2">
357 <li className="list-inline-item">
366 {pv.person.banned && (
367 <li className="list-inline-item badge badge-danger">
373 <div className="flex-grow-1 unselectable pointer mx-2"></div>
374 {!this.isCurrentUser && (
377 className={`d-flex align-self-start btn btn-secondary mr-2 ${
378 !pv.person.matrix_user_id && "invisible"
381 href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
383 {i18n.t("send_secure_message")}
386 className={"d-flex align-self-start btn btn-secondary"}
387 to={`/create_private_message/recipient/${pv.person.id}`}
389 {i18n.t("send_message")}
395 <div className="d-flex align-items-center mb-2">
398 dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
403 <ul class="list-inline mb-2">
404 <li className="list-inline-item badge badge-light">
405 {i18n.t("number_of_posts", { count: pv.counts.post_count })}
407 <li className="list-inline-item badge badge-light">
408 {i18n.t("number_of_comments", {
409 count: pv.counts.comment_count,
414 <div class="text-muted">
415 {i18n.t("joined")}{" "}
416 <MomentTime data={pv.person} showAgo ignoreUpdated />
418 <div className="d-flex align-items-center text-muted mb-2">
420 <span className="ml-2">
421 {i18n.t("cake_day_title")}{" "}
422 {moment.utc(pv.person.published).local().format("MMM DD, YYYY")}
434 {this.state.personRes.moderates.length > 0 && (
435 <div class="card border-secondary mb-3">
436 <div class="card-body">
437 <h5>{i18n.t("moderates")}</h5>
438 <ul class="list-unstyled mb-0">
439 {this.state.personRes.moderates.map(cmv => (
441 <CommunityLink community={cmv.community} />
453 let follows = UserService.Instance.myUserInfo.follows;
456 {follows.length > 0 && (
457 <div class="card border-secondary mb-3">
458 <div class="card-body">
459 <h5>{i18n.t("subscribed")}</h5>
460 <ul class="list-unstyled mb-0">
461 {follows.map(cfv => (
463 <CommunityLink community={cfv.community} />
474 updateUrl(paramUpdates: UrlParams) {
475 const page = paramUpdates.page || this.state.page;
476 const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
477 const sortStr = paramUpdates.sort || this.state.sort;
479 let typeView = `/u/${this.state.userName}`;
481 this.props.history.push(
482 `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
484 this.state.loading = true;
485 this.setState(this.state);
486 this.fetchUserData();
489 handlePageChange(page: number) {
490 this.updateUrl({ page });
493 handleSortChange(val: SortType) {
494 this.updateUrl({ sort: val, page: 1 });
497 handleViewChange(i: Profile, event: any) {
499 view: PersonDetailsView[Number(event.target.value)],
504 parseMessage(msg: any) {
505 let op = wsUserOp(msg);
508 toast(i18n.t(msg.error), "danger");
509 if (msg.error == "couldnt_find_that_username_or_email") {
510 this.context.router.history.push("/");
513 } else if (msg.reconnect) {
514 this.fetchUserData();
515 } else if (op == UserOperation.GetPersonDetails) {
516 // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
517 // and set the parent state if it is not set or differs
518 // TODO this might need to get abstracted
519 let data = wsJsonToRes<GetPersonDetailsResponse>(msg).data;
520 this.state.personRes = data;
522 this.state.loading = false;
523 this.setState(this.state);
524 restoreScrollPosition(this.context);
525 } else if (op == UserOperation.AddAdmin) {
526 let data = wsJsonToRes<AddAdminResponse>(msg).data;
527 this.state.siteRes.admins = data.admins;
528 this.setState(this.state);
529 } else if (op == UserOperation.CreateCommentLike) {
530 let data = wsJsonToRes<CommentResponse>(msg).data;
531 createCommentLikeRes(data.comment_view, this.state.personRes.comments);
532 this.setState(this.state);
534 op == UserOperation.EditComment ||
535 op == UserOperation.DeleteComment ||
536 op == UserOperation.RemoveComment
538 let data = wsJsonToRes<CommentResponse>(msg).data;
539 editCommentRes(data.comment_view, this.state.personRes.comments);
540 this.setState(this.state);
541 } else if (op == UserOperation.CreateComment) {
542 let data = wsJsonToRes<CommentResponse>(msg).data;
544 UserService.Instance.myUserInfo &&
545 data.comment_view.creator.id ==
546 UserService.Instance.myUserInfo.local_user_view.person.id
548 toast(i18n.t("reply_sent"));
550 } else if (op == UserOperation.SaveComment) {
551 let data = wsJsonToRes<CommentResponse>(msg).data;
552 saveCommentRes(data.comment_view, this.state.personRes.comments);
553 this.setState(this.state);
555 op == UserOperation.EditPost ||
556 op == UserOperation.DeletePost ||
557 op == UserOperation.RemovePost ||
558 op == UserOperation.LockPost ||
559 op == UserOperation.StickyPost ||
560 op == UserOperation.SavePost
562 let data = wsJsonToRes<PostResponse>(msg).data;
563 editPostFindRes(data.post_view, this.state.personRes.posts);
564 this.setState(this.state);
565 } else if (op == UserOperation.CreatePostLike) {
566 let data = wsJsonToRes<PostResponse>(msg).data;
567 createPostLikeFindRes(data.post_view, this.state.personRes.posts);
568 this.setState(this.state);
569 } else if (op == UserOperation.BanPerson) {
570 let data = wsJsonToRes<BanPersonResponse>(msg).data;
571 this.state.personRes.comments
572 .filter(c => c.creator.id == data.person_view.person.id)
573 .forEach(c => (c.creator.banned = data.banned));
574 this.state.personRes.posts
575 .filter(c => c.creator.id == data.person_view.person.id)
576 .forEach(c => (c.creator.banned = data.banned));
577 this.setState(this.state);
578 } else if (op == UserOperation.BlockPerson) {
579 let data = wsJsonToRes<BlockPersonResponse>(msg).data;
580 updatePersonBlock(data);