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,
31 restoreScrollPosition,
45 import { BannerIconHeader } from "../common/banner-icon-header";
46 import { HtmlTags } from "../common/html-tags";
47 import { Icon, Spinner } from "../common/icon";
48 import { MomentTime } from "../common/moment-time";
49 import { SortSelect } from "../common/sort-select";
50 import { CommunityLink } from "../community/community-link";
51 import { PersonDetails } from "./person-details";
52 import { PersonListing } from "./person-listing";
54 interface ProfileState {
55 personRes: GetPersonDetailsResponse;
57 view: PersonDetailsView;
61 siteRes: GetSiteResponse;
64 interface ProfileProps {
65 view: PersonDetailsView;
68 person_id: number | null;
78 export class Profile extends Component<any, ProfileState> {
79 private isoData = setIsoData(this.context);
80 private subscription: Subscription;
81 private emptyState: ProfileState = {
83 userName: getUsernameFromProps(this.props),
85 view: Profile.getViewFromProps(this.props.match.view),
86 sort: Profile.getSortTypeFromProps(this.props.match.sort),
87 page: Profile.getPageFromProps(this.props.match.page),
88 siteRes: this.isoData.site_res,
91 constructor(props: any, context: any) {
92 super(props, context);
94 this.state = this.emptyState;
95 this.handleSortChange = this.handleSortChange.bind(this);
96 this.handlePageChange = this.handlePageChange.bind(this);
98 this.parseMessage = this.parseMessage.bind(this);
99 this.subscription = wsSubscribe(this.parseMessage);
101 // Only fetch the data if coming from another route
102 if (this.isoData.path == this.context.router.route.match.url) {
103 this.state.personRes = this.isoData.routeData[0];
104 this.state.loading = false;
106 this.fetchUserData();
113 let form: GetPersonDetails = {
114 username: this.state.userName,
115 sort: this.state.sort,
116 saved_only: this.state.view === PersonDetailsView.Saved,
117 page: this.state.page,
119 auth: authField(false),
121 WebSocketService.Instance.send(wsClient.getPersonDetails(form));
124 get isCurrentUser() {
126 UserService.Instance.myUserInfo?.local_user_view.person.id ==
127 this.state.personRes.person_view.person.id
131 static getViewFromProps(view: string): PersonDetailsView {
132 return view ? PersonDetailsView[view] : PersonDetailsView.Overview;
135 static getSortTypeFromProps(sort: string): SortType {
136 return sort ? routeSortTypeToEnum(sort) : SortType.New;
139 static getPageFromProps(page: number): number {
140 return page ? Number(page) : 1;
143 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
144 let pathSplit = req.path.split("/");
145 let promises: Promise<any>[] = [];
147 // It can be /u/me, or /username/1
148 let idOrName = pathSplit[2];
149 let person_id: number;
150 let username: string;
151 if (isNaN(Number(idOrName))) {
154 person_id = Number(idOrName);
157 let view = this.getViewFromProps(pathSplit[4]);
158 let sort = this.getSortTypeFromProps(pathSplit[6]);
159 let page = this.getPageFromProps(Number(pathSplit[8]));
161 let form: GetPersonDetails = {
163 saved_only: view === PersonDetailsView.Saved,
167 setOptionalAuth(form, req.auth);
168 this.setIdOrName(form, person_id, username);
169 promises.push(req.client.getPersonDetails(form));
173 static setIdOrName(obj: any, id: number, name_: string) {
177 obj.username = name_;
181 componentWillUnmount() {
182 this.subscription.unsubscribe();
183 saveScrollPosition(this.context);
186 static getDerivedStateFromProps(props: any): ProfileProps {
188 view: this.getViewFromProps(props.match.params.view),
189 sort: this.getSortTypeFromProps(props.match.params.sort),
190 page: this.getPageFromProps(props.match.params.page),
191 person_id: Number(props.match.params.id) || null,
192 username: props.match.params.username,
196 componentDidUpdate(lastProps: any) {
197 // Necessary if you are on a post and you click another post (same route)
199 lastProps.location.pathname.split("/")[2] !==
200 lastProps.history.location.pathname.split("/")[2]
202 // Couldnt get a refresh working. This does for now.
207 get documentTitle(): string {
208 return `@${this.state.personRes.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`;
211 get bioTag(): string {
212 return this.state.personRes.person_view.person.bio
213 ? previewLines(this.state.personRes.person_view.person.bio)
219 <div class="container">
220 {this.state.loading ? (
226 <div class="col-12 col-md-8">
229 title={this.documentTitle}
230 path={this.context.router.route.match.url}
231 description={this.bioTag}
232 image={this.state.personRes.person_view.person.avatar}
237 {!this.state.loading && this.selects()}
239 personRes={this.state.personRes}
240 admins={this.state.siteRes.admins}
241 sort={this.state.sort}
242 page={this.state.page}
245 this.state.siteRes.site_view.site.enable_downvotes
247 enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
248 view={this.state.view}
249 onPageChange={this.handlePageChange}
253 {!this.state.loading && (
254 <div class="col-12 col-md-4">
256 {this.isCurrentUser && this.follows()}
267 <div class="btn-group btn-group-toggle flex-wrap mb-2">
269 className={`btn btn-outline-secondary pointer
270 ${this.state.view == PersonDetailsView.Overview && "active"}
275 value={PersonDetailsView.Overview}
276 checked={this.state.view === PersonDetailsView.Overview}
277 onChange={linkEvent(this, this.handleViewChange)}
282 className={`btn btn-outline-secondary pointer
283 ${this.state.view == PersonDetailsView.Comments && "active"}
288 value={PersonDetailsView.Comments}
289 checked={this.state.view == PersonDetailsView.Comments}
290 onChange={linkEvent(this, this.handleViewChange)}
295 className={`btn btn-outline-secondary pointer
296 ${this.state.view == PersonDetailsView.Posts && "active"}
301 value={PersonDetailsView.Posts}
302 checked={this.state.view == PersonDetailsView.Posts}
303 onChange={linkEvent(this, this.handleViewChange)}
308 className={`btn btn-outline-secondary pointer
309 ${this.state.view == PersonDetailsView.Saved && "active"}
314 value={PersonDetailsView.Saved}
315 checked={this.state.view == PersonDetailsView.Saved}
316 onChange={linkEvent(this, this.handleViewChange)}
326 <div className="mb-2">
327 <span class="mr-3">{this.viewRadios()}</span>
329 sort={this.state.sort}
330 onChange={this.handleSortChange}
335 href={`/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`}
339 <Icon icon="rss" classes="text-muted small mx-2" />
346 let pv = this.state.personRes?.person_view;
350 <BannerIconHeader banner={pv.person.banner} icon={pv.person.avatar} />
353 <div class="mb-0 d-flex flex-wrap">
355 {pv.person.display_name && (
356 <h5 class="mb-0">{pv.person.display_name}</h5>
358 <ul class="list-inline mb-2">
359 <li className="list-inline-item">
368 {pv.person.banned && (
369 <li className="list-inline-item badge badge-danger">
373 {pv.person.admin && (
374 <li className="list-inline-item badge badge-light">
378 {pv.person.bot_account && (
379 <li className="list-inline-item badge badge-light">
380 {i18n.t("bot_account").toLowerCase()}
385 <div className="flex-grow-1 unselectable pointer mx-2"></div>
386 {!this.isCurrentUser && (
389 className={`d-flex align-self-start btn btn-secondary mr-2 ${
390 !pv.person.matrix_user_id && "invisible"
393 href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
395 {i18n.t("send_secure_message")}
398 className={"d-flex align-self-start btn btn-secondary"}
399 to={`/create_private_message/recipient/${pv.person.id}`}
401 {i18n.t("send_message")}
407 <div className="d-flex align-items-center mb-2">
410 dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
415 <ul class="list-inline mb-2">
416 <li className="list-inline-item badge badge-light">
417 {i18n.t("number_of_posts", {
418 count: pv.counts.post_count,
419 formattedCount: numToSI(pv.counts.post_count),
422 <li className="list-inline-item badge badge-light">
423 {i18n.t("number_of_comments", {
424 count: pv.counts.comment_count,
425 formattedCount: numToSI(pv.counts.comment_count),
430 <div class="text-muted">
431 {i18n.t("joined")}{" "}
432 <MomentTime data={pv.person} showAgo ignoreUpdated />
434 <div className="d-flex align-items-center text-muted mb-2">
436 <span className="ml-2">
437 {i18n.t("cake_day_title")}{" "}
438 {moment.utc(pv.person.published).local().format("MMM DD, YYYY")}
450 {this.state.personRes.moderates.length > 0 && (
451 <div class="card border-secondary mb-3">
452 <div class="card-body">
453 <h5>{i18n.t("moderates")}</h5>
454 <ul class="list-unstyled mb-0">
455 {this.state.personRes.moderates.map(cmv => (
457 <CommunityLink community={cmv.community} />
469 let follows = UserService.Instance.myUserInfo.follows;
472 {follows.length > 0 && (
473 <div class="card border-secondary mb-3">
474 <div class="card-body">
475 <h5>{i18n.t("subscribed")}</h5>
476 <ul class="list-unstyled mb-0">
477 {follows.map(cfv => (
479 <CommunityLink community={cfv.community} />
490 updateUrl(paramUpdates: UrlParams) {
491 const page = paramUpdates.page || this.state.page;
492 const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
493 const sortStr = paramUpdates.sort || this.state.sort;
495 let typeView = `/u/${this.state.userName}`;
497 this.props.history.push(
498 `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
500 this.state.loading = true;
501 this.setState(this.state);
502 this.fetchUserData();
505 handlePageChange(page: number) {
506 this.updateUrl({ page });
509 handleSortChange(val: SortType) {
510 this.updateUrl({ sort: val, page: 1 });
513 handleViewChange(i: Profile, event: any) {
515 view: PersonDetailsView[Number(event.target.value)],
520 parseMessage(msg: any) {
521 let op = wsUserOp(msg);
524 toast(i18n.t(msg.error), "danger");
525 if (msg.error == "couldnt_find_that_username_or_email") {
526 this.context.router.history.push("/");
529 } else if (msg.reconnect) {
530 this.fetchUserData();
531 } else if (op == UserOperation.GetPersonDetails) {
532 // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
533 // and set the parent state if it is not set or differs
534 // TODO this might need to get abstracted
535 let data = wsJsonToRes<GetPersonDetailsResponse>(msg).data;
536 this.state.personRes = data;
538 this.state.loading = false;
539 this.setState(this.state);
540 restoreScrollPosition(this.context);
541 } else if (op == UserOperation.AddAdmin) {
542 let data = wsJsonToRes<AddAdminResponse>(msg).data;
543 this.state.siteRes.admins = data.admins;
544 this.setState(this.state);
545 } else if (op == UserOperation.CreateCommentLike) {
546 let data = wsJsonToRes<CommentResponse>(msg).data;
547 createCommentLikeRes(data.comment_view, this.state.personRes.comments);
548 this.setState(this.state);
550 op == UserOperation.EditComment ||
551 op == UserOperation.DeleteComment ||
552 op == UserOperation.RemoveComment
554 let data = wsJsonToRes<CommentResponse>(msg).data;
555 editCommentRes(data.comment_view, this.state.personRes.comments);
556 this.setState(this.state);
557 } else if (op == UserOperation.CreateComment) {
558 let data = wsJsonToRes<CommentResponse>(msg).data;
560 UserService.Instance.myUserInfo &&
561 data.comment_view.creator.id ==
562 UserService.Instance.myUserInfo.local_user_view.person.id
564 toast(i18n.t("reply_sent"));
566 } else if (op == UserOperation.SaveComment) {
567 let data = wsJsonToRes<CommentResponse>(msg).data;
568 saveCommentRes(data.comment_view, this.state.personRes.comments);
569 this.setState(this.state);
571 op == UserOperation.EditPost ||
572 op == UserOperation.DeletePost ||
573 op == UserOperation.RemovePost ||
574 op == UserOperation.LockPost ||
575 op == UserOperation.StickyPost ||
576 op == UserOperation.SavePost
578 let data = wsJsonToRes<PostResponse>(msg).data;
579 editPostFindRes(data.post_view, this.state.personRes.posts);
580 this.setState(this.state);
581 } else if (op == UserOperation.CreatePostLike) {
582 let data = wsJsonToRes<PostResponse>(msg).data;
583 createPostLikeFindRes(data.post_view, this.state.personRes.posts);
584 this.setState(this.state);
585 } else if (op == UserOperation.BanPerson) {
586 let data = wsJsonToRes<BanPersonResponse>(msg).data;
587 this.state.personRes.comments
588 .filter(c => c.creator.id == data.person_view.person.id)
589 .forEach(c => (c.creator.banned = data.banned));
590 this.state.personRes.posts
591 .filter(c => c.creator.id == data.person_view.person.id)
592 .forEach(c => (c.creator.banned = data.banned));
593 this.setState(this.state);
594 } else if (op == UserOperation.BlockPerson) {
595 let data = wsJsonToRes<BlockPersonResponse>(msg).data;
596 updatePersonBlock(data);