]> Untitled Git - lemmy-ui.git/blobdiff - src/shared/components/person/settings.tsx
component classes v2
[lemmy-ui.git] / src / shared / components / person / settings.tsx
index 838c1ec1081b415d26d49f12a54009d914f7535e..b97064e6042262ad0446d32dc0c03fc4701f6554 100644 (file)
@@ -1,44 +1,31 @@
-import { None, Option, Some } from "@sniptt/monads";
+import { NoOptionI18nKeys } from "i18next";
 import { Component, linkEvent } from "inferno";
 import {
-  BlockCommunity,
   BlockCommunityResponse,
-  BlockPerson,
   BlockPersonResponse,
-  ChangePassword,
   CommunityBlockView,
-  CommunityView,
-  DeleteAccount,
+  DeleteAccountResponse,
   GetSiteResponse,
   ListingType,
   LoginResponse,
   PersonBlockView,
-  PersonViewSafe,
-  SaveUserSettings,
   SortType,
-  toUndefined,
-  UserOperation,
-  wsJsonToRes,
-  wsUserOp,
 } from "lemmy-js-client";
-import { Subscription } from "rxjs";
 import { i18n, languages } from "../../i18next";
-import { UserService, WebSocketService } from "../../services";
+import { UserService } from "../../services";
+import { HttpService, RequestState } from "../../services/HttpService";
 import {
-  auth,
+  Choice,
   capitalizeFirstLetter,
-  choicesConfig,
-  communitySelectName,
   communityToChoice,
   debounce,
   elementUrl,
-  enableNsfw,
+  emDash,
   fetchCommunities,
   fetchThemeList,
   fetchUsers,
-  getLanguages,
-  isBrowser,
-  personSelectName,
+  myAuth,
+  myAuthRequired,
   personToChoice,
   relTags,
   setIsoData,
@@ -48,137 +35,195 @@ import {
   toast,
   updateCommunityBlock,
   updatePersonBlock,
-  wsClient,
-  wsSubscribe,
 } from "../../utils";
 import { HtmlTags } from "../common/html-tags";
 import { Icon, Spinner } from "../common/icon";
 import { ImageUploadForm } from "../common/image-upload-form";
+import { LanguageSelect } from "../common/language-select";
 import { ListingTypeSelect } from "../common/listing-type-select";
 import { MarkdownTextArea } from "../common/markdown-textarea";
+import { SearchableSelect } from "../common/searchable-select";
 import { SortSelect } from "../common/sort-select";
+import Tabs from "../common/tabs";
 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;
-  deleteAccountForm: DeleteAccount;
+  saveRes: RequestState<LoginResponse>;
+  changePasswordRes: RequestState<LoginResponse>;
+  deleteAccountRes: RequestState<DeleteAccountResponse>;
+  // TODO redo these forms
+  saveUserSettingsForm: {
+    show_nsfw?: boolean;
+    theme?: string;
+    default_sort_type?: SortType;
+    default_listing_type?: ListingType;
+    interface_language?: string;
+    avatar?: string;
+    banner?: string;
+    display_name?: string;
+    email?: string;
+    bio?: string;
+    matrix_user_id?: string;
+    show_avatars?: boolean;
+    show_scores?: boolean;
+    send_notifications_to_email?: boolean;
+    bot_account?: boolean;
+    show_bot_accounts?: boolean;
+    show_read_posts?: boolean;
+    show_new_post_notifs?: boolean;
+    discussion_languages?: number[];
+    generate_totp_2fa?: boolean;
+  };
+  changePasswordForm: {
+    new_password?: string;
+    new_password_verify?: string;
+    old_password?: string;
+  };
+  deleteAccountForm: {
+    password?: string;
+  };
   personBlocks: PersonBlockView[];
-  blockPerson: Option<PersonViewSafe>;
   communityBlocks: CommunityBlockView[];
-  blockCommunityId: number;
-  blockCommunity?: CommunityView;
   currentTab: string;
   themeList: string[];
-  saveUserSettingsLoading: boolean;
-  changePasswordLoading: boolean;
-  deleteAccountLoading: boolean;
   deleteAccountShowConfirm: boolean;
   siteRes: GetSiteResponse;
+  searchCommunityLoading: boolean;
+  searchCommunityOptions: Choice[];
+  searchPersonLoading: boolean;
+  searchPersonOptions: Choice[];
 }
 
+type FilterType = "user" | "community";
+
+const Filter = ({
+  filterType,
+  options,
+  onChange,
+  onSearch,
+  loading,
+}: {
+  filterType: FilterType;
+  options: Choice[];
+  onSearch: (text: string) => void;
+  onChange: (choice: Choice) => void;
+  loading: boolean;
+}) => (
+  <div className="mb-3 row">
+    <label
+      className="col-md-4 col-form-label"
+      htmlFor={`block-${filterType}-filter`}
+    >
+      {i18n.t(`block_${filterType}` as NoOptionI18nKeys)}
+    </label>
+    <div className="col-md-8">
+      <SearchableSelect
+        id={`block-${filterType}-filter`}
+        options={[
+          { label: emDash, value: "0", disabled: true } as Choice,
+        ].concat(options)}
+        loading={loading}
+        onChange={onChange}
+        onSearch={onSearch}
+      />
+    </div>
+  </div>
+);
+
 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: new SaveUserSettings({
-      show_nsfw: None,
-      show_scores: None,
-      show_avatars: None,
-      show_read_posts: None,
-      show_bot_accounts: None,
-      show_new_post_notifs: None,
-      default_sort_type: None,
-      default_listing_type: None,
-      theme: None,
-      lang: None,
-      avatar: None,
-      banner: None,
-      display_name: None,
-      email: None,
-      bio: None,
-      matrix_user_id: None,
-      send_notifications_to_email: None,
-      bot_account: None,
-      auth: undefined,
-    }),
-    changePasswordForm: new ChangePassword({
-      new_password: undefined,
-      new_password_verify: undefined,
-      old_password: undefined,
-      auth: undefined,
-    }),
-    saveUserSettingsLoading: false,
-    changePasswordLoading: false,
-    deleteAccountLoading: false,
+  state: SettingsState = {
+    saveRes: { state: "empty" },
+    deleteAccountRes: { state: "empty" },
+    changePasswordRes: { state: "empty" },
+    saveUserSettingsForm: {},
+    changePasswordForm: {},
     deleteAccountShowConfirm: false,
-    deleteAccountForm: new DeleteAccount({
-      password: undefined,
-      auth: undefined,
-    }),
+    deleteAccountForm: {},
     personBlocks: [],
-    blockPerson: None,
     communityBlocks: [],
-    blockCommunityId: 0,
     currentTab: "settings",
     siteRes: this.isoData.site_res,
     themeList: [],
+    searchCommunityLoading: false,
+    searchCommunityOptions: [],
+    searchPersonLoading: false,
+    searchPersonOptions: [],
   };
 
   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.handleDiscussionLanguageChange =
+      this.handleDiscussionLanguageChange.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.userSettings = this.userSettings.bind(this);
+    this.blockCards = this.blockCards.bind(this);
+
+    this.handleBlockPerson = this.handleBlockPerson.bind(this);
+    this.handleBlockCommunity = this.handleBlockCommunity.bind(this);
+
+    const mui = UserService.Instance.myUserInfo;
+    if (mui) {
+      const {
+        local_user: {
+          show_nsfw,
+          theme,
+          default_sort_type,
+          default_listing_type,
+          interface_language,
+          show_avatars,
+          show_bot_accounts,
+          show_scores,
+          show_read_posts,
+          show_new_post_notifs,
+          send_notifications_to_email,
+          email,
+        },
+        person: {
+          avatar,
+          banner,
+          display_name,
+          bot_account,
+          bio,
+          matrix_user_id,
+        },
+      } = mui.local_user_view;
 
-    this.parseMessage = this.parseMessage.bind(this);
-    this.subscription = wsSubscribe(this.parseMessage);
-
-    if (UserService.Instance.myUserInfo.isSome()) {
-      let mui = UserService.Instance.myUserInfo.unwrap();
-      let luv = mui.local_user_view;
       this.state = {
         ...this.state,
         personBlocks: mui.person_blocks,
         communityBlocks: mui.community_blocks,
         saveUserSettingsForm: {
           ...this.state.saveUserSettingsForm,
-          show_nsfw: Some(luv.local_user.show_nsfw),
-          theme: Some(luv.local_user.theme ? luv.local_user.theme : "browser"),
-          default_sort_type: Some(luv.local_user.default_sort_type),
-          default_listing_type: Some(luv.local_user.default_listing_type),
-          lang: Some(luv.local_user.lang),
-          avatar: luv.person.avatar,
-          banner: luv.person.banner,
-          display_name: luv.person.display_name,
-          show_avatars: Some(luv.local_user.show_avatars),
-          bot_account: Some(luv.person.bot_account),
-          show_bot_accounts: Some(luv.local_user.show_bot_accounts),
-          show_scores: Some(luv.local_user.show_scores),
-          show_read_posts: Some(luv.local_user.show_read_posts),
-          show_new_post_notifs: Some(luv.local_user.show_new_post_notifs),
-          email: luv.local_user.email,
-          bio: luv.person.bio,
-          send_notifications_to_email: Some(
-            luv.local_user.send_notifications_to_email
-          ),
-          matrix_user_id: luv.person.matrix_user_id,
+          show_nsfw,
+          theme: theme ?? "browser",
+          default_sort_type,
+          default_listing_type,
+          interface_language,
+          discussion_languages: mui.discussion_languages,
+          avatar,
+          banner,
+          display_name,
+          show_avatars,
+          bot_account,
+          show_bot_accounts,
+          show_scores,
+          show_read_posts,
+          show_new_post_notifs,
+          email,
+          bio,
+          send_notifications_to_email,
+          matrix_user_id,
         },
       };
     }
@@ -189,55 +234,33 @@ export class Settings extends Component<any, SettingsState> {
     this.setState({ themeList: await fetchThemeList() });
   }
 
-  componentWillUnmount() {
-    this.subscription.unsubscribe();
-  }
-
   get documentTitle(): string {
     return i18n.t("settings");
   }
 
   render() {
     return (
-      <div className="container">
-        <>
-          <HtmlTags
-            title={this.documentTitle}
-            path={this.context.router.route.match.url}
-            description={Some(this.documentTitle)}
-            image={this.state.saveUserSettingsForm.avatar}
-          />
-          <ul className="nav nav-tabs mb-2">
-            <li className="nav-item">
-              <button
-                className={`nav-link btn ${
-                  this.state.currentTab == "settings" && "active"
-                }`}
-                onClick={linkEvent(
-                  { ctx: this, tab: "settings" },
-                  this.handleSwitchTab
-                )}
-              >
-                {i18n.t("settings")}
-              </button>
-            </li>
-            <li className="nav-item">
-              <button
-                className={`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 className="person-settings container-lg">
+        <HtmlTags
+          title={this.documentTitle}
+          path={this.context.router.route.match.url}
+          description={this.documentTitle}
+          image={this.state.saveUserSettingsForm.avatar}
+        />
+        <Tabs
+          tabs={[
+            {
+              key: "settings",
+              label: i18n.t("settings"),
+              getNode: this.userSettings,
+            },
+            {
+              key: "blocks",
+              label: i18n.t("blocks"),
+              getNode: this.blockCards,
+            },
+          ]}
+        />
       </div>
     );
   }
@@ -281,7 +304,7 @@ export class Settings extends Component<any, SettingsState> {
       <>
         <h5>{i18n.t("change_password")}</h5>
         <form onSubmit={linkEvent(this, this.handleChangePasswordSubmit)}>
-          <div className="form-group row">
+          <div className="mb-3 row">
             <label className="col-sm-5 col-form-label" htmlFor="user-password">
               {i18n.t("new_password")}
             </label>
@@ -297,7 +320,7 @@ export class Settings extends Component<any, SettingsState> {
               />
             </div>
           </div>
-          <div className="form-group row">
+          <div className="mb-3 row">
             <label
               className="col-sm-5 col-form-label"
               htmlFor="user-verify-password"
@@ -316,7 +339,7 @@ export class Settings extends Component<any, SettingsState> {
               />
             </div>
           </div>
-          <div className="form-group row">
+          <div className="mb-3 row">
             <label
               className="col-sm-5 col-form-label"
               htmlFor="user-old-password"
@@ -335,9 +358,12 @@ export class Settings extends Component<any, SettingsState> {
               />
             </div>
           </div>
-          <div className="form-group">
-            <button type="submit" className="btn btn-block btn-secondary mr-4">
-              {this.state.changePasswordLoading ? (
+          <div className="input-group mb-3">
+            <button
+              type="submit"
+              className="btn d-block btn-secondary me-4 w-100"
+            >
+              {this.state.changePasswordRes.state === "loading" ? (
                 <Spinner />
               ) : (
                 capitalizeFirstLetter(i18n.t("save"))
@@ -350,9 +376,17 @@ export class Settings extends Component<any, SettingsState> {
   }
 
   blockUserCard() {
+    const { searchPersonLoading, searchPersonOptions } = this.state;
+
     return (
       <div>
-        {this.blockUserForm()}
+        <Filter
+          filterType="user"
+          loading={searchPersonLoading}
+          onChange={this.handleBlockPerson}
+          onSearch={this.handlePersonSearch}
+          options={searchPersonOptions}
+        />
         {this.blockedUsersList()}
       </div>
     );
@@ -385,40 +419,18 @@ export class Settings extends Component<any, SettingsState> {
     );
   }
 
-  blockUserForm() {
-    return (
-      <div className="form-group row">
-        <label
-          className="col-md-4 col-form-label"
-          htmlFor="block-person-filter"
-        >
-          {i18n.t("block_user")}
-        </label>
-        <div className="col-md-8">
-          <select
-            className="form-control"
-            id="block-person-filter"
-            value={this.state.blockPerson.map(p => p.person.id).unwrapOr(0)}
-          >
-            <option value="0">—</option>
-            {this.state.blockPerson.match({
-              some: personView => (
-                <option value={personView.person.id}>
-                  {personSelectName(personView)}
-                </option>
-              ),
-              none: <></>,
-            })}
-          </select>
-        </div>
-      </div>
-    );
-  }
-
   blockCommunityCard() {
+    const { searchCommunityLoading, searchCommunityOptions } = this.state;
+
     return (
       <div>
-        {this.blockCommunityForm()}
+        <Filter
+          filterType="community"
+          loading={searchCommunityLoading}
+          onChange={this.handleBlockCommunity}
+          onSearch={this.handleCommunitySearch}
+          options={searchCommunityOptions}
+        />
         {this.blockedCommunitiesList()}
       </div>
     );
@@ -451,58 +463,31 @@ export class Settings extends Component<any, SettingsState> {
     );
   }
 
-  blockCommunityForm() {
-    return (
-      <div className="form-group row">
-        <label
-          className="col-md-4 col-form-label"
-          htmlFor="block-community-filter"
-        >
-          {i18n.t("block_community")}
-        </label>
-        <div className="col-md-8">
-          <select
-            className="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() {
+    const selectedLangs = this.state.saveUserSettingsForm.discussion_languages;
+
     return (
       <>
         <h5>{i18n.t("settings")}</h5>
         <form onSubmit={linkEvent(this, this.handleSaveSettingsSubmit)}>
-          <div className="form-group row">
-            <label className="col-sm-5 col-form-label" htmlFor="display-name">
+          <div className="mb-3 row">
+            <label className="col-sm-3 col-form-label" htmlFor="display-name">
               {i18n.t("display_name")}
             </label>
-            <div className="col-sm-7">
+            <div className="col-sm-9">
               <input
                 id="display-name"
                 type="text"
                 className="form-control"
                 placeholder={i18n.t("optional")}
-                value={toUndefined(
-                  this.state.saveUserSettingsForm.display_name
-                )}
+                value={this.state.saveUserSettingsForm.display_name}
                 onInput={linkEvent(this, this.handleDisplayNameChange)}
                 pattern="^(?!@)(.+)$"
                 minLength={3}
               />
             </div>
           </div>
-          <div className="form-group row">
+          <div className="mb-3 row">
             <label className="col-sm-3 col-form-label" htmlFor="user-bio">
               {i18n.t("bio")}
             </label>
@@ -510,14 +495,14 @@ export class Settings extends Component<any, SettingsState> {
               <MarkdownTextArea
                 initialContent={this.state.saveUserSettingsForm.bio}
                 onContentChange={this.handleBioChange}
-                maxLength={Some(300)}
-                placeholder={None}
-                buttonTitle={None}
+                maxLength={300}
                 hideNavigationWarnings
+                allLanguages={this.state.siteRes.all_languages}
+                siteLanguages={this.state.siteRes.discussion_languages}
               />
             </div>
           </div>
-          <div className="form-group row">
+          <div className="mb-3 row">
             <label className="col-sm-3 col-form-label" htmlFor="user-email">
               {i18n.t("email")}
             </label>
@@ -527,34 +512,34 @@ export class Settings extends Component<any, SettingsState> {
                 id="user-email"
                 className="form-control"
                 placeholder={i18n.t("optional")}
-                value={toUndefined(this.state.saveUserSettingsForm.email)}
+                value={this.state.saveUserSettingsForm.email}
                 onInput={linkEvent(this, this.handleEmailChange)}
                 minLength={3}
               />
             </div>
           </div>
-          <div className="form-group row">
-            <label className="col-sm-5 col-form-label" htmlFor="matrix-user-id">
+          <div className="mb-3 row">
+            <label className="col-sm-3 col-form-label" htmlFor="matrix-user-id">
               <a href={elementUrl} rel={relTags}>
                 {i18n.t("matrix_user_id")}
               </a>
             </label>
-            <div className="col-sm-7">
+            <div className="col-sm-9">
               <input
                 id="matrix-user-id"
                 type="text"
                 className="form-control"
                 placeholder="@user:example.com"
-                value={toUndefined(
-                  this.state.saveUserSettingsForm.matrix_user_id
-                )}
+                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 className="form-group row">
-            <label className="col-sm-3">{i18n.t("avatar")}</label>
+          <div className="mb-3 row">
+            <label className="col-sm-3 col-form-label">
+              {i18n.t("avatar")}
+            </label>
             <div className="col-sm-9">
               <ImageUploadForm
                 uploadTitle={i18n.t("upload_avatar")}
@@ -565,8 +550,10 @@ export class Settings extends Component<any, SettingsState> {
               />
             </div>
           </div>
-          <div className="form-group row">
-            <label className="col-sm-3">{i18n.t("banner")}</label>
+          <div className="mb-3 row">
+            <label className="col-sm-3 col-form-label">
+              {i18n.t("banner")}
+            </label>
             <div className="col-sm-9">
               <ImageUploadForm
                 uploadTitle={i18n.t("upload_banner")}
@@ -576,19 +563,19 @@ export class Settings extends Component<any, SettingsState> {
               />
             </div>
           </div>
-          <div className="form-group row">
-            <label className="col-sm-3" htmlFor="user-language">
-              {i18n.t("language")}
+          <div className="mb-3 row">
+            <label className="col-sm-3 form-label" htmlFor="user-language">
+              {i18n.t("interface_language")}
             </label>
             <div className="col-sm-9">
               <select
                 id="user-language"
-                value={toUndefined(this.state.saveUserSettingsForm.lang)}
-                onChange={linkEvent(this, this.handleLangChange)}
-                className="custom-select w-auto"
+                value={this.state.saveUserSettingsForm.interface_language}
+                onChange={linkEvent(this, this.handleInterfaceLangChange)}
+                className="form-select d-inline-block w-auto"
               >
                 <option disabled aria-hidden="true">
-                  {i18n.t("language")}
+                  {i18n.t("interface_language")}
                 </option>
                 <option value="browser">{i18n.t("browser_default")}</option>
                 <option disabled aria-hidden="true">
@@ -604,16 +591,25 @@ export class Settings extends Component<any, SettingsState> {
               </select>
             </div>
           </div>
-          <div className="form-group row">
-            <label className="col-sm-3" htmlFor="user-theme">
+          <LanguageSelect
+            allLanguages={this.state.siteRes.all_languages}
+            siteLanguages={this.state.siteRes.discussion_languages}
+            selectedLanguageIds={selectedLangs}
+            multiple={true}
+            showLanguageWarning={true}
+            showSite
+            onChange={this.handleDiscussionLanguageChange}
+          />
+          <div className="mb-3 row">
+            <label className="col-sm-3 col-form-label" htmlFor="user-theme">
               {i18n.t("theme")}
             </label>
             <div className="col-sm-9">
               <select
                 id="user-theme"
-                value={toUndefined(this.state.saveUserSettingsForm.theme)}
+                value={this.state.saveUserSettingsForm.theme}
                 onChange={linkEvent(this, this.handleThemeChange)}
-                className="custom-select w-auto"
+                className="form-select d-inline-block w-auto"
               >
                 <option disabled aria-hidden="true">
                   {i18n.t("theme")}
@@ -627,16 +623,13 @@ export class Settings extends Component<any, SettingsState> {
               </select>
             </div>
           </div>
-          <form className="form-group row">
-            <label className="col-sm-3">{i18n.t("type")}</label>
+          <form className="mb-3 row">
+            <label className="col-sm-3 col-form-label">{i18n.t("type")}</label>
             <div className="col-sm-9">
               <ListingTypeSelect
                 type_={
-                  Object.values(ListingType)[
-                    this.state.saveUserSettingsForm.default_listing_type.unwrapOr(
-                      1
-                    )
-                  ]
+                  this.state.saveUserSettingsForm.default_listing_type ??
+                  "Local"
                 }
                 showLocal={showLocal(this.isoData)}
                 showSubscribed
@@ -644,48 +637,40 @@ export class Settings extends Component<any, SettingsState> {
               />
             </div>
           </form>
-          <form className="form-group row">
-            <label className="col-sm-3">{i18n.t("sort_type")}</label>
+          <form className="mb-3 row">
+            <label className="col-sm-3 col-form-label">
+              {i18n.t("sort_type")}
+            </label>
             <div className="col-sm-9">
               <SortSelect
                 sort={
-                  Object.values(SortType)[
-                    this.state.saveUserSettingsForm.default_sort_type.unwrapOr(
-                      0
-                    )
-                  ]
+                  this.state.saveUserSettingsForm.default_sort_type ?? "Active"
                 }
                 onChange={this.handleSortTypeChange}
               />
             </div>
           </form>
-          {enableNsfw(this.state.siteRes) && (
-            <div className="form-group">
-              <div className="form-check">
-                <input
-                  className="form-check-input"
-                  id="user-show-nsfw"
-                  type="checkbox"
-                  checked={toUndefined(
-                    this.state.saveUserSettingsForm.show_nsfw
-                  )}
-                  onChange={linkEvent(this, this.handleShowNsfwChange)}
-                />
-                <label className="form-check-label" htmlFor="user-show-nsfw">
-                  {i18n.t("show_nsfw")}
-                </label>
-              </div>
+          <div className="input-group mb-3">
+            <div className="form-check">
+              <input
+                className="form-check-input"
+                id="user-show-nsfw"
+                type="checkbox"
+                checked={this.state.saveUserSettingsForm.show_nsfw}
+                onChange={linkEvent(this, this.handleShowNsfwChange)}
+              />
+              <label className="form-check-label" htmlFor="user-show-nsfw">
+                {i18n.t("show_nsfw")}
+              </label>
             </div>
-          )}
-          <div className="form-group">
+          </div>
+          <div className="input-group mb-3">
             <div className="form-check">
               <input
                 className="form-check-input"
                 id="user-show-scores"
                 type="checkbox"
-                checked={toUndefined(
-                  this.state.saveUserSettingsForm.show_scores
-                )}
+                checked={this.state.saveUserSettingsForm.show_scores}
                 onChange={linkEvent(this, this.handleShowScoresChange)}
               />
               <label className="form-check-label" htmlFor="user-show-scores">
@@ -693,15 +678,13 @@ export class Settings extends Component<any, SettingsState> {
               </label>
             </div>
           </div>
-          <div className="form-group">
+          <div className="input-group mb-3">
             <div className="form-check">
               <input
                 className="form-check-input"
                 id="user-show-avatars"
                 type="checkbox"
-                checked={toUndefined(
-                  this.state.saveUserSettingsForm.show_avatars
-                )}
+                checked={this.state.saveUserSettingsForm.show_avatars}
                 onChange={linkEvent(this, this.handleShowAvatarsChange)}
               />
               <label className="form-check-label" htmlFor="user-show-avatars">
@@ -709,15 +692,13 @@ export class Settings extends Component<any, SettingsState> {
               </label>
             </div>
           </div>
-          <div className="form-group">
+          <div className="input-group mb-3">
             <div className="form-check">
               <input
                 className="form-check-input"
                 id="user-bot-account"
                 type="checkbox"
-                checked={toUndefined(
-                  this.state.saveUserSettingsForm.bot_account
-                )}
+                checked={this.state.saveUserSettingsForm.bot_account}
                 onChange={linkEvent(this, this.handleBotAccount)}
               />
               <label className="form-check-label" htmlFor="user-bot-account">
@@ -725,15 +706,13 @@ export class Settings extends Component<any, SettingsState> {
               </label>
             </div>
           </div>
-          <div className="form-group">
+          <div className="input-group mb-3">
             <div className="form-check">
               <input
                 className="form-check-input"
                 id="user-show-bot-accounts"
                 type="checkbox"
-                checked={toUndefined(
-                  this.state.saveUserSettingsForm.show_bot_accounts
-                )}
+                checked={this.state.saveUserSettingsForm.show_bot_accounts}
                 onChange={linkEvent(this, this.handleShowBotAccounts)}
               />
               <label
@@ -744,15 +723,13 @@ export class Settings extends Component<any, SettingsState> {
               </label>
             </div>
           </div>
-          <div className="form-group">
+          <div className="input-group mb-3">
             <div className="form-check">
               <input
                 className="form-check-input"
                 id="user-show-read-posts"
                 type="checkbox"
-                checked={toUndefined(
-                  this.state.saveUserSettingsForm.show_read_posts
-                )}
+                checked={this.state.saveUserSettingsForm.show_read_posts}
                 onChange={linkEvent(this, this.handleReadPosts)}
               />
               <label
@@ -763,15 +740,13 @@ export class Settings extends Component<any, SettingsState> {
               </label>
             </div>
           </div>
-          <div className="form-group">
+          <div className="input-group mb-3">
             <div className="form-check">
               <input
                 className="form-check-input"
                 id="user-show-new-post-notifs"
                 type="checkbox"
-                checked={toUndefined(
-                  this.state.saveUserSettingsForm.show_new_post_notifs
-                )}
+                checked={this.state.saveUserSettingsForm.show_new_post_notifs}
                 onChange={linkEvent(this, this.handleShowNewPostNotifs)}
               />
               <label
@@ -782,16 +757,16 @@ export class Settings extends Component<any, SettingsState> {
               </label>
             </div>
           </div>
-          <div className="form-group">
+          <div className="input-group mb-3">
             <div className="form-check">
               <input
                 className="form-check-input"
                 id="user-send-notifications-to-email"
                 type="checkbox"
                 disabled={!this.state.saveUserSettingsForm.email}
-                checked={toUndefined(
+                checked={
                   this.state.saveUserSettingsForm.send_notifications_to_email
-                )}
+                }
                 onChange={linkEvent(
                   this,
                   this.handleSendNotificationsToEmailChange
@@ -805,9 +780,10 @@ export class Settings extends Component<any, SettingsState> {
               </label>
             </div>
           </div>
-          <div className="form-group">
-            <button type="submit" className="btn btn-block btn-secondary mr-4">
-              {this.state.saveUserSettingsLoading ? (
+          {this.totpSection()}
+          <div className="input-group mb-3">
+            <button type="submit" className="btn d-block btn-secondary me-4">
+              {this.state.saveRes.state === "loading" ? (
                 <Spinner />
               ) : (
                 capitalizeFirstLetter(i18n.t("save"))
@@ -815,9 +791,9 @@ export class Settings extends Component<any, SettingsState> {
             </button>
           </div>
           <hr />
-          <div className="form-group">
+          <div className="input-group mb-3">
             <button
-              className="btn btn-block btn-danger"
+              className="btn d-block btn-danger"
               onClick={linkEvent(
                 this,
                 this.handleDeleteAccountShowConfirmToggle
@@ -842,11 +818,11 @@ export class Settings extends Component<any, SettingsState> {
                   className="form-control my-2"
                 />
                 <button
-                  className="btn btn-danger mr-4"
+                  className="btn btn-danger me-4"
                   disabled={!this.state.deleteAccountForm.password}
                   onClick={linkEvent(this, this.handleDeleteAccount)}
                 >
-                  {this.state.deleteAccountLoading ? (
+                  {this.state.deleteAccountRes.state === "loading" ? (
                     <Spinner />
                   ) : (
                     capitalizeFirstLetter(i18n.t("delete"))
@@ -869,359 +845,391 @@ export class Settings extends Component<any, SettingsState> {
     );
   }
 
-  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) => {
-            try {
-              let persons = (await fetchUsers(e.detail.value)).users;
-              let choices = persons.map(pvs => personToChoice(pvs));
-              this.blockPersonChoices.setChoices(
-                choices,
-                "value",
-                "label",
-                true
-              );
-            } catch (err) {
-              console.error(err);
-            }
-          }),
-          false
-        );
-      }
-    }
+  totpSection() {
+    const totpUrl =
+      UserService.Instance.myUserInfo?.local_user_view.local_user.totp_2fa_url;
+
+    return (
+      <>
+        {!totpUrl && (
+          <div className="input-group mb-3">
+            <div className="form-check">
+              <input
+                className="form-check-input"
+                id="user-generate-totp"
+                type="checkbox"
+                checked={this.state.saveUserSettingsForm.generate_totp_2fa}
+                onChange={linkEvent(this, this.handleGenerateTotp)}
+              />
+              <label className="form-check-label" htmlFor="user-generate-totp">
+                {i18n.t("set_up_two_factor")}
+              </label>
+            </div>
+          </div>
+        )}
+
+        {totpUrl && (
+          <>
+            <div>
+              <a className="btn btn-secondary mb-2" href={totpUrl}>
+                {i18n.t("two_factor_link")}
+              </a>
+            </div>
+            <div className="input-group mb-3">
+              <div className="form-check">
+                <input
+                  className="form-check-input"
+                  id="user-remove-totp"
+                  type="checkbox"
+                  checked={
+                    this.state.saveUserSettingsForm.generate_totp_2fa == false
+                  }
+                  onChange={linkEvent(this, this.handleRemoveTotp)}
+                />
+                <label className="form-check-label" htmlFor="user-remove-totp">
+                  {i18n.t("remove_two_factor")}
+                </label>
+              </div>
+            </div>
+          </>
+        )}
+      </>
+    );
   }
 
-  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) => {
-            try {
-              let communities = (await fetchCommunities(e.detail.value))
-                .communities;
-              let choices = communities.map(cv => communityToChoice(cv));
-              this.blockCommunityChoices.setChoices(
-                choices,
-                "value",
-                "label",
-                true
-              );
-            } catch (err) {
-              console.log(err);
-            }
-          }),
-          false
-        );
-      }
+  handlePersonSearch = debounce(async (text: string) => {
+    this.setState({ searchPersonLoading: true });
+
+    const searchPersonOptions: Choice[] = [];
+
+    if (text.length > 0) {
+      searchPersonOptions.push(...(await fetchUsers(text)).map(personToChoice));
     }
-  }
 
-  handleBlockPerson(personId: number) {
-    if (personId != 0) {
-      let blockUserForm = new BlockPerson({
-        person_id: personId,
+    this.setState({
+      searchPersonLoading: false,
+      searchPersonOptions,
+    });
+  });
+
+  handleCommunitySearch = debounce(async (text: string) => {
+    this.setState({ searchCommunityLoading: true });
+
+    const searchCommunityOptions: Choice[] = [];
+
+    if (text.length > 0) {
+      searchCommunityOptions.push(
+        ...(await fetchCommunities(text)).map(communityToChoice)
+      );
+    }
+
+    this.setState({
+      searchCommunityLoading: false,
+      searchCommunityOptions,
+    });
+  });
+
+  async handleBlockPerson({ value }: Choice) {
+    if (value !== "0") {
+      const res = await HttpService.client.blockPerson({
+        person_id: Number(value),
         block: true,
-        auth: auth().unwrap(),
+        auth: myAuthRequired(),
       });
-      WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
+      this.personBlock(res);
     }
   }
 
-  handleUnblockPerson(i: { ctx: Settings; recipientId: number }) {
-    let blockUserForm = new BlockPerson({
-      person_id: i.recipientId,
+  async handleUnblockPerson({
+    ctx,
+    recipientId,
+  }: {
+    ctx: Settings;
+    recipientId: number;
+  }) {
+    const res = await HttpService.client.blockPerson({
+      person_id: recipientId,
       block: false,
-      auth: auth().unwrap(),
+      auth: myAuthRequired(),
     });
-    WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
+    ctx.personBlock(res);
   }
 
-  handleBlockCommunity(community_id: number) {
-    if (community_id != 0) {
-      let blockCommunityForm = new BlockCommunity({
-        community_id,
+  async handleBlockCommunity({ value }: Choice) {
+    if (value !== "0") {
+      const res = await HttpService.client.blockCommunity({
+        community_id: Number(value),
         block: true,
-        auth: auth().unwrap(),
+        auth: myAuthRequired(),
       });
-      WebSocketService.Instance.send(
-        wsClient.blockCommunity(blockCommunityForm)
-      );
+      this.communityBlock(res);
     }
   }
 
-  handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
-    let blockCommunityForm = new BlockCommunity({
-      community_id: i.communityId,
-      block: false,
-      auth: auth().unwrap(),
-    });
-    WebSocketService.Instance.send(wsClient.blockCommunity(blockCommunityForm));
+  async handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
+    const auth = myAuth();
+    if (auth) {
+      const res = await HttpService.client.blockCommunity({
+        community_id: i.communityId,
+        block: false,
+        auth: myAuthRequired(),
+      });
+      i.ctx.communityBlock(res);
+    }
   }
 
   handleShowNsfwChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_nsfw = Some(event.target.checked);
-    i.setState(i.state);
+    i.setState(
+      s => ((s.saveUserSettingsForm.show_nsfw = event.target.checked), s)
+    );
   }
 
   handleShowAvatarsChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_avatars = Some(event.target.checked);
-    UserService.Instance.myUserInfo.match({
-      some: mui =>
-        (mui.local_user_view.local_user.show_avatars = event.target.checked),
-      none: void 0,
-    });
-    i.setState(i.state);
+    const mui = UserService.Instance.myUserInfo;
+    if (mui) {
+      mui.local_user_view.local_user.show_avatars = event.target.checked;
+    }
+    i.setState(
+      s => ((s.saveUserSettingsForm.show_avatars = event.target.checked), s)
+    );
   }
 
   handleBotAccount(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.bot_account = Some(event.target.checked);
-    i.setState(i.state);
+    i.setState(
+      s => ((s.saveUserSettingsForm.bot_account = event.target.checked), s)
+    );
   }
 
   handleShowBotAccounts(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_bot_accounts = Some(event.target.checked);
-    i.setState(i.state);
+    i.setState(
+      s => (
+        (s.saveUserSettingsForm.show_bot_accounts = event.target.checked), s
+      )
+    );
   }
 
   handleReadPosts(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_read_posts = Some(event.target.checked);
-    i.setState(i.state);
+    i.setState(
+      s => ((s.saveUserSettingsForm.show_read_posts = event.target.checked), s)
+    );
   }
 
   handleShowNewPostNotifs(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_new_post_notifs = Some(
-      event.target.checked
+    i.setState(
+      s => (
+        (s.saveUserSettingsForm.show_new_post_notifs = event.target.checked), s
+      )
     );
-    i.setState(i.state);
   }
 
   handleShowScoresChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_scores = Some(event.target.checked);
-    UserService.Instance.myUserInfo.match({
-      some: mui =>
-        (mui.local_user_view.local_user.show_scores = event.target.checked),
-      none: void 0,
-    });
-    i.setState(i.state);
+    const mui = UserService.Instance.myUserInfo;
+    if (mui) {
+      mui.local_user_view.local_user.show_scores = event.target.checked;
+    }
+    i.setState(
+      s => ((s.saveUserSettingsForm.show_scores = event.target.checked), s)
+    );
+  }
+
+  handleGenerateTotp(i: Settings, event: any) {
+    // Coerce false to undefined here, so it won't generate it.
+    const checked: boolean | undefined = event.target.checked || undefined;
+    if (checked) {
+      toast(i18n.t("two_factor_setup_instructions"));
+    }
+    i.setState(s => ((s.saveUserSettingsForm.generate_totp_2fa = checked), s));
+  }
+
+  handleRemoveTotp(i: Settings, event: any) {
+    // Coerce true to undefined here, so it won't generate it.
+    const checked: boolean | undefined = !event.target.checked && undefined;
+    i.setState(s => ((s.saveUserSettingsForm.generate_totp_2fa = checked), s));
   }
 
   handleSendNotificationsToEmailChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.send_notifications_to_email = Some(
-      event.target.checked
+    i.setState(
+      s => (
+        (s.saveUserSettingsForm.send_notifications_to_email =
+          event.target.checked),
+        s
+      )
     );
-    i.setState(i.state);
   }
 
   handleThemeChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.theme = Some(event.target.value);
+    i.setState(s => ((s.saveUserSettingsForm.theme = event.target.value), s));
     setTheme(event.target.value, true);
-    i.setState(i.state);
   }
 
-  handleLangChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.lang = Some(event.target.value);
-    i18n.changeLanguage(
-      getLanguages(i.state.saveUserSettingsForm.lang.unwrap())[0]
+  handleInterfaceLangChange(i: Settings, event: any) {
+    const newLang = event.target.value ?? "browser";
+    i18n.changeLanguage(newLang === "browser" ? navigator.languages : newLang);
+
+    i.setState(
+      s => ((s.saveUserSettingsForm.interface_language = event.target.value), s)
     );
-    i.setState(i.state);
   }
 
-  handleSortTypeChange(val: SortType) {
+  handleDiscussionLanguageChange(val: number[]) {
     this.setState(
-      s => (
-        (s.saveUserSettingsForm.default_sort_type = Some(
-          Object.keys(SortType).indexOf(val)
-        )),
-        s
-      )
+      s => ((s.saveUserSettingsForm.discussion_languages = val), s)
     );
   }
 
+  handleSortTypeChange(val: SortType) {
+    this.setState(s => ((s.saveUserSettingsForm.default_sort_type = val), s));
+  }
+
   handleListingTypeChange(val: ListingType) {
     this.setState(
-      s => (
-        (s.saveUserSettingsForm.default_listing_type = Some(
-          Object.keys(ListingType).indexOf(val)
-        )),
-        s
-      )
+      s => ((s.saveUserSettingsForm.default_listing_type = val), s)
     );
   }
 
   handleEmailChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.email = Some(event.target.value);
-    i.setState(i.state);
+    i.setState(s => ((s.saveUserSettingsForm.email = event.target.value), s));
   }
 
   handleBioChange(val: string) {
-    this.setState(s => ((s.saveUserSettingsForm.bio = Some(val)), s));
+    this.setState(s => ((s.saveUserSettingsForm.bio = val), s));
   }
 
   handleAvatarUpload(url: string) {
-    this.setState(s => ((s.saveUserSettingsForm.avatar = Some(url)), s));
+    this.setState(s => ((s.saveUserSettingsForm.avatar = url), s));
   }
 
   handleAvatarRemove() {
-    this.setState(s => ((s.saveUserSettingsForm.avatar = Some("")), s));
+    this.setState(s => ((s.saveUserSettingsForm.avatar = ""), s));
   }
 
   handleBannerUpload(url: string) {
-    this.setState(s => ((s.saveUserSettingsForm.banner = Some(url)), s));
+    this.setState(s => ((s.saveUserSettingsForm.banner = url), s));
   }
 
   handleBannerRemove() {
-    this.setState(s => ((s.saveUserSettingsForm.banner = Some("")), s));
+    this.setState(s => ((s.saveUserSettingsForm.banner = ""), s));
   }
 
   handleDisplayNameChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.display_name = Some(event.target.value);
-    i.setState(i.state);
+    i.setState(
+      s => ((s.saveUserSettingsForm.display_name = event.target.value), s)
+    );
   }
 
   handleMatrixUserIdChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.matrix_user_id = Some(event.target.value);
-    i.setState(i.state);
+    i.setState(
+      s => ((s.saveUserSettingsForm.matrix_user_id = event.target.value), s)
+    );
   }
 
   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);
+    const newPass: string | undefined =
+      event.target.value == "" ? undefined : event.target.value;
+    i.setState(s => ((s.changePasswordForm.new_password = newPass), s));
   }
 
   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);
+    const newPassVerify: string | undefined =
+      event.target.value == "" ? undefined : event.target.value;
+    i.setState(
+      s => ((s.changePasswordForm.new_password_verify = newPassVerify), s)
+    );
   }
 
   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);
+    const oldPass: string | undefined =
+      event.target.value == "" ? undefined : event.target.value;
+    i.setState(s => ((s.changePasswordForm.old_password = oldPass), s));
   }
 
-  handleSaveSettingsSubmit(i: Settings, event: any) {
+  async handleSaveSettingsSubmit(i: Settings, event: any) {
     event.preventDefault();
-    i.setState({ saveUserSettingsLoading: true });
-    i.setState(s => ((s.saveUserSettingsForm.auth = auth().unwrap()), s));
+    i.setState({ saveRes: { state: "loading" } });
+
+    const saveRes = await HttpService.client.saveUserSettings({
+      ...i.state.saveUserSettingsForm,
+      auth: myAuthRequired(),
+    });
+    if (saveRes.state === "success") {
+      UserService.Instance.login(saveRes.data);
+      location.reload();
+      toast(i18n.t("saved"));
+      window.scrollTo(0, 0);
+    }
 
-    let form = new SaveUserSettings({ ...i.state.saveUserSettingsForm });
-    WebSocketService.Instance.send(wsClient.saveUserSettings(form));
+    i.setState({ saveRes });
   }
 
-  handleChangePasswordSubmit(i: Settings, event: any) {
+  async handleChangePasswordSubmit(i: Settings, event: any) {
     event.preventDefault();
-    i.setState({ changePasswordLoading: true });
-    i.setState(s => ((s.changePasswordForm.auth = auth().unwrap()), s));
-
-    let form = new ChangePassword({ ...i.state.changePasswordForm });
+    const { new_password, new_password_verify, old_password } =
+      i.state.changePasswordForm;
+
+    if (new_password && old_password && new_password_verify) {
+      i.setState({ changePasswordRes: { state: "loading" } });
+      const changePasswordRes = await HttpService.client.changePassword({
+        new_password,
+        new_password_verify,
+        old_password,
+        auth: myAuthRequired(),
+      });
+      if (changePasswordRes.state === "success") {
+        UserService.Instance.login(changePasswordRes.data);
+        window.scrollTo(0, 0);
+        toast(i18n.t("password_changed"));
+      }
 
-    WebSocketService.Instance.send(wsClient.changePassword(form));
+      i.setState({ changePasswordRes });
+    }
   }
 
-  handleDeleteAccountShowConfirmToggle(i: Settings, event: any) {
-    event.preventDefault();
+  handleDeleteAccountShowConfirmToggle(i: Settings) {
     i.setState({ deleteAccountShowConfirm: !i.state.deleteAccountShowConfirm });
   }
 
   handleDeleteAccountPasswordChange(i: Settings, event: any) {
-    i.state.deleteAccountForm.password = event.target.value;
-    i.setState(i.state);
+    i.setState(s => ((s.deleteAccountForm.password = event.target.value), s));
   }
 
-  handleDeleteAccount(i: Settings, event: any) {
-    event.preventDefault();
-    i.setState({ deleteAccountLoading: true });
-    i.setState(s => ((s.deleteAccountForm.auth = auth().unwrap()), s));
-
-    let form = new DeleteAccount({ ...i.state.deleteAccountForm });
+  async handleDeleteAccount(i: Settings) {
+    const password = i.state.deleteAccountForm.password;
+    if (password) {
+      i.setState({ deleteAccountRes: { state: "loading" } });
+      const deleteAccountRes = await HttpService.client.deleteAccount({
+        password,
+        auth: myAuthRequired(),
+      });
+      if (deleteAccountRes.state === "success") {
+        UserService.Instance.logout();
+        this.context.router.history.replace("/");
+      }
 
-    WebSocketService.Instance.send(wsClient.deleteAccount(form));
+      i.setState({ deleteAccountRes });
+    }
   }
 
   handleSwitchTab(i: { ctx: Settings; tab: string }) {
     i.ctx.setState({ currentTab: i.tab });
+  }
 
-    if (i.ctx.state.currentTab == "blocks") {
-      i.ctx.setupBlockPersonChoices();
-      i.ctx.setupBlockCommunityChoices();
+  personBlock(res: RequestState<BlockPersonResponse>) {
+    if (res.state === "success") {
+      updatePersonBlock(res.data);
+      const mui = UserService.Instance.myUserInfo;
+      if (mui) {
+        this.setState({ personBlocks: mui.person_blocks });
+      }
     }
   }
 
-  parseMessage(msg: any) {
-    let op = wsUserOp(msg);
-    console.log(msg);
-    if (msg.error) {
-      this.setState({
-        saveUserSettingsLoading: false,
-        changePasswordLoading: false,
-        deleteAccountLoading: false,
-      });
-      toast(i18n.t(msg.error), "danger");
-      return;
-    } else if (op == UserOperation.SaveUserSettings) {
-      let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
-      UserService.Instance.login(data);
-      this.setState({ saveUserSettingsLoading: false });
-      toast(i18n.t("saved"));
-      window.scrollTo(0, 0);
-    } else if (op == UserOperation.ChangePassword) {
-      let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
-      UserService.Instance.login(data);
-      this.setState({ changePasswordLoading: false });
-      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, BlockPersonResponse);
-      updatePersonBlock(data).match({
-        some: blocks => this.setState({ personBlocks: blocks }),
-        none: void 0,
-      });
-    } else if (op == UserOperation.BlockCommunity) {
-      let data = wsJsonToRes<BlockCommunityResponse>(
-        msg,
-        BlockCommunityResponse
-      );
-      updateCommunityBlock(data).match({
-        some: blocks => this.setState({ communityBlocks: blocks }),
-        none: void 0,
-      });
+  communityBlock(res: RequestState<BlockCommunityResponse>) {
+    if (res.state === "success") {
+      updateCommunityBlock(res.data);
+      const mui = UserService.Instance.myUserInfo;
+      if (mui) {
+        this.setState({ communityBlocks: mui.community_blocks });
+      }
     }
   }
 }