]> Untitled Git - lemmy-ui.git/blobdiff - src/shared/components/person/profile.tsx
Adding Community Language fixes. #783 (#868)
[lemmy-ui.git] / src / shared / components / person / profile.tsx
index 0f567d2c4230d6a3593f00e84f2892bd40ab6d3a..47399843cab521e119195d8f15d02d9e1a2a8ad0 100644 (file)
@@ -1,7 +1,9 @@
+import { None, Option, Some } from "@sniptt/monads";
 import { Component, linkEvent } from "inferno";
 import { Link } from "inferno-router";
 import {
   AddAdminResponse,
+  BanPerson,
   BanPersonResponse,
   BlockPerson,
   BlockPersonResponse,
@@ -10,8 +12,12 @@ import {
   GetPersonDetailsResponse,
   GetSiteResponse,
   PostResponse,
+  PurgeItemResponse,
   SortType,
+  toUndefined,
   UserOperation,
+  wsJsonToRes,
+  wsUserOp,
 } from "lemmy-js-client";
 import moment from "moment";
 import { Subscription } from "rxjs";
@@ -19,29 +25,33 @@ import { i18n } from "../../i18next";
 import { InitialFetchRequest, PersonDetailsView } from "../../interfaces";
 import { UserService, WebSocketService } from "../../services";
 import {
-  authField,
+  auth,
+  canMod,
+  capitalizeFirstLetter,
   createCommentLikeRes,
   createPostLikeFindRes,
   editCommentRes,
   editPostFindRes,
+  enableDownvotes,
+  enableNsfw,
   fetchLimit,
+  futureDaysToUnixTime,
   getUsernameFromProps,
+  isAdmin,
+  isBanned,
   mdToHtml,
   numToSI,
-  previewLines,
+  relTags,
   restoreScrollPosition,
   routeSortTypeToEnum,
   saveCommentRes,
   saveScrollPosition,
   setIsoData,
-  setOptionalAuth,
   setupTippy,
   toast,
   updatePersonBlock,
   wsClient,
-  wsJsonToRes,
   wsSubscribe,
-  wsUserOp,
 } from "../../utils";
 import { BannerIconHeader } from "../common/banner-icon-header";
 import { HtmlTags } from "../common/html-tags";
@@ -53,13 +63,17 @@ import { PersonDetails } from "./person-details";
 import { PersonListing } from "./person-listing";
 
 interface ProfileState {
-  personRes: GetPersonDetailsResponse;
+  personRes: Option<GetPersonDetailsResponse>;
   userName: string;
   view: PersonDetailsView;
   sort: SortType;
   page: number;
   loading: boolean;
   personBlocked: boolean;
+  banReason: Option<string>;
+  banExpireDays: Option<number>;
+  showBanDialog: boolean;
+  removeData: boolean;
   siteRes: GetSiteResponse;
 }
 
@@ -78,10 +92,10 @@ interface UrlParams {
 }
 
 export class Profile extends Component<any, ProfileState> {
-  private isoData = setIsoData(this.context);
+  private isoData = setIsoData(this.context, GetPersonDetailsResponse);
   private subscription: Subscription;
   private emptyState: ProfileState = {
-    personRes: undefined,
+    personRes: None,
     userName: getUsernameFromProps(this.props),
     loading: true,
     view: Profile.getViewFromProps(this.props.match.view),
@@ -89,6 +103,10 @@ export class Profile extends Component<any, ProfileState> {
     page: Profile.getPageFromProps(this.props.match.page),
     personBlocked: false,
     siteRes: this.isoData.site_res,
+    showBanDialog: false,
+    banReason: null,
+    banExpireDays: null,
+    removeData: false,
   };
 
   constructor(props: any, context: any) {
@@ -103,38 +121,56 @@ export class Profile extends Component<any, ProfileState> {
 
     // Only fetch the data if coming from another route
     if (this.isoData.path == this.context.router.route.match.url) {
-      this.state.personRes = this.isoData.routeData[0];
-      this.state.loading = false;
+      this.state = {
+        ...this.state,
+        personRes: Some(this.isoData.routeData[0] as GetPersonDetailsResponse),
+        loading: false,
+      };
     } else {
       this.fetchUserData();
     }
-
-    this.setPersonBlock();
   }
 
   fetchUserData() {
-    let form: GetPersonDetails = {
-      username: this.state.userName,
-      sort: this.state.sort,
-      saved_only: this.state.view === PersonDetailsView.Saved,
-      page: this.state.page,
-      limit: fetchLimit,
-      auth: authField(false),
-    };
+    let form = new GetPersonDetails({
+      username: Some(this.state.userName),
+      person_id: None,
+      community_id: None,
+      sort: Some(this.state.sort),
+      saved_only: Some(this.state.view === PersonDetailsView.Saved),
+      page: Some(this.state.page),
+      limit: Some(fetchLimit),
+      auth: auth(false).ok(),
+    });
     WebSocketService.Instance.send(wsClient.getPersonDetails(form));
   }
 
-  get isCurrentUser() {
-    return (
-      UserService.Instance.myUserInfo?.local_user_view.person.id ==
-      this.state.personRes.person_view.person.id
-    );
+  get amCurrentUser() {
+    return UserService.Instance.myUserInfo.match({
+      some: mui =>
+        this.state.personRes.match({
+          some: res =>
+            mui.local_user_view.person.id == res.person_view.person.id,
+          none: false,
+        }),
+      none: false,
+    });
   }
 
   setPersonBlock() {
-    this.state.personBlocked = UserService.Instance.myUserInfo?.person_blocks
-      .map(a => a.target.id)
-      .includes(this.state.personRes?.person_view.person.id);
+    UserService.Instance.myUserInfo.match({
+      some: mui =>
+        this.state.personRes.match({
+          some: res =>
+            this.setState({
+              personBlocked: mui.person_blocks
+                .map(a => a.target.id)
+                .includes(res.person_view.person.id),
+            }),
+          none: void 0,
+        }),
+      none: void 0,
+    });
   }
 
   static getViewFromProps(view: string): PersonDetailsView {
@@ -151,43 +187,27 @@ export class Profile extends Component<any, ProfileState> {
 
   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
     let pathSplit = req.path.split("/");
-    let promises: Promise<any>[] = [];
-
-    // It can be /u/me, or /username/1
-    let idOrName = pathSplit[2];
-    let person_id: number;
-    let username: string;
-    if (isNaN(Number(idOrName))) {
-      username = idOrName;
-    } else {
-      person_id = Number(idOrName);
-    }
 
+    let username = pathSplit[2];
     let view = this.getViewFromProps(pathSplit[4]);
-    let sort = this.getSortTypeFromProps(pathSplit[6]);
-    let page = this.getPageFromProps(Number(pathSplit[8]));
+    let sort = Some(this.getSortTypeFromProps(pathSplit[6]));
+    let page = Some(this.getPageFromProps(Number(pathSplit[8])));
 
-    let form: GetPersonDetails = {
+    let form = new GetPersonDetails({
+      username: Some(username),
+      person_id: None,
+      community_id: None,
       sort,
-      saved_only: view === PersonDetailsView.Saved,
+      saved_only: Some(view === PersonDetailsView.Saved),
       page,
-      limit: fetchLimit,
-    };
-    setOptionalAuth(form, req.auth);
-    this.setIdOrName(form, person_id, username);
-    promises.push(req.client.getPersonDetails(form));
-    return promises;
-  }
-
-  static setIdOrName(obj: any, id: number, name_: string) {
-    if (id) {
-      obj.person_id = id;
-    } else {
-      obj.username = name_;
-    }
+      limit: Some(fetchLimit),
+      auth: req.auth,
+    });
+    return [req.client.getPersonDetails(form)];
   }
 
   componentDidMount() {
+    this.setPersonBlock();
     setupTippy();
   }
 
@@ -218,58 +238,61 @@ export class Profile extends Component<any, ProfileState> {
   }
 
   get documentTitle(): string {
-    return `@${this.state.personRes.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`;
-  }
-
-  get bioTag(): string {
-    return this.state.personRes.person_view.person.bio
-      ? previewLines(this.state.personRes.person_view.person.bio)
-      : undefined;
+    return this.state.personRes.match({
+      some: res =>
+        `@${res.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`,
+      none: "",
+    });
   }
 
   render() {
     return (
-      <div class="container">
+      <div className="container-lg">
         {this.state.loading ? (
           <h5>
             <Spinner large />
           </h5>
         ) : (
-          <div class="row">
-            <div class="col-12 col-md-8">
-              <>
-                <HtmlTags
-                  title={this.documentTitle}
-                  path={this.context.router.route.match.url}
-                  description={this.bioTag}
-                  image={this.state.personRes.person_view.person.avatar}
-                />
-                {this.userInfo()}
-                <hr />
-              </>
-              {!this.state.loading && this.selects()}
-              <PersonDetails
-                personRes={this.state.personRes}
-                admins={this.state.siteRes.admins}
-                sort={this.state.sort}
-                page={this.state.page}
-                limit={fetchLimit}
-                enableDownvotes={
-                  this.state.siteRes.site_view.site.enable_downvotes
-                }
-                enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
-                view={this.state.view}
-                onPageChange={this.handlePageChange}
-              />
-            </div>
-
-            {!this.state.loading && (
-              <div class="col-12 col-md-4">
-                {this.moderates()}
-                {this.isCurrentUser && this.follows()}
+          this.state.personRes.match({
+            some: res => (
+              <div className="row">
+                <div className="col-12 col-md-8">
+                  <>
+                    <HtmlTags
+                      title={this.documentTitle}
+                      path={this.context.router.route.match.url}
+                      description={res.person_view.person.bio}
+                      image={res.person_view.person.avatar}
+                    />
+                    {this.userInfo()}
+                    <hr />
+                  </>
+                  {!this.state.loading && this.selects()}
+                  <PersonDetails
+                    personRes={res}
+                    admins={this.state.siteRes.admins}
+                    sort={this.state.sort}
+                    page={this.state.page}
+                    limit={fetchLimit}
+                    enableDownvotes={enableDownvotes(this.state.siteRes)}
+                    enableNsfw={enableNsfw(this.state.siteRes)}
+                    view={this.state.view}
+                    onPageChange={this.handlePageChange}
+                    allLanguages={this.state.siteRes.all_languages}
+                    siteLanguages={this.state.siteRes.discussion_languages}
+                  />
+                </div>
+
+                {!this.state.loading && (
+                  <div className="col-12 col-md-4">
+                    {this.moderates()}
+                    {this.amCurrentUser && this.follows()}
+                  </div>
+                )}
               </div>
-            )}
-          </div>
+            ),
+            none: <></>,
+          })
         )}
       </div>
     );
@@ -277,7 +300,7 @@ export class Profile extends Component<any, ProfileState> {
 
   viewRadios() {
     return (
-      <div class="btn-group btn-group-toggle flex-wrap mb-2">
+      <div className="btn-group btn-group-toggle flex-wrap mb-2">
         <label
           className={`btn btn-outline-secondary pointer
             ${this.state.view == PersonDetailsView.Overview && "active"}
@@ -339,14 +362,14 @@ export class Profile extends Component<any, ProfileState> {
 
     return (
       <div className="mb-2">
-        <span class="mr-3">{this.viewRadios()}</span>
+        <span className="mr-3">{this.viewRadios()}</span>
         <SortSelect
           sort={this.state.sort}
           onChange={this.handleSortChange}
           hideHot
           hideMostComments
         />
-        <a href={profileRss} rel="noopener" title="RSS">
+        <a href={profileRss} rel={relTags} title="RSS">
           <Icon icon="rss" classes="text-muted small mx-2" />
         </a>
         <link rel="alternate" type="application/atom+xml" href={profileRss} />
@@ -355,184 +378,348 @@ export class Profile extends Component<any, ProfileState> {
   }
   handleBlockPerson(personId: number) {
     if (personId != 0) {
-      let blockUserForm: BlockPerson = {
+      let blockUserForm = new BlockPerson({
         person_id: personId,
         block: true,
-        auth: authField(),
-      };
+        auth: auth().unwrap(),
+      });
       WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
     }
   }
   handleUnblockPerson(recipientId: number) {
-    let blockUserForm: BlockPerson = {
+    let blockUserForm = new BlockPerson({
       person_id: recipientId,
       block: false,
-      auth: authField(),
-    };
+      auth: auth().unwrap(),
+    });
     WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
   }
 
   userInfo() {
-    let pv = this.state.personRes?.person_view;
-
-    return (
-      <div>
-        <BannerIconHeader banner={pv.person.banner} icon={pv.person.avatar} />
-        <div class="mb-3">
-          <div class="">
-            <div class="mb-0 d-flex flex-wrap">
-              <div>
-                {pv.person.display_name && (
-                  <h5 class="mb-0">{pv.person.display_name}</h5>
-                )}
-                <ul class="list-inline mb-2">
-                  <li className="list-inline-item">
-                    <PersonListing
-                      person={pv.person}
-                      realLink
-                      useApubName
-                      muted
-                      hideAvatar
-                    />
-                  </li>
-                  {pv.person.banned && (
-                    <li className="list-inline-item badge badge-danger">
-                      {i18n.t("banned")}
-                    </li>
-                  )}
-                  {pv.person.admin && (
+    return this.state.personRes
+      .map(r => r.person_view)
+      .match({
+        some: pv => (
+          <div>
+            <BannerIconHeader
+              banner={pv.person.banner}
+              icon={pv.person.avatar}
+            />
+            <div className="mb-3">
+              <div className="">
+                <div className="mb-0 d-flex flex-wrap">
+                  <div>
+                    {pv.person.display_name.match({
+                      some: displayName => (
+                        <h5 className="mb-0">{displayName}</h5>
+                      ),
+                      none: <></>,
+                    })}
+                    <ul className="list-inline mb-2">
+                      <li className="list-inline-item">
+                        <PersonListing
+                          person={pv.person}
+                          realLink
+                          useApubName
+                          muted
+                          hideAvatar
+                        />
+                      </li>
+                      {isBanned(pv.person) && (
+                        <li className="list-inline-item badge badge-danger">
+                          {i18n.t("banned")}
+                        </li>
+                      )}
+                      {pv.person.deleted && (
+                        <li className="list-inline-item badge badge-danger">
+                          {i18n.t("deleted")}
+                        </li>
+                      )}
+                      {pv.person.admin && (
+                        <li className="list-inline-item badge badge-light">
+                          {i18n.t("admin")}
+                        </li>
+                      )}
+                      {pv.person.bot_account && (
+                        <li className="list-inline-item badge badge-light">
+                          {i18n.t("bot_account").toLowerCase()}
+                        </li>
+                      )}
+                    </ul>
+                  </div>
+                  {this.banDialog()}
+                  <div className="flex-grow-1 unselectable pointer mx-2"></div>
+                  {!this.amCurrentUser &&
+                    UserService.Instance.myUserInfo.isSome() && (
+                      <>
+                        <a
+                          className={`d-flex align-self-start btn btn-secondary mr-2 ${
+                            !pv.person.matrix_user_id && "invisible"
+                          }`}
+                          rel={relTags}
+                          href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
+                        >
+                          {i18n.t("send_secure_message")}
+                        </a>
+                        <Link
+                          className={
+                            "d-flex align-self-start btn btn-secondary mr-2"
+                          }
+                          to={`/create_private_message/recipient/${pv.person.id}`}
+                        >
+                          {i18n.t("send_message")}
+                        </Link>
+                        {this.state.personBlocked ? (
+                          <button
+                            className={
+                              "d-flex align-self-start btn btn-secondary mr-2"
+                            }
+                            onClick={linkEvent(
+                              pv.person.id,
+                              this.handleUnblockPerson
+                            )}
+                          >
+                            {i18n.t("unblock_user")}
+                          </button>
+                        ) : (
+                          <button
+                            className={
+                              "d-flex align-self-start btn btn-secondary mr-2"
+                            }
+                            onClick={linkEvent(
+                              pv.person.id,
+                              this.handleBlockPerson
+                            )}
+                          >
+                            {i18n.t("block_user")}
+                          </button>
+                        )}
+                      </>
+                    )}
+
+                  {canMod(
+                    None,
+                    Some(this.state.siteRes.admins),
+                    pv.person.id
+                  ) &&
+                    !isAdmin(Some(this.state.siteRes.admins), pv.person.id) &&
+                    !this.state.showBanDialog &&
+                    (!isBanned(pv.person) ? (
+                      <button
+                        className={
+                          "d-flex align-self-start btn btn-secondary mr-2"
+                        }
+                        onClick={linkEvent(this, this.handleModBanShow)}
+                        aria-label={i18n.t("ban")}
+                      >
+                        {capitalizeFirstLetter(i18n.t("ban"))}
+                      </button>
+                    ) : (
+                      <button
+                        className={
+                          "d-flex align-self-start btn btn-secondary mr-2"
+                        }
+                        onClick={linkEvent(this, this.handleModBanSubmit)}
+                        aria-label={i18n.t("unban")}
+                      >
+                        {capitalizeFirstLetter(i18n.t("unban"))}
+                      </button>
+                    ))}
+                </div>
+                {pv.person.bio.match({
+                  some: bio => (
+                    <div className="d-flex align-items-center mb-2">
+                      <div
+                        className="md-div"
+                        dangerouslySetInnerHTML={mdToHtml(bio)}
+                      />
+                    </div>
+                  ),
+                  none: <></>,
+                })}
+                <div>
+                  <ul className="list-inline mb-2">
                     <li className="list-inline-item badge badge-light">
-                      {i18n.t("admin")}
+                      {i18n.t("number_of_posts", {
+                        count: pv.counts.post_count,
+                        formattedCount: numToSI(pv.counts.post_count),
+                      })}
                     </li>
-                  )}
-                  {pv.person.bot_account && (
                     <li className="list-inline-item badge badge-light">
-                      {i18n.t("bot_account").toLowerCase()}
+                      {i18n.t("number_of_comments", {
+                        count: pv.counts.comment_count,
+                        formattedCount: numToSI(pv.counts.comment_count),
+                      })}
                     </li>
-                  )}
-                </ul>
+                  </ul>
+                </div>
+                <div className="text-muted">
+                  {i18n.t("joined")}{" "}
+                  <MomentTime
+                    published={pv.person.published}
+                    updated={None}
+                    showAgo
+                    ignoreUpdated
+                  />
+                </div>
+                <div className="d-flex align-items-center text-muted mb-2">
+                  <Icon icon="cake" />
+                  <span className="ml-2">
+                    {i18n.t("cake_day_title")}{" "}
+                    {moment
+                      .utc(pv.person.published)
+                      .local()
+                      .format("MMM DD, YYYY")}
+                  </span>
+                </div>
               </div>
-              <div className="flex-grow-1 unselectable pointer mx-2"></div>
-              {!this.isCurrentUser && UserService.Instance.myUserInfo && (
-                <>
-                  <a
-                    className={`d-flex align-self-start btn btn-secondary mr-2 ${
-                      !pv.person.matrix_user_id && "invisible"
-                    }`}
-                    rel="noopener"
-                    href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
+            </div>
+          </div>
+        ),
+        none: <></>,
+      });
+  }
+
+  banDialog() {
+    return this.state.personRes
+      .map(r => r.person_view)
+      .match({
+        some: pv => (
+          <>
+            {this.state.showBanDialog && (
+              <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
+                <div className="form-group row col-12">
+                  <label
+                    className="col-form-label"
+                    htmlFor="profile-ban-reason"
                   >
-                    {i18n.t("send_secure_message")}
-                  </a>
-                  <Link
-                    className={"d-flex align-self-start btn btn-secondary mr-2"}
-                    to={`/create_private_message/recipient/${pv.person.id}`}
+                    {i18n.t("reason")}
+                  </label>
+                  <input
+                    type="text"
+                    id="profile-ban-reason"
+                    className="form-control mr-2"
+                    placeholder={i18n.t("reason")}
+                    value={toUndefined(this.state.banReason)}
+                    onInput={linkEvent(this, this.handleModBanReasonChange)}
+                  />
+                  <label className="col-form-label" htmlFor={`mod-ban-expires`}>
+                    {i18n.t("expires")}
+                  </label>
+                  <input
+                    type="number"
+                    id={`mod-ban-expires`}
+                    className="form-control mr-2"
+                    placeholder={i18n.t("number_of_days")}
+                    value={toUndefined(this.state.banExpireDays)}
+                    onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
+                  />
+                  <div className="form-group">
+                    <div className="form-check">
+                      <input
+                        className="form-check-input"
+                        id="mod-ban-remove-data"
+                        type="checkbox"
+                        checked={this.state.removeData}
+                        onChange={linkEvent(
+                          this,
+                          this.handleModRemoveDataChange
+                        )}
+                      />
+                      <label
+                        className="form-check-label"
+                        htmlFor="mod-ban-remove-data"
+                        title={i18n.t("remove_content_more")}
+                      >
+                        {i18n.t("remove_content")}
+                      </label>
+                    </div>
+                  </div>
+                </div>
+                {/* TODO hold off on expires until later */}
+                {/* <div class="form-group row"> */}
+                {/*   <label class="col-form-label">Expires</label> */}
+                {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
+                {/* </div> */}
+                <div className="form-group row">
+                  <button
+                    type="reset"
+                    className="btn btn-secondary mr-2"
+                    aria-label={i18n.t("cancel")}
+                    onClick={linkEvent(this, this.handleModBanSubmitCancel)}
                   >
-                    {i18n.t("send_message")}
-                  </Link>
-                  {this.state.personBlocked ? (
-                    <button
-                      className={"d-flex align-self-start btn btn-secondary"}
-                      onClick={linkEvent(
-                        pv.person.id,
-                        this.handleUnblockPerson
-                      )}
-                    >
-                      {i18n.t("unblock_user")}
-                    </button>
-                  ) : (
-                    <button
-                      className={"d-flex align-self-start btn btn-secondary"}
-                      onClick={linkEvent(pv.person.id, this.handleBlockPerson)}
-                    >
-                      {i18n.t("block_user")}
-                    </button>
-                  )}
-                </>
-              )}
-            </div>
-            {pv.person.bio && (
-              <div className="d-flex align-items-center mb-2">
-                <div
-                  className="md-div"
-                  dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
-                />
-              </div>
+                    {i18n.t("cancel")}
+                  </button>
+                  <button
+                    type="submit"
+                    className="btn btn-secondary"
+                    aria-label={i18n.t("ban")}
+                  >
+                    {i18n.t("ban")} {pv.person.name}
+                  </button>
+                </div>
+              </form>
             )}
-            <div>
-              <ul class="list-inline mb-2">
-                <li className="list-inline-item badge badge-light">
-                  {i18n.t("number_of_posts", {
-                    count: pv.counts.post_count,
-                    formattedCount: numToSI(pv.counts.post_count),
-                  })}
-                </li>
-                <li className="list-inline-item badge badge-light">
-                  {i18n.t("number_of_comments", {
-                    count: pv.counts.comment_count,
-                    formattedCount: numToSI(pv.counts.comment_count),
-                  })}
-                </li>
-              </ul>
-            </div>
-            <div class="text-muted">
-              {i18n.t("joined")}{" "}
-              <MomentTime data={pv.person} showAgo ignoreUpdated />
-            </div>
-            <div className="d-flex align-items-center text-muted mb-2">
-              <Icon icon="cake" />
-              <span className="ml-2">
-                {i18n.t("cake_day_title")}{" "}
-                {moment.utc(pv.person.published).local().format("MMM DD, YYYY")}
-              </span>
-            </div>
-          </div>
-        </div>
-      </div>
-    );
+          </>
+        ),
+        none: <></>,
+      });
   }
 
   moderates() {
-    return (
-      <div>
-        {this.state.personRes.moderates.length > 0 && (
-          <div class="card border-secondary mb-3">
-            <div class="card-body">
-              <h5>{i18n.t("moderates")}</h5>
-              <ul class="list-unstyled mb-0">
-                {this.state.personRes.moderates.map(cmv => (
-                  <li>
-                    <CommunityLink community={cmv.community} />
-                  </li>
-                ))}
-              </ul>
-            </div>
-          </div>
-        )}
-      </div>
-    );
+    return this.state.personRes
+      .map(r => r.moderates)
+      .match({
+        some: moderates => {
+          if (moderates.length > 0) {
+            return (
+              <div className="card border-secondary mb-3">
+                <div className="card-body">
+                  <h5>{i18n.t("moderates")}</h5>
+                  <ul className="list-unstyled mb-0">
+                    {moderates.map(cmv => (
+                      <li key={cmv.community.id}>
+                        <CommunityLink community={cmv.community} />
+                      </li>
+                    ))}
+                  </ul>
+                </div>
+              </div>
+            );
+          } else {
+            return <></>;
+          }
+        },
+        none: void 0,
+      });
   }
 
   follows() {
-    let follows = UserService.Instance.myUserInfo.follows;
-    return (
-      <div>
-        {follows.length > 0 && (
-          <div class="card border-secondary mb-3">
-            <div class="card-body">
-              <h5>{i18n.t("subscribed")}</h5>
-              <ul class="list-unstyled mb-0">
-                {follows.map(cfv => (
-                  <li>
-                    <CommunityLink community={cfv.community} />
-                  </li>
-                ))}
-              </ul>
-            </div>
-          </div>
-        )}
-      </div>
-    );
+    return UserService.Instance.myUserInfo
+      .map(m => m.follows)
+      .match({
+        some: follows => {
+          if (follows.length > 0) {
+            return (
+              <div className="card border-secondary mb-3">
+                <div className="card-body">
+                  <h5>{i18n.t("subscribed")}</h5>
+                  <ul className="list-unstyled mb-0">
+                    {follows.map(cfv => (
+                      <li key={cfv.community.id}>
+                        <CommunityLink community={cfv.community} />
+                      </li>
+                    ))}
+                  </ul>
+                </div>
+              </div>
+            );
+          } else {
+            return <></>;
+          }
+        },
+        none: void 0,
+      });
   }
 
   updateUrl(paramUpdates: UrlParams) {
@@ -545,13 +732,12 @@ export class Profile extends Component<any, ProfileState> {
     this.props.history.push(
       `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
     );
-    this.state.loading = true;
-    this.setState(this.state);
+    this.setState({ loading: true });
     this.fetchUserData();
   }
 
   handlePageChange(page: number) {
-    this.updateUrl({ page });
+    this.updateUrl({ page: page });
   }
 
   handleSortChange(val: SortType) {
@@ -565,6 +751,55 @@ export class Profile extends Component<any, ProfileState> {
     });
   }
 
+  handleModBanShow(i: Profile) {
+    i.setState({ showBanDialog: true });
+  }
+
+  handleModBanReasonChange(i: Profile, event: any) {
+    i.setState({ banReason: event.target.value });
+  }
+
+  handleModBanExpireDaysChange(i: Profile, event: any) {
+    i.setState({ banExpireDays: event.target.value });
+  }
+
+  handleModRemoveDataChange(i: Profile, event: any) {
+    i.setState({ removeData: event.target.checked });
+  }
+
+  handleModBanSubmitCancel(i: Profile, event?: any) {
+    event.preventDefault();
+    i.setState({ showBanDialog: false });
+  }
+
+  handleModBanSubmit(i: Profile, event?: any) {
+    if (event) event.preventDefault();
+
+    i.state.personRes
+      .map(r => r.person_view.person)
+      .match({
+        some: person => {
+          // If its an unban, restore all their data
+          let ban = !person.banned;
+          if (ban == false) {
+            i.setState({ removeData: false });
+          }
+          let form = new BanPerson({
+            person_id: person.id,
+            ban,
+            remove_data: Some(i.state.removeData),
+            reason: i.state.banReason,
+            expires: i.state.banExpireDays.map(futureDaysToUnixTime),
+            auth: auth().unwrap(),
+          });
+          WebSocketService.Instance.send(wsClient.banPerson(form));
+
+          i.setState({ showBanDialog: false });
+        },
+        none: void 0,
+      });
+  }
+
   parseMessage(msg: any) {
     let op = wsUserOp(msg);
     console.log(msg);
@@ -580,71 +815,107 @@ export class Profile extends Component<any, ProfileState> {
       // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
       // and set the parent state if it is not set or differs
       // TODO this might need to get abstracted
-      let data = wsJsonToRes<GetPersonDetailsResponse>(msg).data;
-      this.state.personRes = data;
-      console.log(data);
-      this.state.loading = false;
+      let data = wsJsonToRes<GetPersonDetailsResponse>(
+        msg,
+        GetPersonDetailsResponse
+      );
+      this.setState({ personRes: Some(data), loading: false });
       this.setPersonBlock();
-      this.setState(this.state);
       restoreScrollPosition(this.context);
     } else if (op == UserOperation.AddAdmin) {
-      let data = wsJsonToRes<AddAdminResponse>(msg).data;
-      this.state.siteRes.admins = data.admins;
-      this.setState(this.state);
+      let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse);
+      this.setState(s => ((s.siteRes.admins = data.admins), s));
     } else if (op == UserOperation.CreateCommentLike) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      createCommentLikeRes(data.comment_view, this.state.personRes.comments);
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
+      createCommentLikeRes(
+        data.comment_view,
+        this.state.personRes.map(r => r.comments).unwrapOr([])
+      );
       this.setState(this.state);
     } else if (
       op == UserOperation.EditComment ||
       op == UserOperation.DeleteComment ||
       op == UserOperation.RemoveComment
     ) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      editCommentRes(data.comment_view, this.state.personRes.comments);
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
+      editCommentRes(
+        data.comment_view,
+        this.state.personRes.map(r => r.comments).unwrapOr([])
+      );
       this.setState(this.state);
     } else if (op == UserOperation.CreateComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      if (
-        UserService.Instance.myUserInfo &&
-        data.comment_view.creator.id ==
-          UserService.Instance.myUserInfo?.local_user_view.person.id
-      ) {
-        toast(i18n.t("reply_sent"));
-      }
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
+      UserService.Instance.myUserInfo.match({
+        some: mui => {
+          if (data.comment_view.creator.id == mui.local_user_view.person.id) {
+            toast(i18n.t("reply_sent"));
+          }
+        },
+        none: void 0,
+      });
     } else if (op == UserOperation.SaveComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      saveCommentRes(data.comment_view, this.state.personRes.comments);
+      let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
+      saveCommentRes(
+        data.comment_view,
+        this.state.personRes.map(r => r.comments).unwrapOr([])
+      );
       this.setState(this.state);
     } else if (
       op == UserOperation.EditPost ||
       op == UserOperation.DeletePost ||
       op == UserOperation.RemovePost ||
       op == UserOperation.LockPost ||
-      op == UserOperation.StickyPost ||
+      op == UserOperation.FeaturePost ||
       op == UserOperation.SavePost
     ) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
-      editPostFindRes(data.post_view, this.state.personRes.posts);
+      let data = wsJsonToRes<PostResponse>(msg, PostResponse);
+      editPostFindRes(
+        data.post_view,
+        this.state.personRes.map(r => r.posts).unwrapOr([])
+      );
       this.setState(this.state);
     } else if (op == UserOperation.CreatePostLike) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
-      createPostLikeFindRes(data.post_view, this.state.personRes.posts);
+      let data = wsJsonToRes<PostResponse>(msg, PostResponse);
+      createPostLikeFindRes(
+        data.post_view,
+        this.state.personRes.map(r => r.posts).unwrapOr([])
+      );
       this.setState(this.state);
     } else if (op == UserOperation.BanPerson) {
-      let data = wsJsonToRes<BanPersonResponse>(msg).data;
-      this.state.personRes.comments
-        .filter(c => c.creator.id == data.person_view.person.id)
-        .forEach(c => (c.creator.banned = data.banned));
-      this.state.personRes.posts
-        .filter(c => c.creator.id == data.person_view.person.id)
-        .forEach(c => (c.creator.banned = data.banned));
-      this.setState(this.state);
+      let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse);
+      this.state.personRes.match({
+        some: res => {
+          res.comments
+            .filter(c => c.creator.id == data.person_view.person.id)
+            .forEach(c => (c.creator.banned = data.banned));
+          res.posts
+            .filter(c => c.creator.id == data.person_view.person.id)
+            .forEach(c => (c.creator.banned = data.banned));
+          let pv = res.person_view;
+
+          if (pv.person.id == data.person_view.person.id) {
+            pv.person.banned = data.banned;
+          }
+          this.setState(this.state);
+        },
+        none: void 0,
+      });
     } else if (op == UserOperation.BlockPerson) {
-      let data = wsJsonToRes<BlockPersonResponse>(msg).data;
+      let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
       updatePersonBlock(data);
       this.setPersonBlock();
       this.setState(this.state);
+    } else if (
+      op == UserOperation.PurgePerson ||
+      op == UserOperation.PurgePost ||
+      op == UserOperation.PurgeComment ||
+      op == UserOperation.PurgeCommunity
+    ) {
+      let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse);
+      if (data.success) {
+        toast(i18n.t("purge_success"));
+        this.context.router.history.push(`/`);
+      }
     }
   }
 }