]> Untitled Git - lemmy-ui.git/commitdiff
Feature/user community block (#362)
authorDessalines <dessalines@users.noreply.github.com>
Fri, 20 Aug 2021 02:56:18 +0000 (22:56 -0400)
committerGitHub <noreply@github.com>
Fri, 20 Aug 2021 02:56:18 +0000 (22:56 -0400)
* Extracting user settings and profile page.

- Auto-collapsing dropdown and navbar on link clicks.
- Fixes #180

* Adding User and Community blocking. Fixes #295

- Added a new settings page.
- Switched to myUserInfo.
- Removing GetFollowedCommunities endpoint

* Fixing blocks

31 files changed:
package.json
src/assets/css/main.css
src/shared/components/app/app.tsx
src/shared/components/app/navbar.tsx
src/shared/components/app/theme.tsx
src/shared/components/comment/comment-form.tsx
src/shared/components/comment/comment-node.tsx
src/shared/components/common/image-upload-form.tsx
src/shared/components/common/listing-type-select.tsx
src/shared/components/common/markdown-textarea.tsx
src/shared/components/common/symbols.tsx
src/shared/components/community/community-form.tsx
src/shared/components/community/community.tsx
src/shared/components/community/create-community.tsx
src/shared/components/community/sidebar.tsx
src/shared/components/home/home.tsx
src/shared/components/modlog.tsx
src/shared/components/person/inbox.tsx
src/shared/components/person/person.tsx [deleted file]
src/shared/components/person/profile.tsx [new file with mode: 0644]
src/shared/components/person/settings.tsx [new file with mode: 0644]
src/shared/components/post/create-post.tsx
src/shared/components/post/post-form.tsx
src/shared/components/post/post-listing.tsx
src/shared/components/post/post.tsx
src/shared/components/private_message/create-private-message.tsx
src/shared/components/private_message/private-message.tsx
src/shared/routes.ts
src/shared/services/UserService.ts
src/shared/utils.ts
yarn.lock

index fa3bd732476425cbc42b6bf748f8d56df50ffe93..0a0e85bdf0ac606e7a748d2d5005c808668e86eb 100644 (file)
@@ -69,7 +69,7 @@
     "husky": "^7.0.1",
     "import-sort-style-module": "^6.0.0",
     "iso-639-1": "^2.1.9",
-    "lemmy-js-client": "0.11.4-rc.12",
+    "lemmy-js-client": "0.11.4-rc.14",
     "lint-staged": "^11.0.1",
     "mini-css-extract-plugin": "^2.1.0",
     "node-fetch": "^2.6.1",
index 8861ebc2ca068875c1a023eec7af513f10650b9e..ef6b743d3c7f3211b6aa4b424a4920ee556b572e 100644 (file)
   }
 }
 
-.dropdown-menu {
+.dropdown-content {
+  position: absolute;
+  background-color: var(--light);
+  min-width: 160px;
+  box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
   z-index: 2000;
 }
 
index 689f7985acabd2800b57bebf1c2e25e1a493f2c4..ddf466b6d6926dbe0ead99906828b83489c98c98 100644 (file)
@@ -26,7 +26,7 @@ export class App extends Component<AppProps, any> {
       <>
         <Provider i18next={i18n}>
           <div>
-            <Theme localUserView={siteRes.my_user} />
+            <Theme myUserInfo={siteRes.my_user} />
             {siteRes &&
               siteRes.site_view &&
               this.props.siteRes.site_view.site.icon && (
index 114ffb779dc6695a29682a3857ce4f2a34da0e0e..06304a8c53e45dee8f3ee21955e3e74fa342c628 100644 (file)
@@ -50,6 +50,7 @@ interface NavbarState {
   unreadCount: number;
   searchParam: string;
   toggleSearch: boolean;
+  showDropdown: boolean;
   onSiteBanner?(url: string): any;
 }
 
@@ -67,6 +68,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
     expanded: false,
     searchParam: "",
     toggleSearch: false,
+    showDropdown: false,
   };
   subscription: any;
 
@@ -122,15 +124,17 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
     }
   }
 
-  handleSearchParam(i: Navbar, event: any) {
-    i.state.searchParam = event.target.value;
-    i.setState(i.state);
+  componentWillUnmount() {
+    this.wsSub.unsubscribe();
+    this.userSub.unsubscribe();
+    this.unreadCountSub.unsubscribe();
   }
 
   updateUrl() {
     const searchParam = this.state.searchParam;
     this.setState({ searchParam: "" });
     this.setState({ toggleSearch: false });
+    this.setState({ showDropdown: false, expanded: false });
     if (searchParam === "") {
       this.context.router.history.push(`/search/`);
     } else {
@@ -141,54 +145,26 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
     }
   }
 
-  handleSearchSubmit(i: Navbar, event: any) {
-    event.preventDefault();
-    i.updateUrl();
-  }
-
-  handleSearchBtn(i: Navbar, event: any) {
-    event.preventDefault();
-    i.setState({ toggleSearch: true });
-
-    i.searchTextField.current.focus();
-    const offsetWidth = i.searchTextField.current.offsetWidth;
-    if (i.state.searchParam && offsetWidth > 100) {
-      i.updateUrl();
-    }
-  }
-
-  handleSearchBlur(i: Navbar, event: any) {
-    if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
-      i.state.toggleSearch = false;
-      i.setState(i.state);
-    }
-  }
-
   render() {
     return this.navbar();
   }
 
-  componentWillUnmount() {
-    this.wsSub.unsubscribe();
-    this.userSub.unsubscribe();
-    this.unreadCountSub.unsubscribe();
-  }
-
   // TODO class active corresponding to current page
   navbar() {
-    let localUserView =
-      UserService.Instance.localUserView || this.props.site_res.my_user;
+    let myUserInfo =
+      UserService.Instance.myUserInfo || this.props.site_res.my_user;
+    let person = myUserInfo?.local_user_view.person;
     return (
       <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
         <div class="container">
           {this.props.site_res.site_view && (
-            <Link
+            <button
               title={
                 this.props.site_res.site_view.site.description ||
                 this.props.site_res.site_view.site.name
               }
-              className="d-flex align-items-center navbar-brand mr-md-3"
-              to="/"
+              className="d-flex align-items-center navbar-brand mr-md-3 btn btn-link"
+              onClick={linkEvent(this, this.handleGotoHome)}
             >
               {this.props.site_res.site_view.site.icon && showAvatars() && (
                 <PictrsImage
@@ -197,12 +173,12 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
                 />
               )}
               {this.props.site_res.site_view.site.name}
-            </Link>
+            </button>
           )}
           {this.state.isLoggedIn && (
-            <Link
-              className="ml-auto p-1 navbar-toggler nav-link border-0"
-              to="/inbox"
+            <button
+              className="ml-auto p-1 navbar-toggler nav-link border-0 btn btn-link"
+              onClick={linkEvent(this, this.handleGotoInbox)}
               title={i18n.t("inbox")}
             >
               <Icon icon="bell" />
@@ -216,7 +192,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
                   {this.state.unreadCount}
                 </span>
               )}
-            </Link>
+            </button>
           )}
           <button
             class="navbar-toggler border-0 p-1"
@@ -232,35 +208,32 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
           >
             <ul class="navbar-nav my-2 mr-auto">
               <li class="nav-item">
-                <Link
-                  className="nav-link"
-                  to="/communities"
+                <button
+                  className="nav-link btn btn-link"
+                  onClick={linkEvent(this, this.handleGotoCommunities)}
                   title={i18n.t("communities")}
                 >
                   {i18n.t("communities")}
-                </Link>
+                </button>
               </li>
               <li class="nav-item">
-                <Link
-                  className="nav-link"
-                  to={{
-                    pathname: "/create_post",
-                    state: { prevPath: this.currentLocation },
-                  }}
+                <button
+                  className="nav-link btn btn-link"
+                  onClick={linkEvent(this, this.handleGotoCreatePost)}
                   title={i18n.t("create_post")}
                 >
                   {i18n.t("create_post")}
-                </Link>
+                </button>
               </li>
               {this.canCreateCommunity && (
                 <li class="nav-item">
-                  <Link
-                    className="nav-link"
-                    to="/create_community"
+                  <button
+                    className="nav-link btn btn-link"
+                    onClick={linkEvent(this, this.handleGotoCreateCommunity)}
                     title={i18n.t("create_community")}
                   >
                     {i18n.t("create_community")}
-                  </Link>
+                  </button>
                 </li>
               )}
               <li class="nav-item">
@@ -276,13 +249,13 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
             <ul class="navbar-nav my-2">
               {this.canAdmin && (
                 <li className="nav-item">
-                  <Link
-                    className="nav-link"
-                    to={`/admin`}
+                  <button
+                    className="nav-link btn btn-link"
+                    onClick={linkEvent(this, this.handleGotoAdmin)}
                     title={i18n.t("admin_settings")}
                   >
                     <Icon icon="settings" />
-                  </Link>
+                  </button>
                 </li>
               )}
             </ul>
@@ -343,34 +316,73 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
                   </li>
                 </ul>
                 <ul class="navbar-nav">
-                  <li className="nav-item">
-                    <Link
-                      className="nav-link"
-                      to={`/u/${localUserView.person.name}`}
-                      title={i18n.t("settings")}
+                  <li class="nav-item dropdown">
+                    <button
+                      class="nav-link btn btn-link dropdown-toggle"
+                      onClick={linkEvent(this, this.handleShowDropdown)}
+                      id="navbarDropdown"
+                      role="button"
+                      aria-expanded="false"
                     >
                       <span>
-                        {localUserView.person.avatar && showAvatars() && (
-                          <PictrsImage src={localUserView.person.avatar} icon />
+                        {person.avatar && showAvatars() && (
+                          <PictrsImage src={person.avatar} icon />
                         )}
-                        {localUserView.person.display_name
-                          ? localUserView.person.display_name
-                          : localUserView.person.name}
+                        {person.display_name
+                          ? person.display_name
+                          : person.name}
                       </span>
-                    </Link>
+                    </button>
+                    {this.state.showDropdown && (
+                      <div class="dropdown-content">
+                        <li className="nav-item">
+                          <button
+                            className="nav-link btn btn-link"
+                            onClick={linkEvent(this, this.handleGotoProfile)}
+                            title={i18n.t("profile")}
+                          >
+                            <Icon icon="user" classes="mr-1" />
+                            {i18n.t("profile")}
+                          </button>
+                        </li>
+                        <li className="nav-item">
+                          <button
+                            className="nav-link btn btn-link"
+                            onClick={linkEvent(this, this.handleGotoSettings)}
+                            title={i18n.t("settings")}
+                          >
+                            <Icon icon="settings" classes="mr-1" />
+                            {i18n.t("settings")}
+                          </button>
+                        </li>
+                        <li>
+                          <hr class="dropdown-divider" />
+                        </li>
+                        <li className="nav-item">
+                          <button
+                            className="nav-link btn btn-link"
+                            onClick={linkEvent(this, this.handleLogoutClick)}
+                            title="test"
+                          >
+                            <Icon icon="log-out" classes="mr-1" />
+                            {i18n.t("logout")}
+                          </button>
+                        </li>
+                      </div>
+                    )}
                   </li>
                 </ul>
               </>
             ) : (
               <ul class="navbar-nav my-2">
                 <li className="ml-2 nav-item">
-                  <Link
+                  <button
                     className="btn btn-success"
-                    to="/login"
+                    onClick={linkEvent(this, this.handleGotoLogin)}
                     title={i18n.t("login_sign_up")}
                   >
                     {i18n.t("login_sign_up")}
-                  </Link>
+                  </button>
                 </li>
               </ul>
             )}
@@ -385,6 +397,95 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
     i.setState(i.state);
   }
 
+  handleSearchParam(i: Navbar, event: any) {
+    i.state.searchParam = event.target.value;
+    i.setState(i.state);
+  }
+
+  handleSearchSubmit(i: Navbar, event: any) {
+    event.preventDefault();
+    i.updateUrl();
+  }
+
+  handleSearchBtn(i: Navbar, event: any) {
+    event.preventDefault();
+    i.setState({ toggleSearch: true });
+
+    i.searchTextField.current.focus();
+    const offsetWidth = i.searchTextField.current.offsetWidth;
+    if (i.state.searchParam && offsetWidth > 100) {
+      i.updateUrl();
+    }
+  }
+
+  handleSearchBlur(i: Navbar, event: any) {
+    if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
+      i.state.toggleSearch = false;
+      i.setState(i.state);
+    }
+  }
+
+  handleLogoutClick(i: Navbar) {
+    i.setState({ showDropdown: false, expanded: false });
+    UserService.Instance.logout();
+    i.context.router.history.push("/");
+    location.reload();
+  }
+
+  handleGotoSettings(i: Navbar) {
+    i.setState({ showDropdown: false, expanded: false });
+    i.context.router.history.push("/settings");
+  }
+
+  handleGotoProfile(i: Navbar) {
+    i.setState({ showDropdown: false, expanded: false });
+    i.context.router.history.push(
+      `/u/${UserService.Instance.myUserInfo.local_user_view.person.name}`
+    );
+  }
+
+  handleGotoCreatePost(i: Navbar) {
+    i.setState({ showDropdown: false, expanded: false });
+    i.context.router.history.push("/create_post", {
+      prevPath: i.currentLocation,
+    });
+  }
+
+  handleGotoCreateCommunity(i: Navbar) {
+    i.setState({ showDropdown: false, expanded: false });
+    i.context.router.history.push(`/create_community`);
+  }
+
+  handleGotoCommunities(i: Navbar) {
+    i.setState({ showDropdown: false, expanded: false });
+    i.context.router.history.push(`/communities`);
+  }
+
+  handleGotoHome(i: Navbar) {
+    i.setState({ showDropdown: false, expanded: false });
+    i.context.router.history.push(`/`);
+  }
+
+  handleGotoInbox(i: Navbar) {
+    i.setState({ showDropdown: false, expanded: false });
+    i.context.router.history.push(`/inbox`);
+  }
+
+  handleGotoAdmin(i: Navbar) {
+    i.setState({ showDropdown: false, expanded: false });
+    i.context.router.history.push(`/admin`);
+  }
+
+  handleGotoLogin(i: Navbar) {
+    i.setState({ showDropdown: false, expanded: false });
+    i.context.router.history.push(`/login`);
+  }
+
+  handleShowDropdown(i: Navbar) {
+    i.state.showDropdown = !i.state.showDropdown;
+    i.setState(i.state);
+  }
+
   parseMessage(msg: any) {
     let op = wsUserOp(msg);
     console.log(msg);
@@ -432,8 +533,10 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
       // This is only called on a successful login
       let data = wsJsonToRes<GetSiteResponse>(msg).data;
       console.log(data.my_user);
-      UserService.Instance.localUserView = data.my_user;
-      setTheme(UserService.Instance.localUserView.local_user.theme);
+      UserService.Instance.myUserInfo = data.my_user;
+      setTheme(
+        UserService.Instance.myUserInfo.local_user_view.local_user.theme
+      );
       i18n.changeLanguage(getLanguage());
       this.state.isLoggedIn = true;
       this.setState(this.state);
@@ -443,7 +546,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
       if (this.state.isLoggedIn) {
         if (
           data.recipient_ids.includes(
-            UserService.Instance.localUserView.local_user.id
+            UserService.Instance.myUserInfo.local_user_view.local_user.id
           )
         ) {
           this.state.replies.push(data.comment_view);
@@ -459,7 +562,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
       if (this.state.isLoggedIn) {
         if (
           data.private_message_view.recipient.id ==
-          UserService.Instance.localUserView.person.id
+          UserService.Instance.myUserInfo.local_user_view.person.id
         ) {
           this.state.messages.push(data.private_message_view);
           this.state.unreadCount++;
@@ -525,10 +628,10 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
 
   get canAdmin(): boolean {
     return (
-      UserService.Instance.localUserView &&
+      UserService.Instance.myUserInfo &&
       this.props.site_res.admins
         .map(a => a.person.id)
-        .includes(UserService.Instance.localUserView.person.id)
+        .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
     );
   }
 
@@ -547,7 +650,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
   }
 
   requestNotificationPermission() {
-    if (UserService.Instance.localUserView) {
+    if (UserService.Instance.myUserInfo) {
       document.addEventListener("DOMContentLoaded", function () {
         if (!Notification) {
           toast(i18n.t("notifications_error"), "danger");
index 624de61bef952b7f1a21e83f8572a89175ab5768..bf5289b127dd9e03b11a2560e7a95b5d92a8d90d 100644 (file)
@@ -1,15 +1,15 @@
 import { Component } from "inferno";
 import { Helmet } from "inferno-helmet";
-import { LocalUserSettingsView } from "lemmy-js-client";
+import { MyUserInfo } from "lemmy-js-client";
 
 interface Props {
-  localUserView: LocalUserSettingsView | undefined;
+  myUserInfo: MyUserInfo | undefined;
 }
 
 export class Theme extends Component<Props> {
   render() {
-    let user = this.props.localUserView;
-    let hasTheme = user && user.local_user.theme !== "browser";
+    let user = this.props.myUserInfo;
+    let hasTheme = user && user.local_user_view.local_user.theme !== "browser";
 
     return (
       <Helmet>
@@ -17,7 +17,7 @@ export class Theme extends Component<Props> {
           <link
             rel="stylesheet"
             type="text/css"
-            href={`/static/assets/css/themes/${user.local_user.theme}.min.css`}
+            href={`/static/assets/css/themes/${user.local_user_view.local_user.theme}.min.css`}
           />
         ) : (
           [
index b7687c802eb221ba449960682d0d1292a8f04c3d..95dff716080238e57665658d385fe8409f479eb4 100644 (file)
@@ -68,7 +68,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
   render() {
     return (
       <div class="mb-3">
-        {UserService.Instance.localUserView ? (
+        {UserService.Instance.myUserInfo ? (
           <MarkdownTextArea
             initialContent={
               this.props.edit
@@ -135,7 +135,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
     console.log(msg);
 
     // Only do the showing and hiding if logged in
-    if (UserService.Instance.localUserView) {
+    if (UserService.Instance.myUserInfo) {
       if (
         op == UserOperation.CreateComment ||
         op == UserOperation.EditComment
index e276b512fc4f912bd8afe80577164f6869ec855f..1007866708aaedab5299ce6a236421390ef7e886 100644 (file)
@@ -5,6 +5,7 @@ import {
   AddModToCommunity,
   BanFromCommunity,
   BanPerson,
+  BlockPerson,
   CommentView,
   CommunityModeratorView,
   CreateCommentLike,
@@ -279,7 +280,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                       )}
                     </button>
                   )}
-                  {UserService.Instance.localUserView && !this.props.viewOnly && (
+                  {UserService.Instance.myUserInfo && !this.props.viewOnly && (
                     <>
                       <button
                         className={`btn btn-link btn-animate ${
@@ -333,15 +334,28 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                       ) : (
                         <>
                           {!this.myComment && (
-                            <button class="btn btn-link btn-animate">
-                              <Link
-                                className="text-muted"
-                                to={`/create_private_message/recipient/${cv.creator.id}`}
-                                title={i18n.t("message").toLowerCase()}
+                            <>
+                              <button class="btn btn-link btn-animate">
+                                <Link
+                                  className="text-muted"
+                                  to={`/create_private_message/recipient/${cv.creator.id}`}
+                                  title={i18n.t("message").toLowerCase()}
+                                >
+                                  <Icon icon="mail" />
+                                </Link>
+                              </button>
+                              <button
+                                class="btn btn-link btn-animate text-muted"
+                                onClick={linkEvent(
+                                  this,
+                                  this.handleBlockUserClick
+                                )}
+                                data-tippy-content={i18n.t("block_user")}
+                                aria-label={i18n.t("block_user")}
                               >
-                                <Icon icon="mail" />
-                              </Link>
-                            </button>
+                                <Icon icon="slash" />
+                              </button>
+                            </>
                           )}
                           <button
                             class="btn btn-link btn-animate text-muted"
@@ -829,7 +843,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   get myComment(): boolean {
     return (
       this.props.node.comment_view.creator.id ==
-      UserService.Instance.localUserView?.person.id
+      UserService.Instance.myUserInfo?.local_user_view.person.id
     );
   }
 
@@ -864,7 +878,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
         .concat(this.props.moderators.map(m => m.moderator.id));
 
       return canMod(
-        UserService.Instance.localUserView,
+        UserService.Instance.myUserInfo,
         adminsThenMods,
         this.props.node.comment_view.creator.id
       );
@@ -877,7 +891,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     return (
       this.props.admins &&
       canMod(
-        UserService.Instance.localUserView,
+        UserService.Instance.myUserInfo,
         this.props.admins.map(a => a.person.id),
         this.props.node.comment_view.creator.id
       )
@@ -887,10 +901,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   get amCommunityCreator(): boolean {
     return (
       this.props.moderators &&
-      UserService.Instance.localUserView &&
+      UserService.Instance.myUserInfo &&
       this.props.node.comment_view.creator.id !=
-        UserService.Instance.localUserView.person.id &&
-      UserService.Instance.localUserView.person.id ==
+        UserService.Instance.myUserInfo.local_user_view.person.id &&
+      UserService.Instance.myUserInfo.local_user_view.person.id ==
         this.props.moderators[0].moderator.id
     );
   }
@@ -898,10 +912,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   get amSiteCreator(): boolean {
     return (
       this.props.admins &&
-      UserService.Instance.localUserView &&
+      UserService.Instance.myUserInfo &&
       this.props.node.comment_view.creator.id !=
-        UserService.Instance.localUserView.person.id &&
-      UserService.Instance.localUserView.person.id ==
+        UserService.Instance.myUserInfo.local_user_view.person.id &&
+      UserService.Instance.myUserInfo.local_user_view.person.id ==
         this.props.admins[0].person.id
     );
   }
@@ -925,6 +939,15 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     i.setState(i.state);
   }
 
+  handleBlockUserClick(i: CommentNode) {
+    let blockUserForm: BlockPerson = {
+      person_id: i.props.node.comment_view.creator.id,
+      block: true,
+      auth: authField(),
+    };
+    WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
+  }
+
   handleDeleteClick(i: CommentNode) {
     let comment = i.props.node.comment_view.comment;
     let deleteForm: DeleteComment = {
index 60823c02fdc1202a31176e75127f59bbe19865d9..a090bc8948a29dea1e9cf12dc2869fede501c13d 100644 (file)
@@ -65,7 +65,7 @@ export class ImageUploadForm extends Component<
           accept="image/*,video/*"
           name={this.id}
           class="d-none"
-          disabled={!UserService.Instance.localUserView}
+          disabled={!UserService.Instance.myUserInfo}
           onChange={linkEvent(this, this.handleImageUpload)}
         />
       </form>
index 1493f64998685c922a28ea6b6431bd767917c9cd..4d00c4628a17d6e132201d7961e06348321c157f 100644 (file)
@@ -44,7 +44,7 @@ export class ListingTypeSelect extends Component<
           className={`btn btn-outline-secondary 
             ${this.state.type_ == ListingType.Subscribed && "active"}
             ${
-              UserService.Instance.localUserView == undefined
+              UserService.Instance.myUserInfo == undefined
                 ? "disabled"
                 : "pointer"
             }
@@ -56,7 +56,7 @@ export class ListingTypeSelect extends Component<
             value={ListingType.Subscribed}
             checked={this.state.type_ == ListingType.Subscribed}
             onChange={linkEvent(this, this.handleTypeChange)}
-            disabled={UserService.Instance.localUserView == undefined}
+            disabled={UserService.Instance.myUserInfo == undefined}
           />
           {i18n.t("subscribed")}
         </label>
index d8f033e520941cab4936900827a5f7e53f639bcf..d5ed4a92892f0b149b41223c2ef511ca5cbc5c91 100644 (file)
@@ -209,7 +209,7 @@ export class MarkdownTextArea extends Component<
               <label
                 htmlFor={`file-upload-${this.id}`}
                 className={`mb-0 ${
-                  UserService.Instance.localUserView && "pointer"
+                  UserService.Instance.myUserInfo && "pointer"
                 }`}
                 data-tippy-content={i18n.t("upload_image")}
               >
@@ -225,7 +225,7 @@ export class MarkdownTextArea extends Component<
                 accept="image/*,video/*"
                 name="file"
                 class="d-none"
-                disabled={!UserService.Instance.localUserView}
+                disabled={!UserService.Instance.myUserInfo}
                 onChange={linkEvent(this, this.handleImageUpload)}
               />
             </form>
index 30941d31e5a59ee16c4737e49dece52b70de718a..f94bc551ee3164cd023361a41e751fbc9babbeaf 100644 (file)
@@ -12,6 +12,15 @@ export const SYMBOLS = (
     xmlnsXlink="http://www.w3.org/1999/xlink"
   >
     <defs>
+      <symbol id="icon-log-out" viewBox="0 0 24 24">
+        <path d="M9 20h-4c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h4c0.552 0 1-0.448 1-1s-0.448-1-1-1h-4c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h4c0.552 0 1-0.448 1-1s-0.448-1-1-1zM18.586 11h-9.586c-0.552 0-1 0.448-1 1s0.448 1 1 1h9.586l-3.293 3.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l5-5c0.092-0.092 0.166-0.202 0.217-0.324 0.15-0.362 0.078-0.795-0.217-1.090l-5-5c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"></path>
+      </symbol>
+      <symbol id="icon-user" viewBox="0 0 24 24">
+        <path d="M21 21v-2c0-1.38-0.561-2.632-1.464-3.536s-2.156-1.464-3.536-1.464h-8c-1.38 0-2.632 0.561-3.536 1.464s-1.464 2.156-1.464 3.536v2c0 0.552 0.448 1 1 1s1-0.448 1-1v-2c0-0.829 0.335-1.577 0.879-2.121s1.292-0.879 2.121-0.879h8c0.829 0 1.577 0.335 2.121 0.879s0.879 1.292 0.879 2.121v2c0 0.552 0.448 1 1 1s1-0.448 1-1zM17 7c0-1.38-0.561-2.632-1.464-3.536s-2.156-1.464-3.536-1.464-2.632 0.561-3.536 1.464-1.464 2.156-1.464 3.536 0.561 2.632 1.464 3.536 2.156 1.464 3.536 1.464 2.632-0.561 3.536-1.464 1.464-2.156 1.464-3.536zM15 7c0 0.829-0.335 1.577-0.879 2.121s-1.292 0.879-2.121 0.879-1.577-0.335-2.121-0.879-0.879-1.292-0.879-2.121 0.335-1.577 0.879-2.121 1.292-0.879 2.121-0.879 1.577 0.335 2.121 0.879 0.879 1.292 0.879 2.121z"></path>
+      </symbol>
+      <symbol id="icon-slash" viewBox="0 0 24 24">
+        <path d="M23 12c0-3.037-1.232-5.789-3.222-7.778s-4.741-3.222-7.778-3.222-5.789 1.232-7.778 3.222-3.222 4.741-3.222 7.778 1.232 5.789 3.222 7.778 4.741 3.222 7.778 3.222 5.789-1.232 7.778-3.222 3.222-4.741 3.222-7.778zM19.032 17.618l-12.65-12.65c1.54-1.232 3.493-1.968 5.618-1.968 2.486 0 4.734 1.006 6.364 2.636s2.636 3.878 2.636 6.364c0 2.125-0.736 4.078-1.968 5.618zM4.968 6.382l12.65 12.65c-1.54 1.232-3.493 1.968-5.618 1.968-2.486 0-4.734-1.006-6.364-2.636s-2.636-3.878-2.636-6.364c0-2.125 0.736-4.078 1.968-5.618z"></path>
+      </symbol>
       <symbol id="icon-menu" viewBox="0 0 24 24">
         <path d="M3 13h18c0.552 0 1-0.448 1-1s-0.448-1-1-1h-18c-0.552 0-1 0.448-1 1s0.448 1 1 1zM3 7h18c0.552 0 1-0.448 1-1s-0.448-1-1-1h-18c-0.552 0-1 0.448-1 1s0.448 1 1 1zM3 19h18c0.552 0 1-0.448 1-1s-0.448-1-1-1h-18c-0.552 0-1 0.448-1 1s0.448 1 1 1z"></path>
       </symbol>
index f76d548561d7473aa2dcd0598301a61c915f898d..45cd5ec4f81a73a3206b3fc3dc4bc9827581e512 100644 (file)
@@ -9,7 +9,7 @@ import {
 } from "lemmy-js-client";
 import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
-import { WebSocketService } from "../../services";
+import { UserService, WebSocketService } from "../../services";
 import {
   authField,
   capitalizeFirstLetter,
@@ -321,10 +321,39 @@ export class CommunityForm extends Component<
       let data = wsJsonToRes<CommunityResponse>(msg).data;
       this.state.loading = false;
       this.props.onCreate(data.community_view);
+
+      // Update myUserInfo
+      let community = data.community_view.community;
+      let person = UserService.Instance.myUserInfo.local_user_view.person;
+      UserService.Instance.myUserInfo.follows.push({
+        community,
+        follower: person,
+      });
+      UserService.Instance.myUserInfo.moderates.push({
+        community,
+        moderator: person,
+      });
     } else if (op == UserOperation.EditCommunity) {
       let data = wsJsonToRes<CommunityResponse>(msg).data;
       this.state.loading = false;
       this.props.onEdit(data.community_view);
+      let community = data.community_view.community;
+
+      let followFound = UserService.Instance.myUserInfo.follows.findIndex(
+        f => f.community.id == community.id
+      );
+      if (followFound) {
+        UserService.Instance.myUserInfo.follows[followFound].community =
+          community;
+      }
+
+      let moderatesFound = UserService.Instance.myUserInfo.moderates.findIndex(
+        f => f.community.id == community.id
+      );
+      if (moderatesFound) {
+        UserService.Instance.myUserInfo.moderates[moderatesFound].community =
+          community;
+      }
     }
   }
 }
index 64db8e1fda6c667a4f9d932517af8a8f78345cf0..3ce5f848ee3855983ece3079c95a1d3f5b1289f8 100644 (file)
@@ -2,6 +2,7 @@ import { Component, linkEvent } from "inferno";
 import {
   AddModToCommunityResponse,
   BanFromCommunityResponse,
+  BlockPersonResponse,
   CommentResponse,
   CommentView,
   CommunityResponse,
@@ -42,6 +43,7 @@ import {
   setOptionalAuth,
   setupTippy,
   toast,
+  updatePersonBlock,
   wsClient,
   wsJsonToRes,
   wsSubscribe,
@@ -178,9 +180,10 @@ export class Community extends Component<any, State> {
 
     let sort: SortType = pathSplit[6]
       ? SortType[pathSplit[6]]
-      : UserService.Instance.localUserView
+      : UserService.Instance.myUserInfo
       ? Object.values(SortType)[
-          UserService.Instance.localUserView.local_user.default_sort_type
+          UserService.Instance.myUserInfo.local_user_view.local_user
+            .default_sort_type
         ]
       : SortType.Active;
 
@@ -490,7 +493,10 @@ export class Community extends Component<any, State> {
     } else if (op == UserOperation.CreatePost) {
       let data = wsJsonToRes<PostResponse>(msg).data;
       this.state.posts.unshift(data.post_view);
-      if (UserService.Instance.localUserView?.local_user.show_new_post_notifs) {
+      if (
+        UserService.Instance.myUserInfo?.local_user_view.local_user
+          .show_new_post_notifs
+      ) {
         notifyPost(data.post_view, this.context.router);
       }
       this.setState(this.state);
@@ -540,6 +546,9 @@ export class Community extends Component<any, State> {
       let data = wsJsonToRes<CommentResponse>(msg).data;
       createCommentLikeRes(data.comment_view, this.state.comments);
       this.setState(this.state);
+    } else if (op == UserOperation.BlockPerson) {
+      let data = wsJsonToRes<BlockPersonResponse>(msg).data;
+      updatePersonBlock(data);
     }
   }
 }
index b96ddba91cb60eb8a3616a88955149bc537cdf4a..c43e727c7cbc7dd0b3b91100a6a4bb02760de5f6 100644 (file)
@@ -28,7 +28,7 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
     this.parseMessage = this.parseMessage.bind(this);
     this.subscription = wsSubscribe(this.parseMessage);
 
-    if (!UserService.Instance.localUserView && isBrowser()) {
+    if (!UserService.Instance.myUserInfo && isBrowser()) {
       toast(i18n.t("not_logged_in"), "danger");
       this.context.router.history.push(`/login`);
     }
index a699ab982998cb8cfd0cfebbe47431660671229a..c0cb991901aaddf26bd36a4fbadc1647b31c7b6d 100644 (file)
@@ -104,7 +104,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
             <a
               class="btn btn-secondary btn-sm mr-2"
               href="#"
-              onClick={linkEvent(community.id, this.handleUnsubscribe)}
+              onClick={linkEvent(this, this.handleUnsubscribe)}
             >
               <Icon icon="check" classes="icon-inline text-success mr-1" />
               {i18n.t("joined")}
@@ -257,10 +257,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
           <a
             class="btn btn-secondary btn-block"
             href="#"
-            onClick={linkEvent(
-              community_view.community.id,
-              this.handleSubscribe
-            )}
+            onClick={linkEvent(this, this.handleSubscribe)}
           >
             {i18n.t("subscribe")}
           </a>
@@ -447,7 +444,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
 
   handleLeaveModTeamClick(i: Sidebar) {
     let form: AddModToCommunity = {
-      person_id: UserService.Instance.localUserView.person.id,
+      person_id: UserService.Instance.myUserInfo.local_user_view.person.id,
       community_id: i.props.community_view.community.id,
       added: false,
       auth: authField(),
@@ -462,48 +459,62 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
     i.setState(i.state);
   }
 
-  handleUnsubscribe(communityId: number, event: any) {
+  handleUnsubscribe(i: Sidebar, event: any) {
     event.preventDefault();
+    let community_id = i.props.community_view.community.id;
     let form: FollowCommunity = {
-      community_id: communityId,
+      community_id,
       follow: false,
       auth: authField(),
     };
     WebSocketService.Instance.send(wsClient.followCommunity(form));
+
+    // Update myUserInfo
+    UserService.Instance.myUserInfo.follows =
+      UserService.Instance.myUserInfo.follows.filter(
+        i => i.community.id != community_id
+      );
   }
 
-  handleSubscribe(communityId: number, event: any) {
+  handleSubscribe(i: Sidebar, event: any) {
     event.preventDefault();
+    let community_id = i.props.community_view.community.id;
     let form: FollowCommunity = {
-      community_id: communityId,
+      community_id,
       follow: true,
       auth: authField(),
     };
     WebSocketService.Instance.send(wsClient.followCommunity(form));
+
+    // Update myUserInfo
+    UserService.Instance.myUserInfo.follows.push({
+      community: i.props.community_view.community,
+      follower: UserService.Instance.myUserInfo.local_user_view.person,
+    });
   }
 
   private get amTopMod(): boolean {
     return (
       this.props.moderators[0].moderator.id ==
-      UserService.Instance.localUserView.person.id
+      UserService.Instance.myUserInfo.local_user_view.person.id
     );
   }
 
   get canMod(): boolean {
     return (
-      UserService.Instance.localUserView &&
+      UserService.Instance.myUserInfo &&
       this.props.moderators
         .map(m => m.moderator.id)
-        .includes(UserService.Instance.localUserView.person.id)
+        .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
     );
   }
 
   get canAdmin(): boolean {
     return (
-      UserService.Instance.localUserView &&
+      UserService.Instance.myUserInfo &&
       this.props.admins
         .map(a => a.person.id)
-        .includes(UserService.Instance.localUserView.person.id)
+        .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
     );
   }
 
index ab2df75e61d9ce3d48ac70176bc8162b0f4cc925..3cdb238d400191db11f92512f606df5caca38ae6 100644 (file)
@@ -4,13 +4,12 @@ import { Link } from "inferno-router";
 import {
   AddAdminResponse,
   BanPersonResponse,
+  BlockPersonResponse,
   CommentResponse,
   CommentView,
-  CommunityFollowerView,
   CommunityView,
   GetComments,
   GetCommentsResponse,
-  GetFollowedCommunitiesResponse,
   GetPosts,
   GetPostsResponse,
   GetSiteResponse,
@@ -49,6 +48,7 @@ import {
   setupTippy,
   showLocal,
   toast,
+  updatePersonBlock,
   wsClient,
   wsJsonToRes,
   wsSubscribe,
@@ -68,7 +68,6 @@ import { PostListings } from "../post/post-listings";
 import { SiteForm } from "./site-form";
 
 interface HomeState {
-  subscribedCommunities: CommunityFollowerView[];
   trendingCommunities: CommunityView[];
   siteRes: GetSiteResponse;
   showEditSite: boolean;
@@ -102,7 +101,6 @@ export class Home extends Component<any, HomeState> {
   private isoData = setIsoData(this.context);
   private subscription: Subscription;
   private emptyState: HomeState = {
-    subscribedCommunities: [],
     trendingCommunities: [],
     siteRes: this.isoData.site_res,
     showEditSite: false,
@@ -139,21 +137,10 @@ export class Home extends Component<any, HomeState> {
         this.state.comments = this.isoData.routeData[0].comments;
       }
       this.state.trendingCommunities = this.isoData.routeData[1].communities;
-      if (UserService.Instance.localUserView) {
-        this.state.subscribedCommunities =
-          this.isoData.routeData[2].communities;
-      }
       this.state.loading = false;
     } else {
       this.fetchTrendingCommunities();
       this.fetchData();
-      if (UserService.Instance.localUserView) {
-        WebSocketService.Instance.send(
-          wsClient.getFollowedCommunities({
-            auth: authField(),
-          })
-        );
-      }
     }
 
     setupTippy();
@@ -204,16 +191,18 @@ export class Home extends Component<any, HomeState> {
     // TODO figure out auth default_listingType, default_sort_type
     let type_: ListingType = pathSplit[5]
       ? ListingType[pathSplit[5]]
-      : UserService.Instance.localUserView
+      : UserService.Instance.myUserInfo
       ? Object.values(ListingType)[
-          UserService.Instance.localUserView.local_user.default_listing_type
+          UserService.Instance.myUserInfo.local_user_view.local_user
+            .default_listing_type
         ]
       : ListingType.Local;
     let sort: SortType = pathSplit[7]
       ? SortType[pathSplit[7]]
-      : UserService.Instance.localUserView
+      : UserService.Instance.myUserInfo
       ? Object.values(SortType)[
-          UserService.Instance.localUserView.local_user.default_sort_type
+          UserService.Instance.myUserInfo.local_user_view.local_user
+            .default_sort_type
         ]
       : SortType.Active;
 
@@ -250,10 +239,6 @@ export class Home extends Component<any, HomeState> {
     };
     promises.push(req.client.listCommunities(trendingCommunitiesForm));
 
-    if (req.auth) {
-      promises.push(req.client.getFollowedCommunities({ auth: req.auth }));
-    }
-
     return promises;
   }
 
@@ -303,8 +288,8 @@ export class Home extends Component<any, HomeState> {
     return (
       <div class="row">
         <div class="col-12">
-          {UserService.Instance.localUserView &&
-            this.state.subscribedCommunities.length > 0 && (
+          {UserService.Instance.myUserInfo &&
+            UserService.Instance.myUserInfo.follows.length > 0 && (
               <button
                 class="btn btn-secondary d-inline-block mb-2 mr-3"
                 onClick={linkEvent(this, this.handleShowSubscribedMobile)}
@@ -377,8 +362,8 @@ export class Home extends Component<any, HomeState> {
               </div>
             </div>
 
-            {UserService.Instance.localUserView &&
-              this.state.subscribedCommunities.length > 0 && (
+            {UserService.Instance.myUserInfo &&
+              UserService.Instance.myUserInfo.follows.length > 0 && (
                 <div class="card border-secondary mb-3">
                   <div class="card-body">{this.subscribedCommunities()}</div>
                 </div>
@@ -443,7 +428,7 @@ export class Home extends Component<any, HomeState> {
           </T>
         </h5>
         <ul class="list-inline mb-0">
-          {this.state.subscribedCommunities.map(cfv => (
+          {UserService.Instance.myUserInfo.follows.map(cfv => (
             <li class="list-inline-item d-inline-block">
               <CommunityLink community={cfv.community} />
             </li>
@@ -704,7 +689,7 @@ export class Home extends Component<any, HomeState> {
             <Icon icon="rss" classes="text-muted small" />
           </a>
         )}
-        {UserService.Instance.localUserView &&
+        {UserService.Instance.myUserInfo &&
           this.state.listingType == ListingType.Subscribed && (
             <a
               href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${this.state.sort}`}
@@ -720,10 +705,10 @@ export class Home extends Component<any, HomeState> {
 
   get canAdmin(): boolean {
     return (
-      UserService.Instance.localUserView &&
+      UserService.Instance.myUserInfo &&
       this.state.siteRes.admins
         .map(a => a.person.id)
-        .includes(UserService.Instance.localUserView.person.id)
+        .includes(UserService.Instance.myUserInfo.local_user_view.person.id)
     );
   }
 
@@ -807,10 +792,6 @@ export class Home extends Component<any, HomeState> {
         wsClient.communityJoin({ community_id: 0 })
       );
       this.fetchData();
-    } else if (op == UserOperation.GetFollowedCommunities) {
-      let data = wsJsonToRes<GetFollowedCommunitiesResponse>(msg).data;
-      this.state.subscribedCommunities = data.communities;
-      this.setState(this.state);
     } else if (op == UserOperation.ListCommunities) {
       let data = wsJsonToRes<ListCommunitiesResponse>(msg).data;
       this.state.trendingCommunities = data.communities;
@@ -836,21 +817,21 @@ export class Home extends Component<any, HomeState> {
       let nsfwCheck =
         !nsfw ||
         (nsfw &&
-          UserService.Instance.localUserView &&
-          UserService.Instance.localUserView.local_user.show_nsfw);
+          UserService.Instance.myUserInfo &&
+          UserService.Instance.myUserInfo.local_user_view.local_user.show_nsfw);
 
       // Only push these if you're on the first page, and you pass the nsfw check
       if (this.state.page == 1 && nsfwCheck) {
         // If you're on subscribed, only push it if you're subscribed.
         if (this.state.listingType == ListingType.Subscribed) {
           if (
-            this.state.subscribedCommunities
+            UserService.Instance.myUserInfo.follows
               .map(c => c.community.id)
               .includes(data.post_view.community.id)
           ) {
             this.state.posts.unshift(data.post_view);
             if (
-              UserService.Instance.localUserView?.local_user
+              UserService.Instance.myUserInfo?.local_user_view.local_user
                 .show_new_post_notifs
             ) {
               notifyPost(data.post_view, this.context.router);
@@ -861,7 +842,7 @@ export class Home extends Component<any, HomeState> {
           if (data.post_view.post.local) {
             this.state.posts.unshift(data.post_view);
             if (
-              UserService.Instance.localUserView?.local_user
+              UserService.Instance.myUserInfo?.local_user_view.local_user
                 .show_new_post_notifs
             ) {
               notifyPost(data.post_view, this.context.router);
@@ -870,7 +851,8 @@ export class Home extends Component<any, HomeState> {
         } else {
           this.state.posts.unshift(data.post_view);
           if (
-            UserService.Instance.localUserView?.local_user.show_new_post_notifs
+            UserService.Instance.myUserInfo?.local_user_view.local_user
+              .show_new_post_notifs
           ) {
             notifyPost(data.post_view, this.context.router);
           }
@@ -937,7 +919,7 @@ export class Home extends Component<any, HomeState> {
         // If you're on subscribed, only push it if you're subscribed.
         if (this.state.listingType == ListingType.Subscribed) {
           if (
-            this.state.subscribedCommunities
+            UserService.Instance.myUserInfo.follows
               .map(c => c.community.id)
               .includes(data.comment_view.community.id)
           ) {
@@ -956,6 +938,9 @@ export class Home extends Component<any, HomeState> {
       let data = wsJsonToRes<CommentResponse>(msg).data;
       createCommentLikeRes(data.comment_view, this.state.comments);
       this.setState(this.state);
+    } else if (op == UserOperation.BlockPerson) {
+      let data = wsJsonToRes<BlockPersonResponse>(msg).data;
+      updatePersonBlock(data);
     }
   }
 }
index fc875f81277632b5aef36a4b7e41e295cbccd96b..2e8527d1fceee3cdc5efbaa2654981417a1289bf 100644 (file)
@@ -409,16 +409,16 @@ export class Modlog extends Component<any, ModlogState> {
 
   get isAdminOrMod(): boolean {
     let isAdmin =
-      UserService.Instance.localUserView &&
+      UserService.Instance.myUserInfo &&
       this.isoData.site_res.admins
         .map(a => a.person.id)
-        .includes(UserService.Instance.localUserView.person.id);
+        .includes(UserService.Instance.myUserInfo.local_user_view.person.id);
     let isMod =
-      UserService.Instance.localUserView &&
+      UserService.Instance.myUserInfo &&
       this.state.communityMods &&
       this.state.communityMods
         .map(m => m.moderator.id)
-        .includes(UserService.Instance.localUserView.person.id);
+        .includes(UserService.Instance.myUserInfo.local_user_view.person.id);
     return isAdmin || isMod;
   }
 
index 10375874a4c6531359b665d44f7874b853d810f7..c18dafaa4b551237f7c21494ff6c40238117e3d2 100644 (file)
@@ -1,5 +1,6 @@
 import { Component, linkEvent } from "inferno";
 import {
+  BlockPersonResponse,
   CommentResponse,
   CommentView,
   GetPersonMentions,
@@ -31,6 +32,7 @@ import {
   setIsoData,
   setupTippy,
   toast,
+  updatePersonBlock,
   wsClient,
   wsJsonToRes,
   wsSubscribe,
@@ -103,7 +105,7 @@ export class Inbox extends Component<any, InboxState> {
     this.handleSortChange = this.handleSortChange.bind(this);
     this.handlePageChange = this.handlePageChange.bind(this);
 
-    if (!UserService.Instance.localUserView && isBrowser()) {
+    if (!UserService.Instance.myUserInfo && isBrowser()) {
       toast(i18n.t("not_logged_in"), "danger");
       this.context.router.history.push(`/login`);
     }
@@ -130,9 +132,9 @@ export class Inbox extends Component<any, InboxState> {
   }
 
   get documentTitle(): string {
-    return `@${UserService.Instance.localUserView.person.name} ${i18n.t(
-      "inbox"
-    )} - ${this.state.site_view.site.name}`;
+    return `@${
+      UserService.Instance.myUserInfo.local_user_view.person.name
+    } ${i18n.t("inbox")} - ${this.state.site_view.site.name}`;
   }
 
   render() {
@@ -722,7 +724,7 @@ export class Inbox extends Component<any, InboxState> {
 
       if (
         data.recipient_ids.includes(
-          UserService.Instance.localUserView.local_user.id
+          UserService.Instance.myUserInfo.local_user_view.local_user.id
         )
       ) {
         this.state.replies.unshift(data.comment_view);
@@ -730,7 +732,7 @@ export class Inbox extends Component<any, InboxState> {
         this.setState(this.state);
       } else if (
         data.comment_view.creator.id ==
-        UserService.Instance.localUserView.person.id
+        UserService.Instance.myUserInfo.local_user_view.person.id
       ) {
         // TODO this seems wrong, you should be using form_id
         toast(i18n.t("reply_sent"));
@@ -739,7 +741,7 @@ export class Inbox extends Component<any, InboxState> {
       let data = wsJsonToRes<PrivateMessageResponse>(msg).data;
       if (
         data.private_message_view.recipient.id ==
-        UserService.Instance.localUserView.person.id
+        UserService.Instance.myUserInfo.local_user_view.person.id
       ) {
         this.state.messages.unshift(data.private_message_view);
         this.state.combined.unshift(
@@ -756,6 +758,9 @@ export class Inbox extends Component<any, InboxState> {
       let data = wsJsonToRes<CommentResponse>(msg).data;
       createCommentLikeRes(data.comment_view, this.state.replies);
       this.setState(this.state);
+    } else if (op == UserOperation.BlockPerson) {
+      let data = wsJsonToRes<BlockPersonResponse>(msg).data;
+      updatePersonBlock(data);
     }
   }
 
@@ -769,10 +774,11 @@ export class Inbox extends Component<any, InboxState> {
       this.state.mentions.filter(r => !r.person_mention.read).length +
       this.state.messages.filter(
         r =>
-          UserService.Instance.localUserView &&
+          UserService.Instance.myUserInfo &&
           !r.private_message.read &&
           // TODO also seems very strange and wrong
-          r.creator.id !== UserService.Instance.localUserView.person.id
+          r.creator.id !==
+            UserService.Instance.myUserInfo.local_user_view.person.id
       ).length
     );
   }
diff --git a/src/shared/components/person/person.tsx b/src/shared/components/person/person.tsx
deleted file mode 100644 (file)
index 2259367..0000000
+++ /dev/null
@@ -1,1351 +0,0 @@
-import { Component, linkEvent } from "inferno";
-import { Link } from "inferno-router";
-import ISO6391 from "iso-639-1";
-import {
-  AddAdminResponse,
-  BanPersonResponse,
-  ChangePassword,
-  CommentResponse,
-  DeleteAccount,
-  GetPersonDetails,
-  GetPersonDetailsResponse,
-  GetSiteResponse,
-  ListingType,
-  LoginResponse,
-  PostResponse,
-  SaveUserSettings,
-  SortType,
-  UserOperation,
-} from "lemmy-js-client";
-import moment from "moment";
-import { Subscription } from "rxjs";
-import { i18n } from "../../i18next";
-import { InitialFetchRequest, PersonDetailsView } from "../../interfaces";
-import { UserService, WebSocketService } from "../../services";
-import {
-  authField,
-  capitalizeFirstLetter,
-  createCommentLikeRes,
-  createPostLikeFindRes,
-  editCommentRes,
-  editPostFindRes,
-  elementUrl,
-  fetchLimit,
-  getLanguage,
-  getUsernameFromProps,
-  languages,
-  mdToHtml,
-  previewLines,
-  restoreScrollPosition,
-  routeSortTypeToEnum,
-  saveCommentRes,
-  saveScrollPosition,
-  setIsoData,
-  setOptionalAuth,
-  setTheme,
-  setupTippy,
-  showLocal,
-  themes,
-  toast,
-  wsClient,
-  wsJsonToRes,
-  wsSubscribe,
-  wsUserOp,
-} from "../../utils";
-import { BannerIconHeader } from "../common/banner-icon-header";
-import { HtmlTags } from "../common/html-tags";
-import { Icon, Spinner } from "../common/icon";
-import { ImageUploadForm } from "../common/image-upload-form";
-import { ListingTypeSelect } from "../common/listing-type-select";
-import { MarkdownTextArea } from "../common/markdown-textarea";
-import { MomentTime } from "../common/moment-time";
-import { SortSelect } from "../common/sort-select";
-import { CommunityLink } from "../community/community-link";
-import { PersonDetails } from "./person-details";
-import { PersonListing } from "./person-listing";
-
-interface PersonState {
-  personRes: GetPersonDetailsResponse;
-  userName: string;
-  view: PersonDetailsView;
-  sort: SortType;
-  page: number;
-  loading: boolean;
-  saveUserSettingsForm: SaveUserSettings;
-  changePasswordForm: ChangePassword;
-  saveUserSettingsLoading: boolean;
-  changePasswordLoading: boolean;
-  deleteAccountLoading: boolean;
-  deleteAccountShowConfirm: boolean;
-  deleteAccountForm: DeleteAccount;
-  siteRes: GetSiteResponse;
-}
-
-interface PersonProps {
-  view: PersonDetailsView;
-  sort: SortType;
-  page: number;
-  person_id: number | null;
-  username: string;
-}
-
-interface UrlParams {
-  view?: string;
-  sort?: SortType;
-  page?: number;
-}
-
-export class Person extends Component<any, PersonState> {
-  private isoData = setIsoData(this.context);
-  private subscription: Subscription;
-  private emptyState: PersonState = {
-    personRes: undefined,
-    userName: getUsernameFromProps(this.props),
-    loading: true,
-    view: Person.getViewFromProps(this.props.match.view),
-    sort: Person.getSortTypeFromProps(this.props.match.sort),
-    page: Person.getPageFromProps(this.props.match.page),
-    saveUserSettingsForm: {
-      auth: authField(false),
-    },
-    changePasswordForm: {
-      new_password: null,
-      new_password_verify: null,
-      old_password: null,
-      auth: authField(false),
-    },
-    saveUserSettingsLoading: null,
-    changePasswordLoading: false,
-    deleteAccountLoading: null,
-    deleteAccountShowConfirm: false,
-    deleteAccountForm: {
-      password: null,
-      auth: authField(false),
-    },
-    siteRes: this.isoData.site_res,
-  };
-
-  constructor(props: any, context: any) {
-    super(props, context);
-
-    this.state = this.emptyState;
-    this.handleSortChange = this.handleSortChange.bind(this);
-    this.handleUserSettingsSortTypeChange =
-      this.handleUserSettingsSortTypeChange.bind(this);
-    this.handleUserSettingsListingTypeChange =
-      this.handleUserSettingsListingTypeChange.bind(this);
-    this.handlePageChange = this.handlePageChange.bind(this);
-    this.handleUserSettingsBioChange =
-      this.handleUserSettingsBioChange.bind(this);
-
-    this.handleAvatarUpload = this.handleAvatarUpload.bind(this);
-    this.handleAvatarRemove = this.handleAvatarRemove.bind(this);
-
-    this.handleBannerUpload = this.handleBannerUpload.bind(this);
-    this.handleBannerRemove = this.handleBannerRemove.bind(this);
-
-    this.parseMessage = this.parseMessage.bind(this);
-    this.subscription = wsSubscribe(this.parseMessage);
-
-    // 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.setUserInfo();
-      this.state.loading = false;
-    } else {
-      this.fetchUserData();
-    }
-
-    setupTippy();
-  }
-
-  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),
-    };
-    WebSocketService.Instance.send(wsClient.getPersonDetails(form));
-  }
-
-  get isCurrentUser() {
-    return (
-      UserService.Instance.localUserView?.person.id ==
-      this.state.personRes.person_view.person.id
-    );
-  }
-
-  static getViewFromProps(view: string): PersonDetailsView {
-    return view ? PersonDetailsView[view] : PersonDetailsView.Overview;
-  }
-
-  static getSortTypeFromProps(sort: string): SortType {
-    return sort ? routeSortTypeToEnum(sort) : SortType.New;
-  }
-
-  static getPageFromProps(page: number): number {
-    return page ? Number(page) : 1;
-  }
-
-  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 view = this.getViewFromProps(pathSplit[4]);
-    let sort = this.getSortTypeFromProps(pathSplit[6]);
-    let page = this.getPageFromProps(Number(pathSplit[8]));
-
-    let form: GetPersonDetails = {
-      sort,
-      saved_only: 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_;
-    }
-  }
-
-  componentWillUnmount() {
-    this.subscription.unsubscribe();
-    saveScrollPosition(this.context);
-  }
-
-  static getDerivedStateFromProps(props: any): PersonProps {
-    return {
-      view: this.getViewFromProps(props.match.params.view),
-      sort: this.getSortTypeFromProps(props.match.params.sort),
-      page: this.getPageFromProps(props.match.params.page),
-      person_id: Number(props.match.params.id) || null,
-      username: props.match.params.username,
-    };
-  }
-
-  componentDidUpdate(lastProps: any) {
-    // Necessary if you are on a post and you click another post (same route)
-    if (
-      lastProps.location.pathname.split("/")[2] !==
-      lastProps.history.location.pathname.split("/")[2]
-    ) {
-      // Couldnt get a refresh working. This does for now.
-      location.reload();
-    }
-  }
-
-  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;
-  }
-
-  render() {
-    return (
-      <div class="container">
-        {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.isCurrentUser && this.userSettings()}
-                {this.moderates()}
-                {this.follows()}
-              </div>
-            )}
-          </div>
-        )}
-      </div>
-    );
-  }
-
-  viewRadios() {
-    return (
-      <div class="btn-group btn-group-toggle flex-wrap mb-2">
-        <label
-          className={`btn btn-outline-secondary pointer
-            ${this.state.view == PersonDetailsView.Overview && "active"}
-          `}
-        >
-          <input
-            type="radio"
-            value={PersonDetailsView.Overview}
-            checked={this.state.view === PersonDetailsView.Overview}
-            onChange={linkEvent(this, this.handleViewChange)}
-          />
-          {i18n.t("overview")}
-        </label>
-        <label
-          className={`btn btn-outline-secondary pointer
-            ${this.state.view == PersonDetailsView.Comments && "active"}
-          `}
-        >
-          <input
-            type="radio"
-            value={PersonDetailsView.Comments}
-            checked={this.state.view == PersonDetailsView.Comments}
-            onChange={linkEvent(this, this.handleViewChange)}
-          />
-          {i18n.t("comments")}
-        </label>
-        <label
-          className={`btn btn-outline-secondary pointer
-            ${this.state.view == PersonDetailsView.Posts && "active"}
-          `}
-        >
-          <input
-            type="radio"
-            value={PersonDetailsView.Posts}
-            checked={this.state.view == PersonDetailsView.Posts}
-            onChange={linkEvent(this, this.handleViewChange)}
-          />
-          {i18n.t("posts")}
-        </label>
-        <label
-          className={`btn btn-outline-secondary pointer
-            ${this.state.view == PersonDetailsView.Saved && "active"}
-          `}
-        >
-          <input
-            type="radio"
-            value={PersonDetailsView.Saved}
-            checked={this.state.view == PersonDetailsView.Saved}
-            onChange={linkEvent(this, this.handleViewChange)}
-          />
-          {i18n.t("saved")}
-        </label>
-      </div>
-    );
-  }
-
-  selects() {
-    return (
-      <div className="mb-2">
-        <span class="mr-3">{this.viewRadios()}</span>
-        <SortSelect
-          sort={this.state.sort}
-          onChange={this.handleSortChange}
-          hideHot
-          hideMostComments
-        />
-        <a
-          href={`/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`}
-          rel="noopener"
-          title="RSS"
-        >
-          <Icon icon="rss" classes="text-muted small mx-2" />
-        </a>
-      </div>
-    );
-  }
-
-  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>
-                  )}
-                </ul>
-              </div>
-              <div className="flex-grow-1 unselectable pointer mx-2"></div>
-              {this.isCurrentUser ? (
-                <button
-                  class="d-flex align-self-start btn btn-secondary mr-2"
-                  onClick={linkEvent(this, this.handleLogoutClick)}
-                >
-                  {i18n.t("logout")}
-                </button>
-              ) : (
-                <>
-                  <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}`}
-                  >
-                    {i18n.t("send_secure_message")}
-                  </a>
-                  <Link
-                    className={"d-flex align-self-start btn btn-secondary"}
-                    to={`/create_private_message/recipient/${pv.person.id}`}
-                  >
-                    {i18n.t("send_message")}
-                  </Link>
-                </>
-              )}
-            </div>
-            {pv.person.bio && (
-              <div className="d-flex align-items-center mb-2">
-                <div
-                  className="md-div"
-                  dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
-                />
-              </div>
-            )}
-            <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 })}
-                </li>
-                <li className="list-inline-item badge badge-light">
-                  {i18n.t("number_of_comments", {
-                    count: 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>
-    );
-  }
-
-  userSettings() {
-    return (
-      <div>
-        <div class="card border-secondary mb-3">
-          <div class="card-body">
-            {this.saveUserSettingsHtmlForm()}
-            <br />
-            {this.changePasswordHtmlForm()}
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-  changePasswordHtmlForm() {
-    return (
-      <>
-        <h5>{i18n.t("change_password")}</h5>
-        <form onSubmit={linkEvent(this, this.handleChangePasswordSubmit)}>
-          <div class="form-group row">
-            <label class="col-lg-5 col-form-label" htmlFor="user-password">
-              {i18n.t("new_password")}
-            </label>
-            <div class="col-lg-7">
-              <input
-                type="password"
-                id="user-password"
-                class="form-control"
-                value={this.state.changePasswordForm.new_password}
-                autoComplete="new-password"
-                maxLength={60}
-                onInput={linkEvent(this, this.handleNewPasswordChange)}
-              />
-            </div>
-          </div>
-          <div class="form-group row">
-            <label
-              class="col-lg-5 col-form-label"
-              htmlFor="user-verify-password"
-            >
-              {i18n.t("verify_password")}
-            </label>
-            <div class="col-lg-7">
-              <input
-                type="password"
-                id="user-verify-password"
-                class="form-control"
-                value={this.state.changePasswordForm.new_password_verify}
-                autoComplete="new-password"
-                maxLength={60}
-                onInput={linkEvent(this, this.handleNewPasswordVerifyChange)}
-              />
-            </div>
-          </div>
-          <div class="form-group row">
-            <label class="col-lg-5 col-form-label" htmlFor="user-old-password">
-              {i18n.t("old_password")}
-            </label>
-            <div class="col-lg-7">
-              <input
-                type="password"
-                id="user-old-password"
-                class="form-control"
-                value={this.state.changePasswordForm.old_password}
-                autoComplete="new-password"
-                maxLength={60}
-                onInput={linkEvent(this, this.handleOldPasswordChange)}
-              />
-            </div>
-          </div>
-          <div class="form-group">
-            <button type="submit" class="btn btn-block btn-secondary mr-4">
-              {this.state.changePasswordLoading ? (
-                <Spinner />
-              ) : (
-                capitalizeFirstLetter(i18n.t("save"))
-              )}
-            </button>
-          </div>
-        </form>
-      </>
-    );
-  }
-
-  saveUserSettingsHtmlForm() {
-    return (
-      <>
-        <h5>{i18n.t("settings")}</h5>
-        <form onSubmit={linkEvent(this, this.handleSaveUserSettingsSubmit)}>
-          <div class="form-group">
-            <label>{i18n.t("avatar")}</label>
-            <ImageUploadForm
-              uploadTitle={i18n.t("upload_avatar")}
-              imageSrc={this.state.saveUserSettingsForm.avatar}
-              onUpload={this.handleAvatarUpload}
-              onRemove={this.handleAvatarRemove}
-              rounded
-            />
-          </div>
-          <div class="form-group">
-            <label>{i18n.t("banner")}</label>
-            <ImageUploadForm
-              uploadTitle={i18n.t("upload_banner")}
-              imageSrc={this.state.saveUserSettingsForm.banner}
-              onUpload={this.handleBannerUpload}
-              onRemove={this.handleBannerRemove}
-            />
-          </div>
-          <div class="form-group">
-            <label htmlFor="user-language">{i18n.t("language")}</label>
-            <select
-              id="user-language"
-              value={this.state.saveUserSettingsForm.lang}
-              onChange={linkEvent(this, this.handleUserSettingsLangChange)}
-              class="ml-2 custom-select w-auto"
-            >
-              <option disabled aria-hidden="true">
-                {i18n.t("language")}
-              </option>
-              <option value="browser">{i18n.t("browser_default")}</option>
-              <option disabled aria-hidden="true">
-                â”€â”€
-              </option>
-              {languages.sort().map(lang => (
-                <option value={lang.code}>
-                  {ISO6391.getNativeName(lang.code) || lang.code}
-                </option>
-              ))}
-            </select>
-          </div>
-          <div class="form-group">
-            <label htmlFor="user-theme">{i18n.t("theme")}</label>
-            <select
-              id="user-theme"
-              value={this.state.saveUserSettingsForm.theme}
-              onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
-              class="ml-2 custom-select w-auto"
-            >
-              <option disabled aria-hidden="true">
-                {i18n.t("theme")}
-              </option>
-              <option value="browser">{i18n.t("browser_default")}</option>
-              {themes.map(theme => (
-                <option value={theme}>{theme}</option>
-              ))}
-            </select>
-          </div>
-          <form className="form-group">
-            <label>
-              <div class="mr-2">{i18n.t("type")}</div>
-            </label>
-            <ListingTypeSelect
-              type_={
-                Object.values(ListingType)[
-                  this.state.saveUserSettingsForm.default_listing_type
-                ]
-              }
-              showLocal={showLocal(this.isoData)}
-              onChange={this.handleUserSettingsListingTypeChange}
-            />
-          </form>
-          <form className="form-group">
-            <label>
-              <div class="mr-2">{i18n.t("sort_type")}</div>
-            </label>
-            <SortSelect
-              sort={
-                Object.values(SortType)[
-                  this.state.saveUserSettingsForm.default_sort_type
-                ]
-              }
-              onChange={this.handleUserSettingsSortTypeChange}
-            />
-          </form>
-          <div class="form-group row">
-            <label class="col-lg-5 col-form-label" htmlFor="display-name">
-              {i18n.t("display_name")}
-            </label>
-            <div class="col-lg-7">
-              <input
-                id="display-name"
-                type="text"
-                class="form-control"
-                placeholder={i18n.t("optional")}
-                value={this.state.saveUserSettingsForm.display_name}
-                onInput={linkEvent(
-                  this,
-                  this.handleUserSettingsPreferredUsernameChange
-                )}
-                pattern="^(?!@)(.+)$"
-                minLength={3}
-              />
-            </div>
-          </div>
-          <div class="form-group row">
-            <label class="col-lg-3 col-form-label" htmlFor="user-bio">
-              {i18n.t("bio")}
-            </label>
-            <div class="col-lg-9">
-              <MarkdownTextArea
-                initialContent={this.state.saveUserSettingsForm.bio}
-                onContentChange={this.handleUserSettingsBioChange}
-                maxLength={300}
-                hideNavigationWarnings
-              />
-            </div>
-          </div>
-          <div class="form-group row">
-            <label class="col-lg-3 col-form-label" htmlFor="user-email">
-              {i18n.t("email")}
-            </label>
-            <div class="col-lg-9">
-              <input
-                type="email"
-                id="user-email"
-                class="form-control"
-                placeholder={i18n.t("optional")}
-                value={this.state.saveUserSettingsForm.email}
-                onInput={linkEvent(this, this.handleUserSettingsEmailChange)}
-                minLength={3}
-              />
-            </div>
-          </div>
-          <div class="form-group row">
-            <label class="col-lg-5 col-form-label" htmlFor="matrix-user-id">
-              <a href={elementUrl} rel="noopener">
-                {i18n.t("matrix_user_id")}
-              </a>
-            </label>
-            <div class="col-lg-7">
-              <input
-                id="matrix-user-id"
-                type="text"
-                class="form-control"
-                placeholder="@user:example.com"
-                value={this.state.saveUserSettingsForm.matrix_user_id}
-                onInput={linkEvent(
-                  this,
-                  this.handleUserSettingsMatrixUserIdChange
-                )}
-                pattern="^@[A-Za-z0-9._=-]+:[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
-              />
-            </div>
-          </div>
-          {this.state.siteRes.site_view.site.enable_nsfw && (
-            <div class="form-group">
-              <div class="form-check">
-                <input
-                  class="form-check-input"
-                  id="user-show-nsfw"
-                  type="checkbox"
-                  checked={this.state.saveUserSettingsForm.show_nsfw}
-                  onChange={linkEvent(
-                    this,
-                    this.handleUserSettingsShowNsfwChange
-                  )}
-                />
-                <label class="form-check-label" htmlFor="user-show-nsfw">
-                  {i18n.t("show_nsfw")}
-                </label>
-              </div>
-            </div>
-          )}
-          <div class="form-group">
-            <div class="form-check">
-              <input
-                class="form-check-input"
-                id="user-show-scores"
-                type="checkbox"
-                checked={this.state.saveUserSettingsForm.show_scores}
-                onChange={linkEvent(
-                  this,
-                  this.handleUserSettingsShowScoresChange
-                )}
-              />
-              <label class="form-check-label" htmlFor="user-show-scores">
-                {i18n.t("show_scores")}
-              </label>
-            </div>
-          </div>
-          <div class="form-group">
-            <div class="form-check">
-              <input
-                class="form-check-input"
-                id="user-show-avatars"
-                type="checkbox"
-                checked={this.state.saveUserSettingsForm.show_avatars}
-                onChange={linkEvent(
-                  this,
-                  this.handleUserSettingsShowAvatarsChange
-                )}
-              />
-              <label class="form-check-label" htmlFor="user-show-avatars">
-                {i18n.t("show_avatars")}
-              </label>
-            </div>
-          </div>
-          <div class="form-group">
-            <div class="form-check">
-              <input
-                class="form-check-input"
-                id="user-bot-account"
-                type="checkbox"
-                checked={this.state.saveUserSettingsForm.bot_account}
-                onChange={linkEvent(this, this.handleUserSettingsBotAccount)}
-              />
-              <label class="form-check-label" htmlFor="user-bot-account">
-                {i18n.t("bot_account")}
-              </label>
-            </div>
-          </div>
-          <div class="form-group">
-            <div class="form-check">
-              <input
-                class="form-check-input"
-                id="user-show-bot-accounts"
-                type="checkbox"
-                checked={this.state.saveUserSettingsForm.show_bot_accounts}
-                onChange={linkEvent(
-                  this,
-                  this.handleUserSettingsShowBotAccounts
-                )}
-              />
-              <label class="form-check-label" htmlFor="user-show-bot-accounts">
-                {i18n.t("show_bot_accounts")}
-              </label>
-            </div>
-          </div>
-          <div class="form-group">
-            <div class="form-check">
-              <input
-                class="form-check-input"
-                id="user-show-read-posts"
-                type="checkbox"
-                checked={this.state.saveUserSettingsForm.show_read_posts}
-                onChange={linkEvent(this, this.handleUserSettingsShowReadPosts)}
-              />
-              <label class="form-check-label" htmlFor="user-show-read-posts">
-                {i18n.t("show_read_posts")}
-              </label>
-            </div>
-          </div>
-          <div class="form-group">
-            <div class="form-check">
-              <input
-                class="form-check-input"
-                id="user-show-new-post-notifs"
-                type="checkbox"
-                checked={this.state.saveUserSettingsForm.show_new_post_notifs}
-                onChange={linkEvent(
-                  this,
-                  this.handleUserSettingsShowNewPostNotifs
-                )}
-              />
-              <label
-                class="form-check-label"
-                htmlFor="user-show-new-post-notifs"
-              >
-                {i18n.t("show_new_post_notifs")}
-              </label>
-            </div>
-          </div>
-          <div class="form-group">
-            <div class="form-check">
-              <input
-                class="form-check-input"
-                id="user-send-notifications-to-email"
-                type="checkbox"
-                disabled={!this.state.saveUserSettingsForm.email}
-                checked={
-                  this.state.saveUserSettingsForm.send_notifications_to_email
-                }
-                onChange={linkEvent(
-                  this,
-                  this.handleUserSettingsSendNotificationsToEmailChange
-                )}
-              />
-              <label
-                class="form-check-label"
-                htmlFor="user-send-notifications-to-email"
-              >
-                {i18n.t("send_notifications_to_email")}
-              </label>
-            </div>
-          </div>
-          <div class="form-group">
-            <button type="submit" class="btn btn-block btn-secondary mr-4">
-              {this.state.saveUserSettingsLoading ? (
-                <Spinner />
-              ) : (
-                capitalizeFirstLetter(i18n.t("save"))
-              )}
-            </button>
-          </div>
-          <hr />
-          <div class="form-group">
-            <button
-              class="btn btn-block btn-danger"
-              onClick={linkEvent(
-                this,
-                this.handleDeleteAccountShowConfirmToggle
-              )}
-            >
-              {i18n.t("delete_account")}
-            </button>
-            {this.state.deleteAccountShowConfirm && (
-              <>
-                <div class="my-2 alert alert-danger" role="alert">
-                  {i18n.t("delete_account_confirm")}
-                </div>
-                <input
-                  type="password"
-                  value={this.state.deleteAccountForm.password}
-                  autoComplete="new-password"
-                  maxLength={60}
-                  onInput={linkEvent(
-                    this,
-                    this.handleDeleteAccountPasswordChange
-                  )}
-                  class="form-control my-2"
-                />
-                <button
-                  class="btn btn-danger mr-4"
-                  disabled={!this.state.deleteAccountForm.password}
-                  onClick={linkEvent(this, this.handleDeleteAccount)}
-                >
-                  {this.state.deleteAccountLoading ? (
-                    <Spinner />
-                  ) : (
-                    capitalizeFirstLetter(i18n.t("delete"))
-                  )}
-                </button>
-                <button
-                  class="btn btn-secondary"
-                  onClick={linkEvent(
-                    this,
-                    this.handleDeleteAccountShowConfirmToggle
-                  )}
-                >
-                  {i18n.t("cancel")}
-                </button>
-              </>
-            )}
-          </div>
-        </form>
-      </>
-    );
-  }
-
-  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>
-    );
-  }
-
-  follows() {
-    return (
-      <div>
-        {this.state.personRes.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">
-                {this.state.personRes.follows.map(cfv => (
-                  <li>
-                    <CommunityLink community={cfv.community} />
-                  </li>
-                ))}
-              </ul>
-            </div>
-          </div>
-        )}
-      </div>
-    );
-  }
-
-  updateUrl(paramUpdates: UrlParams) {
-    const page = paramUpdates.page || this.state.page;
-    const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
-    const sortStr = paramUpdates.sort || this.state.sort;
-
-    let typeView = `/u/${this.state.userName}`;
-
-    this.props.history.push(
-      `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
-    );
-    this.state.loading = true;
-    this.setState(this.state);
-    this.fetchUserData();
-  }
-
-  handlePageChange(page: number) {
-    this.updateUrl({ page });
-  }
-
-  handleSortChange(val: SortType) {
-    this.updateUrl({ sort: val, page: 1 });
-  }
-
-  handleViewChange(i: Person, event: any) {
-    i.updateUrl({
-      view: PersonDetailsView[Number(event.target.value)],
-      page: 1,
-    });
-  }
-
-  handleUserSettingsShowNsfwChange(i: Person, event: any) {
-    i.state.saveUserSettingsForm.show_nsfw = event.target.checked;
-    i.setState(i.state);
-  }
-
-  handleUserSettingsShowAvatarsChange(i: Person, event: any) {
-    i.state.saveUserSettingsForm.show_avatars = event.target.checked;
-    UserService.Instance.localUserView.local_user.show_avatars =
-      event.target.checked; // Just for instant updates
-    i.setState(i.state);
-  }
-
-  handleUserSettingsBotAccount(i: Person, event: any) {
-    i.state.saveUserSettingsForm.bot_account = event.target.checked;
-    i.setState(i.state);
-  }
-
-  handleUserSettingsShowBotAccounts(i: Person, event: any) {
-    i.state.saveUserSettingsForm.show_bot_accounts = event.target.checked;
-    i.setState(i.state);
-  }
-
-  handleUserSettingsShowReadPosts(i: Person, event: any) {
-    i.state.saveUserSettingsForm.show_read_posts = event.target.checked;
-    i.setState(i.state);
-  }
-
-  handleUserSettingsShowNewPostNotifs(i: Person, event: any) {
-    i.state.saveUserSettingsForm.show_new_post_notifs = event.target.checked;
-    i.setState(i.state);
-  }
-
-  handleUserSettingsShowScoresChange(i: Person, event: any) {
-    i.state.saveUserSettingsForm.show_scores = event.target.checked;
-    UserService.Instance.localUserView.local_user.show_scores =
-      event.target.checked; // Just for instant updates
-    i.setState(i.state);
-  }
-
-  handleUserSettingsSendNotificationsToEmailChange(i: Person, event: any) {
-    i.state.saveUserSettingsForm.send_notifications_to_email =
-      event.target.checked;
-    i.setState(i.state);
-  }
-
-  handleUserSettingsThemeChange(i: Person, event: any) {
-    i.state.saveUserSettingsForm.theme = event.target.value;
-    setTheme(event.target.value, true);
-    i.setState(i.state);
-  }
-
-  handleUserSettingsLangChange(i: Person, event: any) {
-    i.state.saveUserSettingsForm.lang = event.target.value;
-    i18n.changeLanguage(getLanguage(i.state.saveUserSettingsForm.lang));
-    i.setState(i.state);
-  }
-
-  handleUserSettingsSortTypeChange(val: SortType) {
-    this.state.saveUserSettingsForm.default_sort_type =
-      Object.keys(SortType).indexOf(val);
-    this.setState(this.state);
-  }
-
-  handleUserSettingsListingTypeChange(val: ListingType) {
-    this.state.saveUserSettingsForm.default_listing_type =
-      Object.keys(ListingType).indexOf(val);
-    this.setState(this.state);
-  }
-
-  handleUserSettingsEmailChange(i: Person, event: any) {
-    i.state.saveUserSettingsForm.email = event.target.value;
-    i.setState(i.state);
-  }
-
-  handleUserSettingsBioChange(val: string) {
-    this.state.saveUserSettingsForm.bio = val;
-    this.setState(this.state);
-  }
-
-  handleAvatarUpload(url: string) {
-    this.state.saveUserSettingsForm.avatar = url;
-    this.setState(this.state);
-  }
-
-  handleAvatarRemove() {
-    this.state.saveUserSettingsForm.avatar = "";
-    this.setState(this.state);
-  }
-
-  handleBannerUpload(url: string) {
-    this.state.saveUserSettingsForm.banner = url;
-    this.setState(this.state);
-  }
-
-  handleBannerRemove() {
-    this.state.saveUserSettingsForm.banner = "";
-    this.setState(this.state);
-  }
-
-  handleUserSettingsPreferredUsernameChange(i: Person, event: any) {
-    i.state.saveUserSettingsForm.display_name = event.target.value;
-    i.setState(i.state);
-  }
-
-  handleUserSettingsMatrixUserIdChange(i: Person, event: any) {
-    i.state.saveUserSettingsForm.matrix_user_id = event.target.value;
-    if (
-      i.state.saveUserSettingsForm.matrix_user_id == "" &&
-      !UserService.Instance.localUserView.person.matrix_user_id
-    ) {
-      i.state.saveUserSettingsForm.matrix_user_id = undefined;
-    }
-    i.setState(i.state);
-  }
-
-  handleNewPasswordChange(i: Person, event: any) {
-    i.state.changePasswordForm.new_password = event.target.value;
-    if (i.state.changePasswordForm.new_password == "") {
-      i.state.changePasswordForm.new_password = undefined;
-    }
-    i.setState(i.state);
-  }
-
-  handleNewPasswordVerifyChange(i: Person, event: any) {
-    i.state.changePasswordForm.new_password_verify = event.target.value;
-    if (i.state.changePasswordForm.new_password_verify == "") {
-      i.state.changePasswordForm.new_password_verify = undefined;
-    }
-    i.setState(i.state);
-  }
-
-  handleOldPasswordChange(i: Person, event: any) {
-    i.state.changePasswordForm.old_password = event.target.value;
-    if (i.state.changePasswordForm.old_password == "") {
-      i.state.changePasswordForm.old_password = undefined;
-    }
-    i.setState(i.state);
-  }
-
-  handleSaveUserSettingsSubmit(i: Person, event: any) {
-    event.preventDefault();
-    i.state.saveUserSettingsLoading = true;
-    i.setState(i.state);
-
-    WebSocketService.Instance.send(
-      wsClient.saveUserSettings(i.state.saveUserSettingsForm)
-    );
-  }
-
-  handleChangePasswordSubmit(i: Person, event: any) {
-    event.preventDefault();
-    i.state.changePasswordLoading = true;
-    i.setState(i.state);
-
-    WebSocketService.Instance.send(
-      wsClient.changePassword(i.state.changePasswordForm)
-    );
-  }
-
-  handleDeleteAccountShowConfirmToggle(i: Person, event: any) {
-    event.preventDefault();
-    i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
-    i.setState(i.state);
-  }
-
-  handleDeleteAccountPasswordChange(i: Person, event: any) {
-    i.state.deleteAccountForm.password = event.target.value;
-    i.setState(i.state);
-  }
-
-  handleLogoutClick(i: Person) {
-    UserService.Instance.logout();
-    i.context.router.history.push("/");
-  }
-
-  handleDeleteAccount(i: Person, event: any) {
-    event.preventDefault();
-    i.state.deleteAccountLoading = true;
-    i.setState(i.state);
-
-    WebSocketService.Instance.send(
-      wsClient.deleteAccount(i.state.deleteAccountForm)
-    );
-  }
-
-  setUserInfo() {
-    if (this.isCurrentUser) {
-      this.state.saveUserSettingsForm.show_nsfw =
-        UserService.Instance.localUserView.local_user.show_nsfw;
-      this.state.saveUserSettingsForm.theme = UserService.Instance.localUserView
-        .local_user.theme
-        ? UserService.Instance.localUserView.local_user.theme
-        : "browser";
-      this.state.saveUserSettingsForm.default_sort_type =
-        UserService.Instance.localUserView.local_user.default_sort_type;
-      this.state.saveUserSettingsForm.default_listing_type =
-        UserService.Instance.localUserView.local_user.default_listing_type;
-      this.state.saveUserSettingsForm.lang =
-        UserService.Instance.localUserView.local_user.lang;
-      this.state.saveUserSettingsForm.avatar =
-        UserService.Instance.localUserView.person.avatar;
-      this.state.saveUserSettingsForm.banner =
-        UserService.Instance.localUserView.person.banner;
-      this.state.saveUserSettingsForm.display_name =
-        UserService.Instance.localUserView.person.display_name;
-      this.state.saveUserSettingsForm.show_avatars =
-        UserService.Instance.localUserView.local_user.show_avatars;
-      this.state.saveUserSettingsForm.bot_account =
-        UserService.Instance.localUserView.person.bot_account;
-      this.state.saveUserSettingsForm.show_bot_accounts =
-        UserService.Instance.localUserView.local_user.show_bot_accounts;
-      this.state.saveUserSettingsForm.show_scores =
-        UserService.Instance.localUserView.local_user.show_scores;
-      this.state.saveUserSettingsForm.show_read_posts =
-        UserService.Instance.localUserView.local_user.show_read_posts;
-      this.state.saveUserSettingsForm.show_new_post_notifs =
-        UserService.Instance.localUserView.local_user.show_new_post_notifs;
-      this.state.saveUserSettingsForm.email =
-        UserService.Instance.localUserView.local_user.email;
-      this.state.saveUserSettingsForm.bio =
-        UserService.Instance.localUserView.person.bio;
-      this.state.saveUserSettingsForm.send_notifications_to_email =
-        UserService.Instance.localUserView.local_user.send_notifications_to_email;
-      this.state.saveUserSettingsForm.matrix_user_id =
-        UserService.Instance.localUserView.person.matrix_user_id;
-    }
-  }
-
-  parseMessage(msg: any) {
-    let op = wsUserOp(msg);
-    console.log(msg);
-    if (msg.error) {
-      toast(i18n.t(msg.error), "danger");
-      if (msg.error == "couldnt_find_that_username_or_email") {
-        this.context.router.history.push("/");
-      }
-      this.setState({
-        deleteAccountLoading: false,
-        saveUserSettingsLoading: false,
-        changePasswordLoading: false,
-      });
-      return;
-    } else if (msg.reconnect) {
-      this.fetchUserData();
-    } else if (op == UserOperation.GetPersonDetails) {
-      // 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.setUserInfo();
-      this.state.loading = false;
-      this.setState(this.state);
-      restoreScrollPosition(this.context);
-    } else if (op == UserOperation.SaveUserSettings) {
-      let data = wsJsonToRes<LoginResponse>(msg).data;
-      UserService.Instance.login(data);
-      this.state.personRes.person_view.person.bio =
-        this.state.saveUserSettingsForm.bio;
-      this.state.personRes.person_view.person.display_name =
-        this.state.saveUserSettingsForm.display_name;
-      this.state.personRes.person_view.person.banner =
-        this.state.saveUserSettingsForm.banner;
-      this.state.personRes.person_view.person.avatar =
-        this.state.saveUserSettingsForm.avatar;
-      this.state.saveUserSettingsLoading = false;
-      this.setState(this.state);
-
-      window.scrollTo(0, 0);
-    } else if (op == UserOperation.ChangePassword) {
-      let data = wsJsonToRes<LoginResponse>(msg).data;
-      UserService.Instance.login(data);
-      this.state.changePasswordLoading = false;
-      this.setState(this.state);
-      window.scrollTo(0, 0);
-      toast(i18n.t("password_changed"));
-    } else if (op == UserOperation.DeleteAccount) {
-      this.setState({
-        deleteAccountLoading: false,
-        deleteAccountShowConfirm: false,
-      });
-      UserService.Instance.logout();
-      window.location.href = "/";
-    } else if (op == UserOperation.AddAdmin) {
-      let data = wsJsonToRes<AddAdminResponse>(msg).data;
-      this.state.siteRes.admins = data.admins;
-      this.setState(this.state);
-    } else if (op == UserOperation.CreateCommentLike) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      createCommentLikeRes(data.comment_view, this.state.personRes.comments);
-      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);
-      this.setState(this.state);
-    } else if (op == UserOperation.CreateComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      if (
-        UserService.Instance.localUserView &&
-        data.comment_view.creator.id ==
-          UserService.Instance.localUserView.person.id
-      ) {
-        toast(i18n.t("reply_sent"));
-      }
-    } else if (op == UserOperation.SaveComment) {
-      let data = wsJsonToRes<CommentResponse>(msg).data;
-      saveCommentRes(data.comment_view, this.state.personRes.comments);
-      this.setState(this.state);
-    } else if (
-      op == UserOperation.EditPost ||
-      op == UserOperation.DeletePost ||
-      op == UserOperation.RemovePost ||
-      op == UserOperation.LockPost ||
-      op == UserOperation.StickyPost ||
-      op == UserOperation.SavePost
-    ) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
-      editPostFindRes(data.post_view, this.state.personRes.posts);
-      this.setState(this.state);
-    } else if (op == UserOperation.CreatePostLike) {
-      let data = wsJsonToRes<PostResponse>(msg).data;
-      createPostLikeFindRes(data.post_view, this.state.personRes.posts);
-      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);
-    }
-  }
-}
diff --git a/src/shared/components/person/profile.tsx b/src/shared/components/person/profile.tsx
new file mode 100644 (file)
index 0000000..815f2fd
--- /dev/null
@@ -0,0 +1,583 @@
+import { Component, linkEvent } from "inferno";
+import { Link } from "inferno-router";
+import {
+  AddAdminResponse,
+  BanPersonResponse,
+  BlockPersonResponse,
+  CommentResponse,
+  GetPersonDetails,
+  GetPersonDetailsResponse,
+  GetSiteResponse,
+  PostResponse,
+  SortType,
+  UserOperation,
+} from "lemmy-js-client";
+import moment from "moment";
+import { Subscription } from "rxjs";
+import { i18n } from "../../i18next";
+import { InitialFetchRequest, PersonDetailsView } from "../../interfaces";
+import { UserService, WebSocketService } from "../../services";
+import {
+  authField,
+  createCommentLikeRes,
+  createPostLikeFindRes,
+  editCommentRes,
+  editPostFindRes,
+  fetchLimit,
+  getUsernameFromProps,
+  mdToHtml,
+  previewLines,
+  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";
+import { Icon, Spinner } from "../common/icon";
+import { MomentTime } from "../common/moment-time";
+import { SortSelect } from "../common/sort-select";
+import { CommunityLink } from "../community/community-link";
+import { PersonDetails } from "./person-details";
+import { PersonListing } from "./person-listing";
+
+interface ProfileState {
+  personRes: GetPersonDetailsResponse;
+  userName: string;
+  view: PersonDetailsView;
+  sort: SortType;
+  page: number;
+  loading: boolean;
+  siteRes: GetSiteResponse;
+}
+
+interface ProfileProps {
+  view: PersonDetailsView;
+  sort: SortType;
+  page: number;
+  person_id: number | null;
+  username: string;
+}
+
+interface UrlParams {
+  view?: string;
+  sort?: SortType;
+  page?: number;
+}
+
+export class Profile extends Component<any, ProfileState> {
+  private isoData = setIsoData(this.context);
+  private subscription: Subscription;
+  private emptyState: ProfileState = {
+    personRes: undefined,
+    userName: getUsernameFromProps(this.props),
+    loading: true,
+    view: Profile.getViewFromProps(this.props.match.view),
+    sort: Profile.getSortTypeFromProps(this.props.match.sort),
+    page: Profile.getPageFromProps(this.props.match.page),
+    siteRes: this.isoData.site_res,
+  };
+
+  constructor(props: any, context: any) {
+    super(props, context);
+
+    this.state = this.emptyState;
+    this.handleSortChange = this.handleSortChange.bind(this);
+
+    this.parseMessage = this.parseMessage.bind(this);
+    this.subscription = wsSubscribe(this.parseMessage);
+
+    // 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;
+    } else {
+      this.fetchUserData();
+    }
+
+    setupTippy();
+  }
+
+  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),
+    };
+    WebSocketService.Instance.send(wsClient.getPersonDetails(form));
+  }
+
+  get isCurrentUser() {
+    return (
+      UserService.Instance.myUserInfo?.local_user_view.person.id ==
+      this.state.personRes.person_view.person.id
+    );
+  }
+
+  static getViewFromProps(view: string): PersonDetailsView {
+    return view ? PersonDetailsView[view] : PersonDetailsView.Overview;
+  }
+
+  static getSortTypeFromProps(sort: string): SortType {
+    return sort ? routeSortTypeToEnum(sort) : SortType.New;
+  }
+
+  static getPageFromProps(page: number): number {
+    return page ? Number(page) : 1;
+  }
+
+  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 view = this.getViewFromProps(pathSplit[4]);
+    let sort = this.getSortTypeFromProps(pathSplit[6]);
+    let page = this.getPageFromProps(Number(pathSplit[8]));
+
+    let form: GetPersonDetails = {
+      sort,
+      saved_only: 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_;
+    }
+  }
+
+  componentWillUnmount() {
+    this.subscription.unsubscribe();
+    saveScrollPosition(this.context);
+  }
+
+  static getDerivedStateFromProps(props: any): ProfileProps {
+    return {
+      view: this.getViewFromProps(props.match.params.view),
+      sort: this.getSortTypeFromProps(props.match.params.sort),
+      page: this.getPageFromProps(props.match.params.page),
+      person_id: Number(props.match.params.id) || null,
+      username: props.match.params.username,
+    };
+  }
+
+  componentDidUpdate(lastProps: any) {
+    // Necessary if you are on a post and you click another post (same route)
+    if (
+      lastProps.location.pathname.split("/")[2] !==
+      lastProps.history.location.pathname.split("/")[2]
+    ) {
+      // Couldnt get a refresh working. This does for now.
+      location.reload();
+    }
+  }
+
+  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;
+  }
+
+  render() {
+    return (
+      <div class="container">
+        {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()}
+                {UserService.Instance.myUserInfo && this.follows()}
+              </div>
+            )}
+          </div>
+        )}
+      </div>
+    );
+  }
+
+  viewRadios() {
+    return (
+      <div class="btn-group btn-group-toggle flex-wrap mb-2">
+        <label
+          className={`btn btn-outline-secondary pointer
+            ${this.state.view == PersonDetailsView.Overview && "active"}
+          `}
+        >
+          <input
+            type="radio"
+            value={PersonDetailsView.Overview}
+            checked={this.state.view === PersonDetailsView.Overview}
+            onChange={linkEvent(this, this.handleViewChange)}
+          />
+          {i18n.t("overview")}
+        </label>
+        <label
+          className={`btn btn-outline-secondary pointer
+            ${this.state.view == PersonDetailsView.Comments && "active"}
+          `}
+        >
+          <input
+            type="radio"
+            value={PersonDetailsView.Comments}
+            checked={this.state.view == PersonDetailsView.Comments}
+            onChange={linkEvent(this, this.handleViewChange)}
+          />
+          {i18n.t("comments")}
+        </label>
+        <label
+          className={`btn btn-outline-secondary pointer
+            ${this.state.view == PersonDetailsView.Posts && "active"}
+          `}
+        >
+          <input
+            type="radio"
+            value={PersonDetailsView.Posts}
+            checked={this.state.view == PersonDetailsView.Posts}
+            onChange={linkEvent(this, this.handleViewChange)}
+          />
+          {i18n.t("posts")}
+        </label>
+        <label
+          className={`btn btn-outline-secondary pointer
+            ${this.state.view == PersonDetailsView.Saved && "active"}
+          `}
+        >
+          <input
+            type="radio"
+            value={PersonDetailsView.Saved}
+            checked={this.state.view == PersonDetailsView.Saved}
+            onChange={linkEvent(this, this.handleViewChange)}
+          />
+          {i18n.t("saved")}
+        </label>
+      </div>
+    );
+  }
+
+  selects() {
+    return (
+      <div className="mb-2">
+        <span class="mr-3">{this.viewRadios()}</span>
+        <SortSelect
+          sort={this.state.sort}
+          onChange={this.handleSortChange}
+          hideHot
+          hideMostComments
+        />
+        <a
+          href={`/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`}
+          rel="noopener"
+          title="RSS"
+        >
+          <Icon icon="rss" classes="text-muted small mx-2" />
+        </a>
+      </div>
+    );
+  }
+
+  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>
+                  )}
+                </ul>
+              </div>
+              <div className="flex-grow-1 unselectable pointer mx-2"></div>
+              {!this.isCurrentUser && (
+                <>
+                  <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}`}
+                  >
+                    {i18n.t("send_secure_message")}
+                  </a>
+                  <Link
+                    className={"d-flex align-self-start btn btn-secondary"}
+                    to={`/create_private_message/recipient/${pv.person.id}`}
+                  >
+                    {i18n.t("send_message")}
+                  </Link>
+                </>
+              )}
+            </div>
+            {pv.person.bio && (
+              <div className="d-flex align-items-center mb-2">
+                <div
+                  className="md-div"
+                  dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
+                />
+              </div>
+            )}
+            <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 })}
+                </li>
+                <li className="list-inline-item badge badge-light">
+                  {i18n.t("number_of_comments", {
+                    count: 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>
+    );
+  }
+
+  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>
+    );
+  }
+
+  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>
+    );
+  }
+
+  updateUrl(paramUpdates: UrlParams) {
+    const page = paramUpdates.page || this.state.page;
+    const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
+    const sortStr = paramUpdates.sort || this.state.sort;
+
+    let typeView = `/u/${this.state.userName}`;
+
+    this.props.history.push(
+      `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
+    );
+    this.state.loading = true;
+    this.setState(this.state);
+    this.fetchUserData();
+  }
+
+  handlePageChange(page: number) {
+    this.updateUrl({ page });
+  }
+
+  handleSortChange(val: SortType) {
+    this.updateUrl({ sort: val, page: 1 });
+  }
+
+  handleViewChange(i: Profile, event: any) {
+    i.updateUrl({
+      view: PersonDetailsView[Number(event.target.value)],
+      page: 1,
+    });
+  }
+
+  parseMessage(msg: any) {
+    let op = wsUserOp(msg);
+    console.log(msg);
+    if (msg.error) {
+      toast(i18n.t(msg.error), "danger");
+      if (msg.error == "couldnt_find_that_username_or_email") {
+        this.context.router.history.push("/");
+      }
+      return;
+    } else if (msg.reconnect) {
+      this.fetchUserData();
+    } else if (op == UserOperation.GetPersonDetails) {
+      // 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;
+      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);
+    } else if (op == UserOperation.CreateCommentLike) {
+      let data = wsJsonToRes<CommentResponse>(msg).data;
+      createCommentLikeRes(data.comment_view, this.state.personRes.comments);
+      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);
+      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"));
+      }
+    } else if (op == UserOperation.SaveComment) {
+      let data = wsJsonToRes<CommentResponse>(msg).data;
+      saveCommentRes(data.comment_view, this.state.personRes.comments);
+      this.setState(this.state);
+    } else if (
+      op == UserOperation.EditPost ||
+      op == UserOperation.DeletePost ||
+      op == UserOperation.RemovePost ||
+      op == UserOperation.LockPost ||
+      op == UserOperation.StickyPost ||
+      op == UserOperation.SavePost
+    ) {
+      let data = wsJsonToRes<PostResponse>(msg).data;
+      editPostFindRes(data.post_view, this.state.personRes.posts);
+      this.setState(this.state);
+    } else if (op == UserOperation.CreatePostLike) {
+      let data = wsJsonToRes<PostResponse>(msg).data;
+      createPostLikeFindRes(data.post_view, this.state.personRes.posts);
+      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);
+    } else if (op == UserOperation.BlockPerson) {
+      let data = wsJsonToRes<BlockPersonResponse>(msg).data;
+      updatePersonBlock(data);
+    }
+  }
+}
diff --git a/src/shared/components/person/settings.tsx b/src/shared/components/person/settings.tsx
new file mode 100644 (file)
index 0000000..955cc30
--- /dev/null
@@ -0,0 +1,1118 @@
+import { Component, linkEvent } from "inferno";
+import ISO6391 from "iso-639-1";
+import {
+  BlockCommunity,
+  BlockCommunityResponse,
+  BlockPerson,
+  BlockPersonResponse,
+  ChangePassword,
+  CommunityBlockView,
+  CommunityView,
+  DeleteAccount,
+  GetSiteResponse,
+  ListingType,
+  LoginResponse,
+  PersonBlockView,
+  PersonViewSafe,
+  SaveUserSettings,
+  SortType,
+  UserOperation,
+} from "lemmy-js-client";
+import { Subscription } from "rxjs";
+import { i18n } from "../../i18next";
+import { UserService, WebSocketService } from "../../services";
+import {
+  authField,
+  capitalizeFirstLetter,
+  choicesConfig,
+  communitySelectName,
+  communityToChoice,
+  debounce,
+  elementUrl,
+  fetchCommunities,
+  fetchUsers,
+  getLanguage,
+  isBrowser,
+  languages,
+  personSelectName,
+  personToChoice,
+  setIsoData,
+  setTheme,
+  setupTippy,
+  showLocal,
+  themes,
+  toast,
+  updateCommunityBlock,
+  updatePersonBlock,
+  wsClient,
+  wsJsonToRes,
+  wsSubscribe,
+  wsUserOp,
+} from "../../utils";
+import { HtmlTags } from "../common/html-tags";
+import { Icon, Spinner } from "../common/icon";
+import { ImageUploadForm } from "../common/image-upload-form";
+import { ListingTypeSelect } from "../common/listing-type-select";
+import { MarkdownTextArea } from "../common/markdown-textarea";
+import { SortSelect } from "../common/sort-select";
+import { CommunityLink } from "../community/community-link";
+import { PersonListing } from "./person-listing";
+
+var Choices: any;
+if (isBrowser()) {
+  Choices = require("choices.js");
+}
+
+interface SettingsState {
+  saveUserSettingsForm: SaveUserSettings;
+  changePasswordForm: ChangePassword;
+  saveUserSettingsLoading: boolean;
+  changePasswordLoading: boolean;
+  deleteAccountLoading: boolean;
+  deleteAccountShowConfirm: boolean;
+  deleteAccountForm: DeleteAccount;
+  personBlocks: PersonBlockView[];
+  blockPersonId: number;
+  blockPerson?: PersonViewSafe;
+  communityBlocks: CommunityBlockView[];
+  blockCommunityId: number;
+  blockCommunity?: CommunityView;
+  currentTab: string;
+  siteRes: GetSiteResponse;
+}
+
+export class Settings extends Component<any, SettingsState> {
+  private isoData = setIsoData(this.context);
+  private blockPersonChoices: any;
+  private blockCommunityChoices: any;
+  private subscription: Subscription;
+  private emptyState: SettingsState = {
+    saveUserSettingsForm: {
+      auth: authField(false),
+    },
+    changePasswordForm: {
+      new_password: null,
+      new_password_verify: null,
+      old_password: null,
+      auth: authField(false),
+    },
+    saveUserSettingsLoading: null,
+    changePasswordLoading: false,
+    deleteAccountLoading: null,
+    deleteAccountShowConfirm: false,
+    deleteAccountForm: {
+      password: null,
+      auth: authField(false),
+    },
+    personBlocks: [],
+    blockPersonId: 0,
+    communityBlocks: [],
+    blockCommunityId: 0,
+    currentTab: "settings",
+    siteRes: this.isoData.site_res,
+  };
+
+  constructor(props: any, context: any) {
+    super(props, context);
+
+    this.state = this.emptyState;
+    this.handleSortTypeChange = this.handleSortTypeChange.bind(this);
+    this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
+    this.handleBioChange = this.handleBioChange.bind(this);
+
+    this.handleAvatarUpload = this.handleAvatarUpload.bind(this);
+    this.handleAvatarRemove = this.handleAvatarRemove.bind(this);
+
+    this.handleBannerUpload = this.handleBannerUpload.bind(this);
+    this.handleBannerRemove = this.handleBannerRemove.bind(this);
+
+    this.parseMessage = this.parseMessage.bind(this);
+    this.subscription = wsSubscribe(this.parseMessage);
+
+    this.setUserInfo();
+
+    setupTippy();
+  }
+
+  componentWillUnmount() {
+    this.subscription.unsubscribe();
+  }
+
+  get documentTitle(): string {
+    return i18n.t("settings");
+  }
+
+  render() {
+    return (
+      <div class="container">
+        <>
+          <HtmlTags
+            title={this.documentTitle}
+            path={this.context.router.route.match.url}
+            description={this.documentTitle}
+            image={this.state.saveUserSettingsForm.avatar}
+          />
+          <ul class="nav nav-tabs mb-2">
+            <li class="nav-item">
+              <button
+                class={`nav-link btn ${
+                  this.state.currentTab == "settings" && "active"
+                }`}
+                onClick={linkEvent(
+                  { ctx: this, tab: "settings" },
+                  this.handleSwitchTab
+                )}
+              >
+                {i18n.t("settings")}
+              </button>
+            </li>
+            <li class="nav-item">
+              <button
+                class={`nav-link btn ${
+                  this.state.currentTab == "blocks" && "active"
+                }`}
+                onClick={linkEvent(
+                  { ctx: this, tab: "blocks" },
+                  this.handleSwitchTab
+                )}
+              >
+                {i18n.t("blocks")}
+              </button>
+            </li>
+          </ul>
+          {this.state.currentTab == "settings" && this.userSettings()}
+          {this.state.currentTab == "blocks" && this.blockCards()}
+        </>
+      </div>
+    );
+  }
+
+  userSettings() {
+    return (
+      <div class="row">
+        <div class="col-12 col-md-6">
+          <div class="card border-secondary mb-3">
+            <div class="card-body">{this.saveUserSettingsHtmlForm()}</div>
+          </div>
+        </div>
+        <div class="col-12 col-md-6">
+          <div class="card border-secondary mb-3">
+            <div class="card-body">{this.changePasswordHtmlForm()}</div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  blockCards() {
+    return (
+      <div class="row">
+        <div class="col-12 col-md-6">
+          <div class="card border-secondary mb-3">
+            <div class="card-body">{this.blockUserCard()}</div>
+          </div>
+        </div>
+        <div class="col-12 col-md-6">
+          <div class="card border-secondary mb-3">
+            <div class="card-body">{this.blockCommunityCard()}</div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  changePasswordHtmlForm() {
+    return (
+      <>
+        <h5>{i18n.t("change_password")}</h5>
+        <form onSubmit={linkEvent(this, this.handleChangePasswordSubmit)}>
+          <div class="form-group row">
+            <label class="col-lg-5 col-form-label" htmlFor="user-password">
+              {i18n.t("new_password")}
+            </label>
+            <div class="col-lg-7">
+              <input
+                type="password"
+                id="user-password"
+                class="form-control"
+                value={this.state.changePasswordForm.new_password}
+                autoComplete="new-password"
+                maxLength={60}
+                onInput={linkEvent(this, this.handleNewPasswordChange)}
+              />
+            </div>
+          </div>
+          <div class="form-group row">
+            <label
+              class="col-lg-5 col-form-label"
+              htmlFor="user-verify-password"
+            >
+              {i18n.t("verify_password")}
+            </label>
+            <div class="col-lg-7">
+              <input
+                type="password"
+                id="user-verify-password"
+                class="form-control"
+                value={this.state.changePasswordForm.new_password_verify}
+                autoComplete="new-password"
+                maxLength={60}
+                onInput={linkEvent(this, this.handleNewPasswordVerifyChange)}
+              />
+            </div>
+          </div>
+          <div class="form-group row">
+            <label class="col-lg-5 col-form-label" htmlFor="user-old-password">
+              {i18n.t("old_password")}
+            </label>
+            <div class="col-lg-7">
+              <input
+                type="password"
+                id="user-old-password"
+                class="form-control"
+                value={this.state.changePasswordForm.old_password}
+                autoComplete="new-password"
+                maxLength={60}
+                onInput={linkEvent(this, this.handleOldPasswordChange)}
+              />
+            </div>
+          </div>
+          <div class="form-group">
+            <button type="submit" class="btn btn-block btn-secondary mr-4">
+              {this.state.changePasswordLoading ? (
+                <Spinner />
+              ) : (
+                capitalizeFirstLetter(i18n.t("save"))
+              )}
+            </button>
+          </div>
+        </form>
+      </>
+    );
+  }
+
+  blockUserCard() {
+    return (
+      <div>
+        {this.blockUserForm()}
+        {this.blockedUsersList()}
+      </div>
+    );
+  }
+
+  blockedUsersList() {
+    return (
+      <>
+        <h5>{i18n.t("blocked_users")}</h5>
+        <ul class="list-unstyled mb-0">
+          {this.state.personBlocks.map(pb => (
+            <li>
+              <span>
+                <PersonListing person={pb.target} />
+                <button
+                  className="btn btn-sm"
+                  onClick={linkEvent(
+                    { ctx: this, recipientId: pb.target.id },
+                    this.handleUnblockPerson
+                  )}
+                  data-tippy-content={i18n.t("unblock_user")}
+                >
+                  <Icon icon="x" classes="icon-inline" />
+                </button>
+              </span>
+            </li>
+          ))}
+        </ul>
+      </>
+    );
+  }
+
+  blockUserForm() {
+    return (
+      <div class="form-group row">
+        <label class="col-md-4 col-form-label" htmlFor="block-person-filter">
+          {i18n.t("block_user")}
+        </label>
+        <div class="col-md-8">
+          <select
+            class="form-control"
+            id="block-person-filter"
+            value={this.state.blockPersonId}
+          >
+            <option value="0">—</option>
+            {this.state.blockPerson && (
+              <option value={this.state.blockPerson.person.id}>
+                {personSelectName(this.state.blockPerson)}
+              </option>
+            )}
+          </select>
+        </div>
+      </div>
+    );
+  }
+
+  blockCommunityCard() {
+    return (
+      <div>
+        {this.blockCommunityForm()}
+        {this.blockedCommunitiesList()}
+      </div>
+    );
+  }
+
+  blockedCommunitiesList() {
+    return (
+      <>
+        <h5>{i18n.t("blocked_communities")}</h5>
+        <ul class="list-unstyled mb-0">
+          {this.state.communityBlocks.map(cb => (
+            <li>
+              <span>
+                <CommunityLink community={cb.community} />
+                <button
+                  className="btn btn-sm"
+                  onClick={linkEvent(
+                    { ctx: this, communityId: cb.community.id },
+                    this.handleUnblockCommunity
+                  )}
+                  data-tippy-content={i18n.t("unblock_community")}
+                >
+                  <Icon icon="x" classes="icon-inline" />
+                </button>
+              </span>
+            </li>
+          ))}
+        </ul>
+      </>
+    );
+  }
+
+  blockCommunityForm() {
+    return (
+      <div class="form-group row">
+        <label class="col-md-4 col-form-label" htmlFor="block-community-filter">
+          {i18n.t("block_community")}
+        </label>
+        <div class="col-md-8">
+          <select
+            class="form-control"
+            id="block-community-filter"
+            value={this.state.blockCommunityId}
+          >
+            <option value="0">—</option>
+            {this.state.blockCommunity && (
+              <option value={this.state.blockCommunity.community.id}>
+                {communitySelectName(this.state.blockCommunity)}
+              </option>
+            )}
+          </select>
+        </div>
+      </div>
+    );
+  }
+
+  saveUserSettingsHtmlForm() {
+    return (
+      <>
+        <h5>{i18n.t("settings")}</h5>
+        <form onSubmit={linkEvent(this, this.handleSaveSettingsSubmit)}>
+          <div class="form-group row">
+            <label class="col-lg-5 col-form-label" htmlFor="display-name">
+              {i18n.t("display_name")}
+            </label>
+            <div class="col-lg-7">
+              <input
+                id="display-name"
+                type="text"
+                class="form-control"
+                placeholder={i18n.t("optional")}
+                value={this.state.saveUserSettingsForm.display_name}
+                onInput={linkEvent(this, this.handleDisplayNameChange)}
+                pattern="^(?!@)(.+)$"
+                minLength={3}
+              />
+            </div>
+          </div>
+          <div class="form-group row">
+            <label class="col-lg-3 col-form-label" htmlFor="user-bio">
+              {i18n.t("bio")}
+            </label>
+            <div class="col-lg-9">
+              <MarkdownTextArea
+                initialContent={this.state.saveUserSettingsForm.bio}
+                onContentChange={this.handleBioChange}
+                maxLength={300}
+                hideNavigationWarnings
+              />
+            </div>
+          </div>
+          <div class="form-group row">
+            <label class="col-lg-3 col-form-label" htmlFor="user-email">
+              {i18n.t("email")}
+            </label>
+            <div class="col-lg-9">
+              <input
+                type="email"
+                id="user-email"
+                class="form-control"
+                placeholder={i18n.t("optional")}
+                value={this.state.saveUserSettingsForm.email}
+                onInput={linkEvent(this, this.handleEmailChange)}
+                minLength={3}
+              />
+            </div>
+          </div>
+          <div class="form-group row">
+            <label class="col-lg-5 col-form-label" htmlFor="matrix-user-id">
+              <a href={elementUrl} rel="noopener">
+                {i18n.t("matrix_user_id")}
+              </a>
+            </label>
+            <div class="col-lg-7">
+              <input
+                id="matrix-user-id"
+                type="text"
+                class="form-control"
+                placeholder="@user:example.com"
+                value={this.state.saveUserSettingsForm.matrix_user_id}
+                onInput={linkEvent(this, this.handleMatrixUserIdChange)}
+                pattern="^@[A-Za-z0-9._=-]+:[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
+              />
+            </div>
+          </div>
+          <div class="form-group">
+            <label>{i18n.t("avatar")}</label>
+            <ImageUploadForm
+              uploadTitle={i18n.t("upload_avatar")}
+              imageSrc={this.state.saveUserSettingsForm.avatar}
+              onUpload={this.handleAvatarUpload}
+              onRemove={this.handleAvatarRemove}
+              rounded
+            />
+          </div>
+          <div class="form-group">
+            <label>{i18n.t("banner")}</label>
+            <ImageUploadForm
+              uploadTitle={i18n.t("upload_banner")}
+              imageSrc={this.state.saveUserSettingsForm.banner}
+              onUpload={this.handleBannerUpload}
+              onRemove={this.handleBannerRemove}
+            />
+          </div>
+          <div class="form-group">
+            <label htmlFor="user-language">{i18n.t("language")}</label>
+            <select
+              id="user-language"
+              value={this.state.saveUserSettingsForm.lang}
+              onChange={linkEvent(this, this.handleLangChange)}
+              class="ml-2 custom-select w-auto"
+            >
+              <option disabled aria-hidden="true">
+                {i18n.t("language")}
+              </option>
+              <option value="browser">{i18n.t("browser_default")}</option>
+              <option disabled aria-hidden="true">
+                â”€â”€
+              </option>
+              {languages.sort().map(lang => (
+                <option value={lang.code}>
+                  {ISO6391.getNativeName(lang.code) || lang.code}
+                </option>
+              ))}
+            </select>
+          </div>
+          <div class="form-group">
+            <label htmlFor="user-theme">{i18n.t("theme")}</label>
+            <select
+              id="user-theme"
+              value={this.state.saveUserSettingsForm.theme}
+              onChange={linkEvent(this, this.handleThemeChange)}
+              class="ml-2 custom-select w-auto"
+            >
+              <option disabled aria-hidden="true">
+                {i18n.t("theme")}
+              </option>
+              <option value="browser">{i18n.t("browser_default")}</option>
+              {themes.map(theme => (
+                <option value={theme}>{theme}</option>
+              ))}
+            </select>
+          </div>
+          <form className="form-group">
+            <label>
+              <div class="mr-2">{i18n.t("type")}</div>
+            </label>
+            <ListingTypeSelect
+              type_={
+                Object.values(ListingType)[
+                  this.state.saveUserSettingsForm.default_listing_type
+                ]
+              }
+              showLocal={showLocal(this.isoData)}
+              onChange={this.handleListingTypeChange}
+            />
+          </form>
+          <form className="form-group">
+            <label>
+              <div class="mr-2">{i18n.t("sort_type")}</div>
+            </label>
+            <SortSelect
+              sort={
+                Object.values(SortType)[
+                  this.state.saveUserSettingsForm.default_sort_type
+                ]
+              }
+              onChange={this.handleSortTypeChange}
+            />
+          </form>
+          {this.state.siteRes.site_view.site.enable_nsfw && (
+            <div class="form-group">
+              <div class="form-check">
+                <input
+                  class="form-check-input"
+                  id="user-show-nsfw"
+                  type="checkbox"
+                  checked={this.state.saveUserSettingsForm.show_nsfw}
+                  onChange={linkEvent(this, this.handleShowNsfwChange)}
+                />
+                <label class="form-check-label" htmlFor="user-show-nsfw">
+                  {i18n.t("show_nsfw")}
+                </label>
+              </div>
+            </div>
+          )}
+          <div class="form-group">
+            <div class="form-check">
+              <input
+                class="form-check-input"
+                id="user-show-scores"
+                type="checkbox"
+                checked={this.state.saveUserSettingsForm.show_scores}
+                onChange={linkEvent(this, this.handleShowScoresChange)}
+              />
+              <label class="form-check-label" htmlFor="user-show-scores">
+                {i18n.t("show_scores")}
+              </label>
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="form-check">
+              <input
+                class="form-check-input"
+                id="user-show-avatars"
+                type="checkbox"
+                checked={this.state.saveUserSettingsForm.show_avatars}
+                onChange={linkEvent(this, this.handleShowAvatarsChange)}
+              />
+              <label class="form-check-label" htmlFor="user-show-avatars">
+                {i18n.t("show_avatars")}
+              </label>
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="form-check">
+              <input
+                class="form-check-input"
+                id="user-bot-account"
+                type="checkbox"
+                checked={this.state.saveUserSettingsForm.bot_account}
+                onChange={linkEvent(this, this.handleBotAccount)}
+              />
+              <label class="form-check-label" htmlFor="user-bot-account">
+                {i18n.t("bot_account")}
+              </label>
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="form-check">
+              <input
+                class="form-check-input"
+                id="user-show-bot-accounts"
+                type="checkbox"
+                checked={this.state.saveUserSettingsForm.show_bot_accounts}
+                onChange={linkEvent(this, this.handleShowBotAccounts)}
+              />
+              <label class="form-check-label" htmlFor="user-show-bot-accounts">
+                {i18n.t("show_bot_accounts")}
+              </label>
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="form-check">
+              <input
+                class="form-check-input"
+                id="user-show-read-posts"
+                type="checkbox"
+                checked={this.state.saveUserSettingsForm.show_read_posts}
+                onChange={linkEvent(this, this.handleReadPosts)}
+              />
+              <label class="form-check-label" htmlFor="user-show-read-posts">
+                {i18n.t("show_read_posts")}
+              </label>
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="form-check">
+              <input
+                class="form-check-input"
+                id="user-show-new-post-notifs"
+                type="checkbox"
+                checked={this.state.saveUserSettingsForm.show_new_post_notifs}
+                onChange={linkEvent(this, this.handleShowNewPostNotifs)}
+              />
+              <label
+                class="form-check-label"
+                htmlFor="user-show-new-post-notifs"
+              >
+                {i18n.t("show_new_post_notifs")}
+              </label>
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="form-check">
+              <input
+                class="form-check-input"
+                id="user-send-notifications-to-email"
+                type="checkbox"
+                disabled={!this.state.saveUserSettingsForm.email}
+                checked={
+                  this.state.saveUserSettingsForm.send_notifications_to_email
+                }
+                onChange={linkEvent(
+                  this,
+                  this.handleSendNotificationsToEmailChange
+                )}
+              />
+              <label
+                class="form-check-label"
+                htmlFor="user-send-notifications-to-email"
+              >
+                {i18n.t("send_notifications_to_email")}
+              </label>
+            </div>
+          </div>
+          <div class="form-group">
+            <button type="submit" class="btn btn-block btn-secondary mr-4">
+              {this.state.saveUserSettingsLoading ? (
+                <Spinner />
+              ) : (
+                capitalizeFirstLetter(i18n.t("save"))
+              )}
+            </button>
+          </div>
+          <hr />
+          <div class="form-group">
+            <button
+              class="btn btn-block btn-danger"
+              onClick={linkEvent(
+                this,
+                this.handleDeleteAccountShowConfirmToggle
+              )}
+            >
+              {i18n.t("delete_account")}
+            </button>
+            {this.state.deleteAccountShowConfirm && (
+              <>
+                <div class="my-2 alert alert-danger" role="alert">
+                  {i18n.t("delete_account_confirm")}
+                </div>
+                <input
+                  type="password"
+                  value={this.state.deleteAccountForm.password}
+                  autoComplete="new-password"
+                  maxLength={60}
+                  onInput={linkEvent(
+                    this,
+                    this.handleDeleteAccountPasswordChange
+                  )}
+                  class="form-control my-2"
+                />
+                <button
+                  class="btn btn-danger mr-4"
+                  disabled={!this.state.deleteAccountForm.password}
+                  onClick={linkEvent(this, this.handleDeleteAccount)}
+                >
+                  {this.state.deleteAccountLoading ? (
+                    <Spinner />
+                  ) : (
+                    capitalizeFirstLetter(i18n.t("delete"))
+                  )}
+                </button>
+                <button
+                  class="btn btn-secondary"
+                  onClick={linkEvent(
+                    this,
+                    this.handleDeleteAccountShowConfirmToggle
+                  )}
+                >
+                  {i18n.t("cancel")}
+                </button>
+              </>
+            )}
+          </div>
+        </form>
+      </>
+    );
+  }
+
+  setupBlockPersonChoices() {
+    if (isBrowser()) {
+      let selectId: any = document.getElementById("block-person-filter");
+      if (selectId) {
+        this.blockPersonChoices = new Choices(selectId, choicesConfig);
+        this.blockPersonChoices.passedElement.element.addEventListener(
+          "choice",
+          (e: any) => {
+            this.handleBlockPerson(Number(e.detail.choice.value));
+          },
+          false
+        );
+        this.blockPersonChoices.passedElement.element.addEventListener(
+          "search",
+          debounce(async (e: any) => {
+            let persons = (await fetchUsers(e.detail.value)).users;
+            let choices = persons.map(pvs => personToChoice(pvs));
+            this.blockPersonChoices.setChoices(choices, "value", "label", true);
+          }, 400),
+          false
+        );
+      }
+    }
+  }
+
+  setupBlockCommunityChoices() {
+    if (isBrowser()) {
+      let selectId: any = document.getElementById("block-community-filter");
+      if (selectId) {
+        this.blockCommunityChoices = new Choices(selectId, choicesConfig);
+        this.blockCommunityChoices.passedElement.element.addEventListener(
+          "choice",
+          (e: any) => {
+            this.handleBlockCommunity(Number(e.detail.choice.value));
+          },
+          false
+        );
+        this.blockCommunityChoices.passedElement.element.addEventListener(
+          "search",
+          debounce(async (e: any) => {
+            let communities = (await fetchCommunities(e.detail.value))
+              .communities;
+            let choices = communities.map(cv => communityToChoice(cv));
+            this.blockCommunityChoices.setChoices(
+              choices,
+              "value",
+              "label",
+              true
+            );
+          }, 400),
+          false
+        );
+      }
+    }
+  }
+
+  handleBlockPerson(personId: number) {
+    if (personId != 0) {
+      let blockUserForm: BlockPerson = {
+        person_id: personId,
+        block: true,
+        auth: authField(),
+      };
+      WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
+    }
+  }
+
+  handleUnblockPerson(i: { ctx: Settings; recipientId: number }) {
+    let blockUserForm: BlockPerson = {
+      person_id: i.recipientId,
+      block: false,
+      auth: authField(),
+    };
+    WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
+  }
+
+  handleBlockCommunity(community_id: number) {
+    if (community_id != 0) {
+      let blockCommunityForm: BlockCommunity = {
+        community_id,
+        block: true,
+        auth: authField(),
+      };
+      WebSocketService.Instance.send(
+        wsClient.blockCommunity(blockCommunityForm)
+      );
+    }
+  }
+
+  handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
+    let blockCommunityForm: BlockCommunity = {
+      community_id: i.communityId,
+      block: false,
+      auth: authField(),
+    };
+    WebSocketService.Instance.send(wsClient.blockCommunity(blockCommunityForm));
+  }
+
+  handleShowNsfwChange(i: Settings, event: any) {
+    i.state.saveUserSettingsForm.show_nsfw = event.target.checked;
+    i.setState(i.state);
+  }
+
+  handleShowAvatarsChange(i: Settings, event: any) {
+    i.state.saveUserSettingsForm.show_avatars = event.target.checked;
+    UserService.Instance.myUserInfo.local_user_view.local_user.show_avatars =
+      event.target.checked; // Just for instant updates
+    i.setState(i.state);
+  }
+
+  handleBotAccount(i: Settings, event: any) {
+    i.state.saveUserSettingsForm.bot_account = event.target.checked;
+    i.setState(i.state);
+  }
+
+  handleShowBotAccounts(i: Settings, event: any) {
+    i.state.saveUserSettingsForm.show_bot_accounts = event.target.checked;
+    i.setState(i.state);
+  }
+
+  handleReadPosts(i: Settings, event: any) {
+    i.state.saveUserSettingsForm.show_read_posts = event.target.checked;
+    i.setState(i.state);
+  }
+
+  handleShowNewPostNotifs(i: Settings, event: any) {
+    i.state.saveUserSettingsForm.show_new_post_notifs = event.target.checked;
+    i.setState(i.state);
+  }
+
+  handleShowScoresChange(i: Settings, event: any) {
+    i.state.saveUserSettingsForm.show_scores = event.target.checked;
+    UserService.Instance.myUserInfo.local_user_view.local_user.show_scores =
+      event.target.checked; // Just for instant updates
+    i.setState(i.state);
+  }
+
+  handleSendNotificationsToEmailChange(i: Settings, event: any) {
+    i.state.saveUserSettingsForm.send_notifications_to_email =
+      event.target.checked;
+    i.setState(i.state);
+  }
+
+  handleThemeChange(i: Settings, event: any) {
+    i.state.saveUserSettingsForm.theme = event.target.value;
+    setTheme(event.target.value, true);
+    i.setState(i.state);
+  }
+
+  handleLangChange(i: Settings, event: any) {
+    i.state.saveUserSettingsForm.lang = event.target.value;
+    i18n.changeLanguage(getLanguage(i.state.saveUserSettingsForm.lang));
+    i.setState(i.state);
+  }
+
+  handleSortTypeChange(val: SortType) {
+    this.state.saveUserSettingsForm.default_sort_type =
+      Object.keys(SortType).indexOf(val);
+    this.setState(this.state);
+  }
+
+  handleListingTypeChange(val: ListingType) {
+    this.state.saveUserSettingsForm.default_listing_type =
+      Object.keys(ListingType).indexOf(val);
+    this.setState(this.state);
+  }
+
+  handleEmailChange(i: Settings, event: any) {
+    i.state.saveUserSettingsForm.email = event.target.value;
+    i.setState(i.state);
+  }
+
+  handleBioChange(val: string) {
+    this.state.saveUserSettingsForm.bio = val;
+    this.setState(this.state);
+  }
+
+  handleAvatarUpload(url: string) {
+    this.state.saveUserSettingsForm.avatar = url;
+    this.setState(this.state);
+  }
+
+  handleAvatarRemove() {
+    this.state.saveUserSettingsForm.avatar = "";
+    this.setState(this.state);
+  }
+
+  handleBannerUpload(url: string) {
+    this.state.saveUserSettingsForm.banner = url;
+    this.setState(this.state);
+  }
+
+  handleBannerRemove() {
+    this.state.saveUserSettingsForm.banner = "";
+    this.setState(this.state);
+  }
+
+  handleDisplayNameChange(i: Settings, event: any) {
+    i.state.saveUserSettingsForm.display_name = event.target.value;
+    i.setState(i.state);
+  }
+
+  handleMatrixUserIdChange(i: Settings, event: any) {
+    i.state.saveUserSettingsForm.matrix_user_id = event.target.value;
+    if (
+      i.state.saveUserSettingsForm.matrix_user_id == "" &&
+      !UserService.Instance.myUserInfo.local_user_view.person.matrix_user_id
+    ) {
+      i.state.saveUserSettingsForm.matrix_user_id = undefined;
+    }
+    i.setState(i.state);
+  }
+
+  handleNewPasswordChange(i: Settings, event: any) {
+    i.state.changePasswordForm.new_password = event.target.value;
+    if (i.state.changePasswordForm.new_password == "") {
+      i.state.changePasswordForm.new_password = undefined;
+    }
+    i.setState(i.state);
+  }
+
+  handleNewPasswordVerifyChange(i: Settings, event: any) {
+    i.state.changePasswordForm.new_password_verify = event.target.value;
+    if (i.state.changePasswordForm.new_password_verify == "") {
+      i.state.changePasswordForm.new_password_verify = undefined;
+    }
+    i.setState(i.state);
+  }
+
+  handleOldPasswordChange(i: Settings, event: any) {
+    i.state.changePasswordForm.old_password = event.target.value;
+    if (i.state.changePasswordForm.old_password == "") {
+      i.state.changePasswordForm.old_password = undefined;
+    }
+    i.setState(i.state);
+  }
+
+  handleSaveSettingsSubmit(i: Settings, event: any) {
+    event.preventDefault();
+    i.state.saveUserSettingsLoading = true;
+    i.setState(i.state);
+
+    WebSocketService.Instance.send(
+      wsClient.saveUserSettings(i.state.saveUserSettingsForm)
+    );
+  }
+
+  handleChangePasswordSubmit(i: Settings, event: any) {
+    event.preventDefault();
+    i.state.changePasswordLoading = true;
+    i.setState(i.state);
+
+    WebSocketService.Instance.send(
+      wsClient.changePassword(i.state.changePasswordForm)
+    );
+  }
+
+  handleDeleteAccountShowConfirmToggle(i: Settings, event: any) {
+    event.preventDefault();
+    i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
+    i.setState(i.state);
+  }
+
+  handleDeleteAccountPasswordChange(i: Settings, event: any) {
+    i.state.deleteAccountForm.password = event.target.value;
+    i.setState(i.state);
+  }
+
+  handleLogoutClick(i: Settings) {
+    UserService.Instance.logout();
+    i.context.router.history.push("/");
+  }
+
+  handleDeleteAccount(i: Settings, event: any) {
+    event.preventDefault();
+    i.state.deleteAccountLoading = true;
+    i.setState(i.state);
+
+    WebSocketService.Instance.send(
+      wsClient.deleteAccount(i.state.deleteAccountForm)
+    );
+  }
+
+  handleSwitchTab(i: { ctx: Settings; tab: string }) {
+    i.ctx.setState({ currentTab: i.tab });
+
+    if (i.ctx.state.currentTab == "blocks") {
+      i.ctx.setupBlockPersonChoices();
+      i.ctx.setupBlockCommunityChoices();
+    }
+  }
+
+  setUserInfo() {
+    let luv = UserService.Instance.myUserInfo.local_user_view;
+    this.state.saveUserSettingsForm.show_nsfw = luv.local_user.show_nsfw;
+    this.state.saveUserSettingsForm.theme = luv.local_user.theme
+      ? luv.local_user.theme
+      : "browser";
+    this.state.saveUserSettingsForm.default_sort_type =
+      luv.local_user.default_sort_type;
+    this.state.saveUserSettingsForm.default_listing_type =
+      luv.local_user.default_listing_type;
+    this.state.saveUserSettingsForm.lang = luv.local_user.lang;
+    this.state.saveUserSettingsForm.avatar = luv.person.avatar;
+    this.state.saveUserSettingsForm.banner = luv.person.banner;
+    this.state.saveUserSettingsForm.display_name = luv.person.display_name;
+    this.state.saveUserSettingsForm.show_avatars = luv.local_user.show_avatars;
+    this.state.saveUserSettingsForm.bot_account = luv.person.bot_account;
+    this.state.saveUserSettingsForm.show_bot_accounts =
+      luv.local_user.show_bot_accounts;
+    this.state.saveUserSettingsForm.show_scores = luv.local_user.show_scores;
+    this.state.saveUserSettingsForm.show_read_posts =
+      luv.local_user.show_read_posts;
+    this.state.saveUserSettingsForm.show_new_post_notifs =
+      luv.local_user.show_new_post_notifs;
+    this.state.saveUserSettingsForm.email = luv.local_user.email;
+    this.state.saveUserSettingsForm.bio = luv.person.bio;
+    this.state.saveUserSettingsForm.send_notifications_to_email =
+      luv.local_user.send_notifications_to_email;
+    this.state.saveUserSettingsForm.matrix_user_id = luv.person.matrix_user_id;
+    this.state.personBlocks = UserService.Instance.myUserInfo.person_blocks;
+    this.state.communityBlocks =
+      UserService.Instance.myUserInfo.community_blocks;
+  }
+
+  parseMessage(msg: any) {
+    let op = wsUserOp(msg);
+    console.log(msg);
+    if (msg.error) {
+      toast(i18n.t(msg.error), "danger");
+      return;
+    } else if (op == UserOperation.SaveUserSettings) {
+      let data = wsJsonToRes<LoginResponse>(msg).data;
+      UserService.Instance.login(data);
+      this.state.saveUserSettingsLoading = false;
+      this.setState(this.state);
+
+      window.scrollTo(0, 0);
+    } else if (op == UserOperation.ChangePassword) {
+      let data = wsJsonToRes<LoginResponse>(msg).data;
+      UserService.Instance.login(data);
+      this.state.changePasswordLoading = false;
+      this.setState(this.state);
+      window.scrollTo(0, 0);
+      toast(i18n.t("password_changed"));
+    } else if (op == UserOperation.DeleteAccount) {
+      this.setState({
+        deleteAccountLoading: false,
+        deleteAccountShowConfirm: false,
+      });
+      UserService.Instance.logout();
+      window.location.href = "/";
+    } else if (op == UserOperation.BlockPerson) {
+      let data = wsJsonToRes<BlockPersonResponse>(msg).data;
+      this.setState({ personBlocks: updatePersonBlock(data) });
+    } else if (op == UserOperation.BlockCommunity) {
+      let data = wsJsonToRes<BlockCommunityResponse>(msg).data;
+      this.setState({ communityBlocks: updateCommunityBlock(data) });
+    }
+  }
+}
index d2945b588f73253820ff9ff8375362fb3be6f70b..b5c95c4a85fb68f14609d0b0757fe8c2ab820a70 100644 (file)
@@ -51,7 +51,7 @@ export class CreatePost extends Component<any, CreatePostState> {
     this.handlePostCreate = this.handlePostCreate.bind(this);
     this.state = this.emptyState;
 
-    if (!UserService.Instance.localUserView && isBrowser()) {
+    if (!UserService.Instance.myUserInfo && isBrowser()) {
       toast(i18n.t("not_logged_in"), "danger");
       this.context.router.history.push(`/login`);
     }
index f3e2c276c7f6b4e551f0222c06ef0d2e6a14a796..10309fbb26cfe0ee23c9de0e11fcf6b74264c8a3 100644 (file)
@@ -194,7 +194,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
                 <label
                   htmlFor="file-upload"
                   className={`${
-                    UserService.Instance.localUserView && "pointer"
+                    UserService.Instance.myUserInfo && "pointer"
                   } d-inline-block float-right text-muted font-weight-bold`}
                   data-tippy-content={i18n.t("upload_image")}
                 >
@@ -206,7 +206,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
                   accept="image/*,video/*"
                   name="file"
                   class="d-none"
-                  disabled={!UserService.Instance.localUserView}
+                  disabled={!UserService.Instance.myUserInfo}
                   onChange={linkEvent(this, this.handleImageUpload)}
                 />
               </form>
@@ -601,7 +601,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       let data = wsJsonToRes<PostResponse>(msg).data;
       if (
         data.post_view.creator.id ==
-        UserService.Instance.localUserView.person.id
+        UserService.Instance.myUserInfo.local_user_view.person.id
       ) {
         this.state.loading = false;
         this.props.onCreate(data.post_view);
@@ -610,7 +610,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       let data = wsJsonToRes<PostResponse>(msg).data;
       if (
         data.post_view.creator.id ==
-        UserService.Instance.localUserView.person.id
+        UserService.Instance.myUserInfo.local_user_view.person.id
       ) {
         this.state.loading = false;
         this.props.onEdit(data.post_view);
index f4374c24e594f87fa745c9f57c9d2652334d269a..9c6da3765acf58ca4e8e7f766d85c2f5a16f5160 100644 (file)
@@ -5,6 +5,7 @@ import {
   AddModToCommunity,
   BanFromCommunity,
   BanPerson,
+  BlockPerson,
   CommunityModeratorView,
   CreatePostLike,
   DeletePost,
@@ -285,6 +286,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
             post_view.creator.banned) && (
             <span className="mx-1 badge badge-danger">{i18n.t("banned")}</span>
           )}
+          {post_view.creator_blocked && (
+            <span className="mx-1 badge badge-danger">{"blocked"}</span>
+          )}
           {this.props.showCommunity && (
             <span>
               <span class="mx-1"> {i18n.t("to")} </span>
@@ -627,7 +631,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   postActions(mobile = false) {
     let post_view = this.props.post_view;
     return (
-      UserService.Instance.localUserView && (
+      UserService.Instance.myUserInfo && (
         <>
           {this.showBody && (
             <>
@@ -655,6 +659,16 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               >
                 <Icon icon="copy" classes="icon-inline" />
               </Link>
+              {!this.myPost && (
+                <button
+                  class="btn btn-link btn-animate text-muted py-0"
+                  onClick={linkEvent(this, this.handleBlockUserClick)}
+                  data-tippy-content={i18n.t("block_user")}
+                  aria-label={i18n.t("block_user")}
+                >
+                  <Icon icon="slash" classes="icon-inline" />
+                </button>
+              )}
             </>
           )}
           {this.myPost && this.showBody && (
@@ -1113,9 +1127,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
   private get myPost(): boolean {
     return (
-      UserService.Instance.localUserView &&
+      UserService.Instance.myUserInfo &&
       this.props.post_view.creator.id ==
-        UserService.Instance.localUserView.person.id
+        UserService.Instance.myUserInfo.local_user_view.person.id
     );
   }
 
@@ -1146,7 +1160,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         .concat(this.props.moderators.map(m => m.moderator.id));
 
       return canMod(
-        UserService.Instance.localUserView,
+        UserService.Instance.myUserInfo,
         adminsThenMods,
         this.props.post_view.creator.id
       );
@@ -1162,7 +1176,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         .concat(this.props.moderators.map(m => m.moderator.id));
 
       return canMod(
-        UserService.Instance.localUserView,
+        UserService.Instance.myUserInfo,
         adminsThenMods,
         this.props.post_view.creator.id,
         true
@@ -1176,7 +1190,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     return (
       this.props.admins &&
       canMod(
-        UserService.Instance.localUserView,
+        UserService.Instance.myUserInfo,
         this.props.admins.map(a => a.person.id),
         this.props.post_view.creator.id
       )
@@ -1186,10 +1200,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   get amCommunityCreator(): boolean {
     return (
       this.props.moderators &&
-      UserService.Instance.localUserView &&
+      UserService.Instance.myUserInfo &&
       this.props.post_view.creator.id !=
-        UserService.Instance.localUserView.person.id &&
-      UserService.Instance.localUserView.person.id ==
+        UserService.Instance.myUserInfo.local_user_view.person.id &&
+      UserService.Instance.myUserInfo.local_user_view.person.id ==
         this.props.moderators[0].moderator.id
     );
   }
@@ -1197,17 +1211,17 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   get amSiteCreator(): boolean {
     return (
       this.props.admins &&
-      UserService.Instance.localUserView &&
+      UserService.Instance.myUserInfo &&
       this.props.post_view.creator.id !=
-        UserService.Instance.localUserView.person.id &&
-      UserService.Instance.localUserView.person.id ==
+        UserService.Instance.myUserInfo.local_user_view.person.id &&
+      UserService.Instance.myUserInfo.local_user_view.person.id ==
         this.props.admins[0].person.id
     );
   }
 
   handlePostLike(i: PostListing, event: any) {
     event.preventDefault();
-    if (!UserService.Instance.localUserView) {
+    if (!UserService.Instance.myUserInfo) {
       this.context.router.history.push(`/login`);
     }
 
@@ -1240,7 +1254,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
   handlePostDisLike(i: PostListing, event: any) {
     event.preventDefault();
-    if (!UserService.Instance.localUserView) {
+    if (!UserService.Instance.myUserInfo) {
       this.context.router.history.push(`/login`);
     }
 
@@ -1287,6 +1301,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     this.setState(this.state);
   }
 
+  handleBlockUserClick(i: PostListing) {
+    let blockUserForm: BlockPerson = {
+      person_id: i.props.post_view.creator.id,
+      block: true,
+      auth: authField(),
+    };
+    WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
+  }
+
   handleDeleteClick(i: PostListing) {
     let deleteForm: DeletePost = {
       post_id: i.props.post_view.post.id,
index 58f1ae13a5ca43aa1cc3f13f5c8aa9c20ee63235..3186242a970c58fa7705822f8874fdc24bb9459f 100644 (file)
@@ -5,6 +5,7 @@ import {
   AddModToCommunityResponse,
   BanFromCommunityResponse,
   BanPersonResponse,
+  BlockPersonResponse,
   CommentResponse,
   CommunityResponse,
   GetCommunityResponse,
@@ -50,6 +51,7 @@ import {
   setOptionalAuth,
   setupTippy,
   toast,
+  updatePersonBlock,
   wsClient,
   wsJsonToRes,
   wsSubscribe,
@@ -237,8 +239,9 @@ export class Post extends Component<any, PostState> {
       : this.state.postRes.post_view.creator.id;
 
     if (
-      UserService.Instance.localUserView &&
-      UserService.Instance.localUserView.person.id == parent_person_id
+      UserService.Instance.myUserInfo &&
+      UserService.Instance.myUserInfo.local_user_view.person.id ==
+        parent_person_id
     ) {
       let form: MarkCommentAsRead = {
         comment_id: found.comment.id,
@@ -617,6 +620,9 @@ export class Post extends Component<any, PostState> {
       this.state.postRes.post_view.community = data.community_view.community;
       this.state.postRes.moderators = data.moderators;
       this.setState(this.state);
+    } else if (op == UserOperation.BlockPerson) {
+      let data = wsJsonToRes<BlockPersonResponse>(msg).data;
+      updatePersonBlock(data);
     }
   }
 }
index 5bd6a486d006588ee5f35310c5d434471feb90fd..b3129f60cbe1c4f5c9cb87278003f641d6644db3 100644 (file)
@@ -54,7 +54,7 @@ export class CreatePrivateMessage extends Component<
     this.parseMessage = this.parseMessage.bind(this);
     this.subscription = wsSubscribe(this.parseMessage);
 
-    if (!UserService.Instance.localUserView) {
+    if (!UserService.Instance.myUserInfo) {
       toast(i18n.t("not_logged_in"), "danger");
       this.context.router.history.push(`/login`);
     }
index 533a694a95672097001ad83d90cebeafbd95ec85..1a7c3b3376aa0552b64aaf80efd66059e398bb8b 100644 (file)
@@ -47,8 +47,8 @@ export class PrivateMessage extends Component<
 
   get mine(): boolean {
     return (
-      UserService.Instance.localUserView &&
-      UserService.Instance.localUserView.person.id ==
+      UserService.Instance.myUserInfo &&
+      UserService.Instance.myUserInfo.local_user_view.person.id ==
         this.props.private_message_view.creator.id
     );
   }
@@ -272,8 +272,9 @@ export class PrivateMessage extends Component<
 
   handlePrivateMessageCreate(message: PrivateMessageView) {
     if (
-      UserService.Instance.localUserView &&
-      message.creator.id == UserService.Instance.localUserView.person.id
+      UserService.Instance.myUserInfo &&
+      message.creator.id ==
+        UserService.Instance.myUserInfo.local_user_view.person.id
     ) {
       this.state.showReply = false;
       this.setState(this.state);
index d48631620f02c1f1b95eca43b283e5f8cc8aca09..92c16960257fb2363f85cf8ebab0ecc13921903f 100644 (file)
@@ -10,7 +10,8 @@ import { PasswordChange } from "./components/home/password_change";
 import { Setup } from "./components/home/setup";
 import { Modlog } from "./components/modlog";
 import { Inbox } from "./components/person/inbox";
-import { Person } from "./components/person/person";
+import { Profile } from "./components/person/profile";
+import { Settings } from "./components/person/settings";
 import { CreatePost } from "./components/post/create-post";
 import { Post } from "./components/post/post";
 import { CreatePrivateMessage } from "./components/private_message/create-private-message";
@@ -83,19 +84,23 @@ export const routes: IRoutePropsWithFetch[] = [
   },
   {
     path: `/u/:username/view/:view/sort/:sort/page/:page`,
-    component: Person,
-    fetchInitialData: req => Person.fetchInitialData(req),
+    component: Profile,
+    fetchInitialData: req => Profile.fetchInitialData(req),
   },
   {
     path: `/u/:username`,
-    component: Person,
-    fetchInitialData: req => Person.fetchInitialData(req),
+    component: Profile,
+    fetchInitialData: req => Profile.fetchInitialData(req),
   },
   {
     path: `/inbox`,
     component: Inbox,
     fetchInitialData: req => Inbox.fetchInitialData(req),
   },
+  {
+    path: `/settings`,
+    component: Settings,
+  },
   {
     path: `/modlog/community/:community_id`,
     component: Modlog,
index 355e802844f3d758dc0fe3082ea2f3c34db35ff7..ef6af05c2306c350316f9fcd2f4b97c38a69587f 100644 (file)
@@ -1,7 +1,7 @@
 // import Cookies from 'js-cookie';
 import IsomorphicCookie from "isomorphic-cookie";
 import jwt_decode from "jwt-decode";
-import { LocalUserSettingsView, LoginResponse } from "lemmy-js-client";
+import { LoginResponse, MyUserInfo } from "lemmy-js-client";
 import { BehaviorSubject, Subject } from "rxjs";
 
 interface Claims {
@@ -12,7 +12,7 @@ interface Claims {
 
 export class UserService {
   private static _instance: UserService;
-  public localUserView: LocalUserSettingsView;
+  public myUserInfo: MyUserInfo;
   public claims: Claims;
   public jwtSub: Subject<string> = new Subject<string>();
   public unreadCountSub: BehaviorSubject<number> = new BehaviorSubject<number>(
@@ -39,7 +39,7 @@ export class UserService {
   public logout() {
     IsomorphicCookie.remove("jwt", { secure: false });
     this.claims = undefined;
-    this.localUserView = undefined;
+    this.myUserInfo = undefined;
     // setTheme();
     this.jwtSub.next("");
     console.log("Logged out.");
index e4f3d9be72456f88f2210d2a9ab05b6b11f0dba8..9e07f7524e8787826eed9932bc6d641e7530a5ee 100644 (file)
@@ -1,13 +1,17 @@
 import emojiShortName from "emoji-short-name";
 import {
+  BlockCommunityResponse,
+  BlockPersonResponse,
   CommentView,
+  CommunityBlockView,
   CommunityView,
   GetSiteMetadata,
   GetSiteResponse,
   LemmyHttp,
   LemmyWebsocket,
   ListingType,
-  LocalUserSettingsView,
+  MyUserInfo,
+  PersonBlockView,
   PersonViewSafe,
   PostView,
   PrivateMessageView,
@@ -249,14 +253,16 @@ export function getUnixTime(text: string): number {
 }
 
 export function canMod(
-  localUserView: LocalUserSettingsView,
+  myUserInfo: MyUserInfo,
   modIds: number[],
   creator_id: number,
   onSelf = false
 ): boolean {
   // You can do moderator actions only on the mods added after you.
-  if (localUserView) {
-    let yourIndex = modIds.findIndex(id => id == localUserView.person.id);
+  if (myUserInfo) {
+    let yourIndex = modIds.findIndex(
+      id => id == myUserInfo.local_user_view.person.id
+    );
     if (yourIndex == -1) {
       return false;
     } else {
@@ -380,11 +386,11 @@ export function debounce(func: any, wait = 1000, immediate = false) {
 
 // TODO
 export function getLanguage(override?: string): string {
-  let localUserView = UserService.Instance.localUserView;
+  let myUserInfo = UserService.Instance.myUserInfo;
   let lang =
     override ||
-    (localUserView?.local_user.lang
-      ? localUserView.local_user.lang
+    (myUserInfo?.local_user_view.local_user.lang
+      ? myUserInfo.local_user_view.local_user.lang
       : "browser");
 
   if (lang == "browser" && isBrowser()) {
@@ -537,15 +543,15 @@ export function objectFlip(obj: any) {
 
 export function showAvatars(): boolean {
   return (
-    UserService.Instance.localUserView?.local_user.show_avatars ||
-    !UserService.Instance.localUserView
+    UserService.Instance.myUserInfo?.local_user_view.local_user.show_avatars ||
+    !UserService.Instance.myUserInfo
   );
 }
 
 export function showScores(): boolean {
   return (
-    UserService.Instance.localUserView?.local_user.show_scores ||
-    !UserService.Instance.localUserView
+    UserService.Instance.myUserInfo?.local_user_view.local_user.show_scores ||
+    !UserService.Instance.myUserInfo
   );
 }
 
@@ -850,9 +856,10 @@ function communitySearch(
 export function getListingTypeFromProps(props: any): ListingType {
   return props.match.params.listing_type
     ? routeListingTypeToEnum(props.match.params.listing_type)
-    : UserService.Instance.localUserView
+    : UserService.Instance.myUserInfo
     ? Object.values(ListingType)[
-        UserService.Instance.localUserView.local_user.default_listing_type
+        UserService.Instance.myUserInfo.local_user_view.local_user
+          .default_listing_type
       ]
     : ListingType.Local;
 }
@@ -873,9 +880,10 @@ export function getDataTypeFromProps(props: any): DataType {
 export function getSortTypeFromProps(props: any): SortType {
   return props.match.params.sort
     ? routeSortTypeToEnum(props.match.params.sort)
-    : UserService.Instance.localUserView
+    : UserService.Instance.myUserInfo
     ? Object.values(SortType)[
-        UserService.Instance.localUserView.local_user.default_sort_type
+        UserService.Instance.myUserInfo.local_user_view.local_user
+          .default_sort_type
       ]
     : SortType.Active;
 }
@@ -922,6 +930,44 @@ export function saveCommentRes(data: CommentView, comments: CommentView[]) {
   }
 }
 
+export function updatePersonBlock(
+  data: BlockPersonResponse
+): PersonBlockView[] {
+  if (data.blocked) {
+    UserService.Instance.myUserInfo.person_blocks.push({
+      person: UserService.Instance.myUserInfo.local_user_view.person,
+      target: data.person_view.person,
+    });
+    toast(`${i18n.t("blocked")} ${data.person_view.person.name}`);
+  } else {
+    UserService.Instance.myUserInfo.person_blocks =
+      UserService.Instance.myUserInfo.person_blocks.filter(
+        i => i.target.id != data.person_view.person.id
+      );
+    toast(`${i18n.t("unblocked")} ${data.person_view.person.name}`);
+  }
+  return UserService.Instance.myUserInfo.person_blocks;
+}
+
+export function updateCommunityBlock(
+  data: BlockCommunityResponse
+): CommunityBlockView[] {
+  if (data.blocked) {
+    UserService.Instance.myUserInfo.community_blocks.push({
+      person: UserService.Instance.myUserInfo.local_user_view.person,
+      community: data.community_view.community,
+    });
+    toast(`${i18n.t("blocked")} ${data.community_view.community.name}`);
+  } else {
+    UserService.Instance.myUserInfo.community_blocks =
+      UserService.Instance.myUserInfo.community_blocks.filter(
+        i => i.community.id != data.community_view.community.id
+      );
+    toast(`${i18n.t("unblocked")} ${data.community_view.community.name}`);
+  }
+  return UserService.Instance.myUserInfo.community_blocks;
+}
+
 export function createCommentLikeRes(
   data: CommentView,
   comments: CommentView[]
@@ -1065,9 +1111,13 @@ export function buildCommentsTree(
   let tree: CommentNodeI[] = [];
   for (let comment_view of comments) {
     let child = map.get(comment_view.comment.id);
-    if (comment_view.comment.parent_id) {
-      let parent_ = map.get(comment_view.comment.parent_id);
-      parent_.children.push(child);
+    let parent_id = comment_view.comment.parent_id;
+    if (parent_id) {
+      let parent = map.get(parent_id);
+      // Necessary because blocked comment might not exist
+      if (parent) {
+        parent.children.push(child);
+      }
     } else {
       tree.push(child);
     }
@@ -1315,14 +1365,14 @@ export const choicesConfig = {
   searchResultLimit: fetchLimit,
   classNames: {
     containerOuter: "choices",
-    containerInner: "choices__inner bg-light border-0",
+    containerInner: "choices__inner bg-secondary border-0",
     input: "form-control",
     inputCloned: "choices__input--cloned",
     list: "choices__list",
     listItems: "choices__list--multiple",
     listSingle: "choices__list--single",
     listDropdown: "choices__list--dropdown",
-    item: "choices__item bg-light",
+    item: "choices__item bg-secondary",
     itemSelectable: "choices__item--selectable",
     itemDisabled: "choices__item--disabled",
     itemChoice: "choices__item--choice",
@@ -1356,6 +1406,6 @@ export function personSelectName(pvs: PersonViewSafe): string {
 }
 
 export function initializeSite(site: GetSiteResponse) {
-  UserService.Instance.localUserView = site.my_user;
+  UserService.Instance.myUserInfo = site.my_user;
   i18n.changeLanguage(getLanguage());
 }
index 978bb0dd666137c5728288c3559d132efe6b93be..e2faec8edf639dd5d4428ca78c6e7929a33a0ac2 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -2863,9 +2863,9 @@ ee-first@1.1.1:
   integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
 
 electron-to-chromium@^1.3.811:
-  version "1.3.812"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.812.tgz#4c4fb407e0e1335056097f172e9f2c0a09efe77d"
-  integrity sha512-7KiUHsKAWtSrjVoTSzxQ0nPLr/a+qoxNZwkwd9LkylTOgOXSVXkQbpIVT0WAUQcI5gXq3SwOTCrK+WfINHOXQg==
+  version "1.3.813"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.813.tgz#751a007d71c00faed8b5e9edaf3634c14b9c5a1f"
+  integrity sha512-YcSRImHt6JZZ2sSuQ4Bzajtk98igQ0iKkksqlzZLzbh4p0OIyJRSvUbsgqfcR8txdfsoYCc4ym306t4p2kP/aw==
 
 emoji-regex@^7.0.1:
   version "7.0.3"
@@ -4996,10 +4996,10 @@ lcid@^1.0.0:
   dependencies:
     invert-kv "^1.0.0"
 
-lemmy-js-client@0.11.4-rc.12:
-  version "0.11.4-rc.12"
-  resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.11.4-rc.12.tgz#a238da35dbde18c9fc8f6f14cb6849d541d0051b"
-  integrity sha512-PzIFA/Q2j8i0ZXOWo0u/rR/RTt97v+G8a8jObHplq8UyyI3EzNIWZ5AS9514H5AoCIAMcDbwP4c/CQPiYf8yhA==
+lemmy-js-client@0.11.4-rc.14:
+  version "0.11.4-rc.14"
+  resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.11.4-rc.14.tgz#dcac5b8dc78c3b04e6b3630ff9351a94aa73e109"
+  integrity sha512-R8M+myyriNQljQlTweVqtUKGBpgmaM7RI4ebYb7N7sYr5Bk5Ip6v2qTNvKAV6BlsDOCTWANOonfeoz/cIerLEg==
 
 levn@^0.4.1:
   version "0.4.1"