]> Untitled Git - lemmy-ui.git/blob - src/shared/components/person/profile.tsx
Fixing nav-link
[lemmy-ui.git] / src / shared / components / person / profile.tsx
1 import { Component, linkEvent } from "inferno";
2 import { Link } from "inferno-router";
3 import {
4   AddAdminResponse,
5   BanPersonResponse,
6   BlockPerson,
7   BlockPersonResponse,
8   CommentResponse,
9   GetPersonDetails,
10   GetPersonDetailsResponse,
11   GetSiteResponse,
12   PostResponse,
13   SortType,
14   UserOperation,
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";
21 import {
22   authField,
23   createCommentLikeRes,
24   createPostLikeFindRes,
25   editCommentRes,
26   editPostFindRes,
27   fetchLimit,
28   getUsernameFromProps,
29   mdToHtml,
30   numToSI,
31   previewLines,
32   restoreScrollPosition,
33   routeSortTypeToEnum,
34   saveCommentRes,
35   saveScrollPosition,
36   setIsoData,
37   setOptionalAuth,
38   setupTippy,
39   toast,
40   updatePersonBlock,
41   wsClient,
42   wsJsonToRes,
43   wsSubscribe,
44   wsUserOp,
45 } from "../../utils";
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";
54
55 interface ProfileState {
56   personRes: GetPersonDetailsResponse;
57   userName: string;
58   view: PersonDetailsView;
59   sort: SortType;
60   page: number;
61   loading: boolean;
62   personBlocked: boolean;
63   siteRes: GetSiteResponse;
64 }
65
66 interface ProfileProps {
67   view: PersonDetailsView;
68   sort: SortType;
69   page: number;
70   person_id: number | null;
71   username: string;
72 }
73
74 interface UrlParams {
75   view?: string;
76   sort?: SortType;
77   page?: number;
78 }
79
80 export class Profile extends Component<any, ProfileState> {
81   private isoData = setIsoData(this.context);
82   private subscription: Subscription;
83   private emptyState: ProfileState = {
84     personRes: undefined,
85     userName: getUsernameFromProps(this.props),
86     loading: true,
87     view: Profile.getViewFromProps(this.props.match.view),
88     sort: Profile.getSortTypeFromProps(this.props.match.sort),
89     page: Profile.getPageFromProps(this.props.match.page),
90     personBlocked: false,
91     siteRes: this.isoData.site_res,
92   };
93
94   constructor(props: any, context: any) {
95     super(props, context);
96
97     this.state = this.emptyState;
98     this.handleSortChange = this.handleSortChange.bind(this);
99     this.handlePageChange = this.handlePageChange.bind(this);
100
101     this.parseMessage = this.parseMessage.bind(this);
102     this.subscription = wsSubscribe(this.parseMessage);
103
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;
108     } else {
109       this.fetchUserData();
110     }
111
112     setupTippy();
113     this.setPersonBlock();
114   }
115
116   fetchUserData() {
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,
122       limit: fetchLimit,
123       auth: authField(false),
124     };
125     WebSocketService.Instance.send(wsClient.getPersonDetails(form));
126   }
127
128   get isCurrentUser() {
129     return (
130       UserService.Instance.myUserInfo?.local_user_view.person.id ==
131       this.state.personRes.person_view.person.id
132     );
133   }
134
135   setPersonBlock() {
136     this.state.personBlocked = UserService.Instance.myUserInfo.person_blocks
137       .map(a => a.target.id)
138       .includes(this.state.personRes?.person_view.person.id);
139   }
140
141   static getViewFromProps(view: string): PersonDetailsView {
142     return view ? PersonDetailsView[view] : PersonDetailsView.Overview;
143   }
144
145   static getSortTypeFromProps(sort: string): SortType {
146     return sort ? routeSortTypeToEnum(sort) : SortType.New;
147   }
148
149   static getPageFromProps(page: number): number {
150     return page ? Number(page) : 1;
151   }
152
153   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
154     let pathSplit = req.path.split("/");
155     let promises: Promise<any>[] = [];
156
157     // It can be /u/me, or /username/1
158     let idOrName = pathSplit[2];
159     let person_id: number;
160     let username: string;
161     if (isNaN(Number(idOrName))) {
162       username = idOrName;
163     } else {
164       person_id = Number(idOrName);
165     }
166
167     let view = this.getViewFromProps(pathSplit[4]);
168     let sort = this.getSortTypeFromProps(pathSplit[6]);
169     let page = this.getPageFromProps(Number(pathSplit[8]));
170
171     let form: GetPersonDetails = {
172       sort,
173       saved_only: view === PersonDetailsView.Saved,
174       page,
175       limit: fetchLimit,
176     };
177     setOptionalAuth(form, req.auth);
178     this.setIdOrName(form, person_id, username);
179     promises.push(req.client.getPersonDetails(form));
180     return promises;
181   }
182
183   static setIdOrName(obj: any, id: number, name_: string) {
184     if (id) {
185       obj.person_id = id;
186     } else {
187       obj.username = name_;
188     }
189   }
190
191   componentWillUnmount() {
192     this.subscription.unsubscribe();
193     saveScrollPosition(this.context);
194   }
195
196   static getDerivedStateFromProps(props: any): ProfileProps {
197     return {
198       view: this.getViewFromProps(props.match.params.view),
199       sort: this.getSortTypeFromProps(props.match.params.sort),
200       page: this.getPageFromProps(props.match.params.page),
201       person_id: Number(props.match.params.id) || null,
202       username: props.match.params.username,
203     };
204   }
205
206   componentDidUpdate(lastProps: any) {
207     // Necessary if you are on a post and you click another post (same route)
208     if (
209       lastProps.location.pathname.split("/")[2] !==
210       lastProps.history.location.pathname.split("/")[2]
211     ) {
212       // Couldnt get a refresh working. This does for now.
213       location.reload();
214     }
215   }
216
217   get documentTitle(): string {
218     return `@${this.state.personRes.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`;
219   }
220
221   get bioTag(): string {
222     return this.state.personRes.person_view.person.bio
223       ? previewLines(this.state.personRes.person_view.person.bio)
224       : undefined;
225   }
226
227   render() {
228     return (
229       <div class="container">
230         {this.state.loading ? (
231           <h5>
232             <Spinner large />
233           </h5>
234         ) : (
235           <div class="row">
236             <div class="col-12 col-md-8">
237               <>
238                 <HtmlTags
239                   title={this.documentTitle}
240                   path={this.context.router.route.match.url}
241                   description={this.bioTag}
242                   image={this.state.personRes.person_view.person.avatar}
243                 />
244                 {this.userInfo()}
245                 <hr />
246               </>
247               {!this.state.loading && this.selects()}
248               <PersonDetails
249                 personRes={this.state.personRes}
250                 admins={this.state.siteRes.admins}
251                 sort={this.state.sort}
252                 page={this.state.page}
253                 limit={fetchLimit}
254                 enableDownvotes={
255                   this.state.siteRes.site_view.site.enable_downvotes
256                 }
257                 enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
258                 view={this.state.view}
259                 onPageChange={this.handlePageChange}
260               />
261             </div>
262
263             {!this.state.loading && (
264               <div class="col-12 col-md-4">
265                 {this.moderates()}
266                 {this.isCurrentUser && this.follows()}
267               </div>
268             )}
269           </div>
270         )}
271       </div>
272     );
273   }
274
275   viewRadios() {
276     return (
277       <div class="btn-group btn-group-toggle flex-wrap mb-2">
278         <label
279           className={`btn btn-outline-secondary pointer
280             ${this.state.view == PersonDetailsView.Overview && "active"}
281           `}
282         >
283           <input
284             type="radio"
285             value={PersonDetailsView.Overview}
286             checked={this.state.view === PersonDetailsView.Overview}
287             onChange={linkEvent(this, this.handleViewChange)}
288           />
289           {i18n.t("overview")}
290         </label>
291         <label
292           className={`btn btn-outline-secondary pointer
293             ${this.state.view == PersonDetailsView.Comments && "active"}
294           `}
295         >
296           <input
297             type="radio"
298             value={PersonDetailsView.Comments}
299             checked={this.state.view == PersonDetailsView.Comments}
300             onChange={linkEvent(this, this.handleViewChange)}
301           />
302           {i18n.t("comments")}
303         </label>
304         <label
305           className={`btn btn-outline-secondary pointer
306             ${this.state.view == PersonDetailsView.Posts && "active"}
307           `}
308         >
309           <input
310             type="radio"
311             value={PersonDetailsView.Posts}
312             checked={this.state.view == PersonDetailsView.Posts}
313             onChange={linkEvent(this, this.handleViewChange)}
314           />
315           {i18n.t("posts")}
316         </label>
317         <label
318           className={`btn btn-outline-secondary pointer
319             ${this.state.view == PersonDetailsView.Saved && "active"}
320           `}
321         >
322           <input
323             type="radio"
324             value={PersonDetailsView.Saved}
325             checked={this.state.view == PersonDetailsView.Saved}
326             onChange={linkEvent(this, this.handleViewChange)}
327           />
328           {i18n.t("saved")}
329         </label>
330       </div>
331     );
332   }
333
334   selects() {
335     return (
336       <div className="mb-2">
337         <span class="mr-3">{this.viewRadios()}</span>
338         <SortSelect
339           sort={this.state.sort}
340           onChange={this.handleSortChange}
341           hideHot
342           hideMostComments
343         />
344         <a
345           href={`/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`}
346           rel="noopener"
347           title="RSS"
348         >
349           <Icon icon="rss" classes="text-muted small mx-2" />
350         </a>
351       </div>
352     );
353   }
354   handleBlockPerson(personId: number) {
355     if (personId != 0) {
356       let blockUserForm: BlockPerson = {
357         person_id: personId,
358         block: true,
359         auth: authField(),
360       };
361       WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
362     }
363   }
364   handleUnblockPerson(recipientId: number) {
365     let blockUserForm: BlockPerson = {
366       person_id: recipientId,
367       block: false,
368       auth: authField(),
369     };
370     WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
371   }
372
373   userInfo() {
374     let pv = this.state.personRes?.person_view;
375
376     return (
377       <div>
378         <BannerIconHeader banner={pv.person.banner} icon={pv.person.avatar} />
379         <div class="mb-3">
380           <div class="">
381             <div class="mb-0 d-flex flex-wrap">
382               <div>
383                 {pv.person.display_name && (
384                   <h5 class="mb-0">{pv.person.display_name}</h5>
385                 )}
386                 <ul class="list-inline mb-2">
387                   <li className="list-inline-item">
388                     <PersonListing
389                       person={pv.person}
390                       realLink
391                       useApubName
392                       muted
393                       hideAvatar
394                     />
395                   </li>
396                   {pv.person.banned && (
397                     <li className="list-inline-item badge badge-danger">
398                       {i18n.t("banned")}
399                     </li>
400                   )}
401                   {pv.person.admin && (
402                     <li className="list-inline-item badge badge-light">
403                       {i18n.t("admin")}
404                     </li>
405                   )}
406                   {pv.person.bot_account && (
407                     <li className="list-inline-item badge badge-light">
408                       {i18n.t("bot_account").toLowerCase()}
409                     </li>
410                   )}
411                 </ul>
412               </div>
413               <div className="flex-grow-1 unselectable pointer mx-2"></div>
414               {!this.isCurrentUser && (
415                 <>
416                   <a
417                     className={`d-flex align-self-start btn btn-secondary mr-2 ${
418                       !pv.person.matrix_user_id && "invisible"
419                     }`}
420                     rel="noopener"
421                     href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
422                   >
423                     {i18n.t("send_secure_message")}
424                   </a>
425                   <Link
426                     className={"d-flex align-self-start btn btn-secondary mr-2"}
427                     to={`/create_private_message/recipient/${pv.person.id}`}
428                   >
429                     {i18n.t("send_message")}
430                   </Link>
431                   {this.state.personBlocked ? (
432                     <button
433                       className={"d-flex align-self-start btn btn-secondary"}
434                       onClick={linkEvent(
435                         pv.person.id,
436                         this.handleUnblockPerson
437                       )}
438                     >
439                       {i18n.t("unblock_user")}
440                     </button>
441                   ) : (
442                     <button
443                       className={"d-flex align-self-start btn btn-secondary"}
444                       onClick={linkEvent(pv.person.id, this.handleBlockPerson)}
445                     >
446                       {i18n.t("block_user")}
447                     </button>
448                   )}
449                 </>
450               )}
451             </div>
452             {pv.person.bio && (
453               <div className="d-flex align-items-center mb-2">
454                 <div
455                   className="md-div"
456                   dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
457                 />
458               </div>
459             )}
460             <div>
461               <ul class="list-inline mb-2">
462                 <li className="list-inline-item badge badge-light">
463                   {i18n.t("number_of_posts", {
464                     count: pv.counts.post_count,
465                     formattedCount: numToSI(pv.counts.post_count),
466                   })}
467                 </li>
468                 <li className="list-inline-item badge badge-light">
469                   {i18n.t("number_of_comments", {
470                     count: pv.counts.comment_count,
471                     formattedCount: numToSI(pv.counts.comment_count),
472                   })}
473                 </li>
474               </ul>
475             </div>
476             <div class="text-muted">
477               {i18n.t("joined")}{" "}
478               <MomentTime data={pv.person} showAgo ignoreUpdated />
479             </div>
480             <div className="d-flex align-items-center text-muted mb-2">
481               <Icon icon="cake" />
482               <span className="ml-2">
483                 {i18n.t("cake_day_title")}{" "}
484                 {moment.utc(pv.person.published).local().format("MMM DD, YYYY")}
485               </span>
486             </div>
487           </div>
488         </div>
489       </div>
490     );
491   }
492
493   moderates() {
494     return (
495       <div>
496         {this.state.personRes.moderates.length > 0 && (
497           <div class="card border-secondary mb-3">
498             <div class="card-body">
499               <h5>{i18n.t("moderates")}</h5>
500               <ul class="list-unstyled mb-0">
501                 {this.state.personRes.moderates.map(cmv => (
502                   <li>
503                     <CommunityLink community={cmv.community} />
504                   </li>
505                 ))}
506               </ul>
507             </div>
508           </div>
509         )}
510       </div>
511     );
512   }
513
514   follows() {
515     let follows = UserService.Instance.myUserInfo.follows;
516     return (
517       <div>
518         {follows.length > 0 && (
519           <div class="card border-secondary mb-3">
520             <div class="card-body">
521               <h5>{i18n.t("subscribed")}</h5>
522               <ul class="list-unstyled mb-0">
523                 {follows.map(cfv => (
524                   <li>
525                     <CommunityLink community={cfv.community} />
526                   </li>
527                 ))}
528               </ul>
529             </div>
530           </div>
531         )}
532       </div>
533     );
534   }
535
536   updateUrl(paramUpdates: UrlParams) {
537     const page = paramUpdates.page || this.state.page;
538     const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
539     const sortStr = paramUpdates.sort || this.state.sort;
540
541     let typeView = `/u/${this.state.userName}`;
542
543     this.props.history.push(
544       `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
545     );
546     this.state.loading = true;
547     this.setState(this.state);
548     this.fetchUserData();
549   }
550
551   handlePageChange(page: number) {
552     this.updateUrl({ page });
553   }
554
555   handleSortChange(val: SortType) {
556     this.updateUrl({ sort: val, page: 1 });
557   }
558
559   handleViewChange(i: Profile, event: any) {
560     i.updateUrl({
561       view: PersonDetailsView[Number(event.target.value)],
562       page: 1,
563     });
564   }
565
566   parseMessage(msg: any) {
567     let op = wsUserOp(msg);
568     console.log(msg);
569     if (msg.error) {
570       toast(i18n.t(msg.error), "danger");
571       if (msg.error == "couldnt_find_that_username_or_email") {
572         this.context.router.history.push("/");
573       }
574       return;
575     } else if (msg.reconnect) {
576       this.fetchUserData();
577     } else if (op == UserOperation.GetPersonDetails) {
578       // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
579       // and set the parent state if it is not set or differs
580       // TODO this might need to get abstracted
581       let data = wsJsonToRes<GetPersonDetailsResponse>(msg).data;
582       this.state.personRes = data;
583       console.log(data);
584       this.state.loading = false;
585       this.setPersonBlock();
586       this.setState(this.state);
587       restoreScrollPosition(this.context);
588     } else if (op == UserOperation.AddAdmin) {
589       let data = wsJsonToRes<AddAdminResponse>(msg).data;
590       this.state.siteRes.admins = data.admins;
591       this.setState(this.state);
592     } else if (op == UserOperation.CreateCommentLike) {
593       let data = wsJsonToRes<CommentResponse>(msg).data;
594       createCommentLikeRes(data.comment_view, this.state.personRes.comments);
595       this.setState(this.state);
596     } else if (
597       op == UserOperation.EditComment ||
598       op == UserOperation.DeleteComment ||
599       op == UserOperation.RemoveComment
600     ) {
601       let data = wsJsonToRes<CommentResponse>(msg).data;
602       editCommentRes(data.comment_view, this.state.personRes.comments);
603       this.setState(this.state);
604     } else if (op == UserOperation.CreateComment) {
605       let data = wsJsonToRes<CommentResponse>(msg).data;
606       if (
607         UserService.Instance.myUserInfo &&
608         data.comment_view.creator.id ==
609           UserService.Instance.myUserInfo.local_user_view.person.id
610       ) {
611         toast(i18n.t("reply_sent"));
612       }
613     } else if (op == UserOperation.SaveComment) {
614       let data = wsJsonToRes<CommentResponse>(msg).data;
615       saveCommentRes(data.comment_view, this.state.personRes.comments);
616       this.setState(this.state);
617     } else if (
618       op == UserOperation.EditPost ||
619       op == UserOperation.DeletePost ||
620       op == UserOperation.RemovePost ||
621       op == UserOperation.LockPost ||
622       op == UserOperation.StickyPost ||
623       op == UserOperation.SavePost
624     ) {
625       let data = wsJsonToRes<PostResponse>(msg).data;
626       editPostFindRes(data.post_view, this.state.personRes.posts);
627       this.setState(this.state);
628     } else if (op == UserOperation.CreatePostLike) {
629       let data = wsJsonToRes<PostResponse>(msg).data;
630       createPostLikeFindRes(data.post_view, this.state.personRes.posts);
631       this.setState(this.state);
632     } else if (op == UserOperation.BanPerson) {
633       let data = wsJsonToRes<BanPersonResponse>(msg).data;
634       this.state.personRes.comments
635         .filter(c => c.creator.id == data.person_view.person.id)
636         .forEach(c => (c.creator.banned = data.banned));
637       this.state.personRes.posts
638         .filter(c => c.creator.id == data.person_view.person.id)
639         .forEach(c => (c.creator.banned = data.banned));
640       this.setState(this.state);
641     } else if (op == UserOperation.BlockPerson) {
642       let data = wsJsonToRes<BlockPersonResponse>(msg).data;
643       updatePersonBlock(data);
644       this.setPersonBlock();
645       this.setState(this.state);
646     }
647   }
648 }