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,
32 restoreScrollPosition,
46 import { BannerIconHeader } from "../common/banner-icon-header";
47 import { HtmlTags } from "../common/html-tags";
48 import { Icon, Spinner } from "../common/icon";
49 import { MomentTime } from "../common/moment-time";
50 import { SortSelect } from "../common/sort-select";
51 import { CommunityLink } from "../community/community-link";
52 import { PersonDetails } from "./person-details";
53 import { PersonListing } from "./person-listing";
55 interface ProfileState {
56 personRes: GetPersonDetailsResponse;
58 view: PersonDetailsView;
62 personBlocked: boolean;
63 siteRes: GetSiteResponse;
66 interface ProfileProps {
67 view: PersonDetailsView;
70 person_id: number | null;
80 export class Profile extends Component<any, ProfileState> {
81 private isoData = setIsoData(this.context);
82 private subscription: Subscription;
83 private emptyState: ProfileState = {
85 userName: getUsernameFromProps(this.props),
87 view: Profile.getViewFromProps(this.props.match.view),
88 sort: Profile.getSortTypeFromProps(this.props.match.sort),
89 page: Profile.getPageFromProps(this.props.match.page),
91 siteRes: this.isoData.site_res,
94 constructor(props: any, context: any) {
95 super(props, context);
97 this.state = this.emptyState;
98 this.handleSortChange = this.handleSortChange.bind(this);
99 this.handlePageChange = this.handlePageChange.bind(this);
101 this.parseMessage = this.parseMessage.bind(this);
102 this.subscription = wsSubscribe(this.parseMessage);
104 // Only fetch the data if coming from another route
105 if (this.isoData.path == this.context.router.route.match.url) {
106 this.state.personRes = this.isoData.routeData[0];
107 this.state.loading = false;
109 this.fetchUserData();
112 this.setPersonBlock();
116 let form: GetPersonDetails = {
117 username: this.state.userName,
118 sort: this.state.sort,
119 saved_only: this.state.view === PersonDetailsView.Saved,
120 page: this.state.page,
122 auth: authField(false),
124 WebSocketService.Instance.send(wsClient.getPersonDetails(form));
127 get isCurrentUser() {
129 UserService.Instance.myUserInfo?.local_user_view.person.id ==
130 this.state.personRes.person_view.person.id
135 this.state.personBlocked = UserService.Instance.myUserInfo?.person_blocks
136 .map(a => a.target.id)
137 .includes(this.state.personRes?.person_view.person.id);
140 static getViewFromProps(view: string): PersonDetailsView {
141 return view ? PersonDetailsView[view] : PersonDetailsView.Overview;
144 static getSortTypeFromProps(sort: string): SortType {
145 return sort ? routeSortTypeToEnum(sort) : SortType.New;
148 static getPageFromProps(page: number): number {
149 return page ? Number(page) : 1;
152 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
153 let pathSplit = req.path.split("/");
154 let promises: Promise<any>[] = [];
156 // It can be /u/me, or /username/1
157 let idOrName = pathSplit[2];
158 let person_id: number;
159 let username: string;
160 if (isNaN(Number(idOrName))) {
163 person_id = Number(idOrName);
166 let view = this.getViewFromProps(pathSplit[4]);
167 let sort = this.getSortTypeFromProps(pathSplit[6]);
168 let page = this.getPageFromProps(Number(pathSplit[8]));
170 let form: GetPersonDetails = {
172 saved_only: view === PersonDetailsView.Saved,
176 setOptionalAuth(form, req.auth);
177 this.setIdOrName(form, person_id, username);
178 promises.push(req.client.getPersonDetails(form));
182 static setIdOrName(obj: any, id: number, name_: string) {
186 obj.username = name_;
190 componentDidMount() {
194 componentWillUnmount() {
195 this.subscription.unsubscribe();
196 saveScrollPosition(this.context);
199 static getDerivedStateFromProps(props: any): ProfileProps {
201 view: this.getViewFromProps(props.match.params.view),
202 sort: this.getSortTypeFromProps(props.match.params.sort),
203 page: this.getPageFromProps(props.match.params.page),
204 person_id: Number(props.match.params.id) || null,
205 username: props.match.params.username,
209 componentDidUpdate(lastProps: any) {
210 // Necessary if you are on a post and you click another post (same route)
212 lastProps.location.pathname.split("/")[2] !==
213 lastProps.history.location.pathname.split("/")[2]
215 // Couldnt get a refresh working. This does for now.
220 get documentTitle(): string {
221 return `@${this.state.personRes.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`;
224 get bioTag(): string {
225 return this.state.personRes.person_view.person.bio
226 ? previewLines(this.state.personRes.person_view.person.bio)
232 <div class="container">
233 {this.state.loading ? (
239 <div class="col-12 col-md-8">
242 title={this.documentTitle}
243 path={this.context.router.route.match.url}
244 description={this.bioTag}
245 image={this.state.personRes.person_view.person.avatar}
250 {!this.state.loading && this.selects()}
252 personRes={this.state.personRes}
253 admins={this.state.siteRes.admins}
254 sort={this.state.sort}
255 page={this.state.page}
258 this.state.siteRes.site_view.site.enable_downvotes
260 enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
261 view={this.state.view}
262 onPageChange={this.handlePageChange}
266 {!this.state.loading && (
267 <div class="col-12 col-md-4">
269 {this.isCurrentUser && this.follows()}
280 <div class="btn-group btn-group-toggle flex-wrap mb-2">
282 className={`btn btn-outline-secondary pointer
283 ${this.state.view == PersonDetailsView.Overview && "active"}
288 value={PersonDetailsView.Overview}
289 checked={this.state.view === PersonDetailsView.Overview}
290 onChange={linkEvent(this, this.handleViewChange)}
295 className={`btn btn-outline-secondary pointer
296 ${this.state.view == PersonDetailsView.Comments && "active"}
301 value={PersonDetailsView.Comments}
302 checked={this.state.view == PersonDetailsView.Comments}
303 onChange={linkEvent(this, this.handleViewChange)}
308 className={`btn btn-outline-secondary pointer
309 ${this.state.view == PersonDetailsView.Posts && "active"}
314 value={PersonDetailsView.Posts}
315 checked={this.state.view == PersonDetailsView.Posts}
316 onChange={linkEvent(this, this.handleViewChange)}
321 className={`btn btn-outline-secondary pointer
322 ${this.state.view == PersonDetailsView.Saved && "active"}
327 value={PersonDetailsView.Saved}
328 checked={this.state.view == PersonDetailsView.Saved}
329 onChange={linkEvent(this, this.handleViewChange)}
338 let profileRss = `/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`;
341 <div className="mb-2">
342 <span class="mr-3">{this.viewRadios()}</span>
344 sort={this.state.sort}
345 onChange={this.handleSortChange}
349 <a href={profileRss} rel="noopener" title="RSS">
350 <Icon icon="rss" classes="text-muted small mx-2" />
352 <link rel="alternate" type="application/atom+xml" href={profileRss} />
356 handleBlockPerson(personId: number) {
358 let blockUserForm: BlockPerson = {
363 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
366 handleUnblockPerson(recipientId: number) {
367 let blockUserForm: BlockPerson = {
368 person_id: recipientId,
372 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
376 let pv = this.state.personRes?.person_view;
380 <BannerIconHeader banner={pv.person.banner} icon={pv.person.avatar} />
383 <div class="mb-0 d-flex flex-wrap">
385 {pv.person.display_name && (
386 <h5 class="mb-0">{pv.person.display_name}</h5>
388 <ul class="list-inline mb-2">
389 <li className="list-inline-item">
398 {pv.person.banned && (
399 <li className="list-inline-item badge badge-danger">
403 {pv.person.admin && (
404 <li className="list-inline-item badge badge-light">
408 {pv.person.bot_account && (
409 <li className="list-inline-item badge badge-light">
410 {i18n.t("bot_account").toLowerCase()}
415 <div className="flex-grow-1 unselectable pointer mx-2"></div>
416 {!this.isCurrentUser && UserService.Instance.myUserInfo && (
419 className={`d-flex align-self-start btn btn-secondary mr-2 ${
420 !pv.person.matrix_user_id && "invisible"
423 href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
425 {i18n.t("send_secure_message")}
428 className={"d-flex align-self-start btn btn-secondary mr-2"}
429 to={`/create_private_message/recipient/${pv.person.id}`}
431 {i18n.t("send_message")}
433 {this.state.personBlocked ? (
435 className={"d-flex align-self-start btn btn-secondary"}
438 this.handleUnblockPerson
441 {i18n.t("unblock_user")}
445 className={"d-flex align-self-start btn btn-secondary"}
446 onClick={linkEvent(pv.person.id, this.handleBlockPerson)}
448 {i18n.t("block_user")}
455 <div className="d-flex align-items-center mb-2">
458 dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
463 <ul class="list-inline mb-2">
464 <li className="list-inline-item badge badge-light">
465 {i18n.t("number_of_posts", {
466 count: pv.counts.post_count,
467 formattedCount: numToSI(pv.counts.post_count),
470 <li className="list-inline-item badge badge-light">
471 {i18n.t("number_of_comments", {
472 count: pv.counts.comment_count,
473 formattedCount: numToSI(pv.counts.comment_count),
478 <div class="text-muted">
479 {i18n.t("joined")}{" "}
480 <MomentTime data={pv.person} showAgo ignoreUpdated />
482 <div className="d-flex align-items-center text-muted mb-2">
484 <span className="ml-2">
485 {i18n.t("cake_day_title")}{" "}
486 {moment.utc(pv.person.published).local().format("MMM DD, YYYY")}
498 {this.state.personRes.moderates.length > 0 && (
499 <div class="card border-secondary mb-3">
500 <div class="card-body">
501 <h5>{i18n.t("moderates")}</h5>
502 <ul class="list-unstyled mb-0">
503 {this.state.personRes.moderates.map(cmv => (
505 <CommunityLink community={cmv.community} />
517 let follows = UserService.Instance.myUserInfo.follows;
520 {follows.length > 0 && (
521 <div class="card border-secondary mb-3">
522 <div class="card-body">
523 <h5>{i18n.t("subscribed")}</h5>
524 <ul class="list-unstyled mb-0">
525 {follows.map(cfv => (
527 <CommunityLink community={cfv.community} />
538 updateUrl(paramUpdates: UrlParams) {
539 const page = paramUpdates.page || this.state.page;
540 const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
541 const sortStr = paramUpdates.sort || this.state.sort;
543 let typeView = `/u/${this.state.userName}`;
545 this.props.history.push(
546 `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
548 this.state.loading = true;
549 this.setState(this.state);
550 this.fetchUserData();
553 handlePageChange(page: number) {
554 this.updateUrl({ page });
557 handleSortChange(val: SortType) {
558 this.updateUrl({ sort: val, page: 1 });
561 handleViewChange(i: Profile, event: any) {
563 view: PersonDetailsView[Number(event.target.value)],
568 parseMessage(msg: any) {
569 let op = wsUserOp(msg);
572 toast(i18n.t(msg.error), "danger");
573 if (msg.error == "couldnt_find_that_username_or_email") {
574 this.context.router.history.push("/");
577 } else if (msg.reconnect) {
578 this.fetchUserData();
579 } else if (op == UserOperation.GetPersonDetails) {
580 // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
581 // and set the parent state if it is not set or differs
582 // TODO this might need to get abstracted
583 let data = wsJsonToRes<GetPersonDetailsResponse>(msg).data;
584 this.state.personRes = data;
586 this.state.loading = false;
587 this.setPersonBlock();
588 this.setState(this.state);
589 restoreScrollPosition(this.context);
590 } else if (op == UserOperation.AddAdmin) {
591 let data = wsJsonToRes<AddAdminResponse>(msg).data;
592 this.state.siteRes.admins = data.admins;
593 this.setState(this.state);
594 } else if (op == UserOperation.CreateCommentLike) {
595 let data = wsJsonToRes<CommentResponse>(msg).data;
596 createCommentLikeRes(data.comment_view, this.state.personRes.comments);
597 this.setState(this.state);
599 op == UserOperation.EditComment ||
600 op == UserOperation.DeleteComment ||
601 op == UserOperation.RemoveComment
603 let data = wsJsonToRes<CommentResponse>(msg).data;
604 editCommentRes(data.comment_view, this.state.personRes.comments);
605 this.setState(this.state);
606 } else if (op == UserOperation.CreateComment) {
607 let data = wsJsonToRes<CommentResponse>(msg).data;
609 UserService.Instance.myUserInfo &&
610 data.comment_view.creator.id ==
611 UserService.Instance.myUserInfo?.local_user_view.person.id
613 toast(i18n.t("reply_sent"));
615 } else if (op == UserOperation.SaveComment) {
616 let data = wsJsonToRes<CommentResponse>(msg).data;
617 saveCommentRes(data.comment_view, this.state.personRes.comments);
618 this.setState(this.state);
620 op == UserOperation.EditPost ||
621 op == UserOperation.DeletePost ||
622 op == UserOperation.RemovePost ||
623 op == UserOperation.LockPost ||
624 op == UserOperation.StickyPost ||
625 op == UserOperation.SavePost
627 let data = wsJsonToRes<PostResponse>(msg).data;
628 editPostFindRes(data.post_view, this.state.personRes.posts);
629 this.setState(this.state);
630 } else if (op == UserOperation.CreatePostLike) {
631 let data = wsJsonToRes<PostResponse>(msg).data;
632 createPostLikeFindRes(data.post_view, this.state.personRes.posts);
633 this.setState(this.state);
634 } else if (op == UserOperation.BanPerson) {
635 let data = wsJsonToRes<BanPersonResponse>(msg).data;
636 this.state.personRes.comments
637 .filter(c => c.creator.id == data.person_view.person.id)
638 .forEach(c => (c.creator.banned = data.banned));
639 this.state.personRes.posts
640 .filter(c => c.creator.id == data.person_view.person.id)
641 .forEach(c => (c.creator.banned = data.banned));
642 this.setState(this.state);
643 } else if (op == UserOperation.BlockPerson) {
644 let data = wsJsonToRes<BlockPersonResponse>(msg).data;
645 updatePersonBlock(data);
646 this.setPersonBlock();
647 this.setState(this.state);