]> 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 95b25901afb97a778dfd0ebd41e74ef7535e868d..b97064e6042262ad0446d32dc0c03fc4701f6554 100644 (file)
@@ -1,26 +1,19 @@
 import { NoOptionI18nKeys } from "i18next";
 import { Component, linkEvent } from "inferno";
 import {
-  BlockCommunity,
   BlockCommunityResponse,
-  BlockPerson,
   BlockPersonResponse,
-  ChangePassword,
   CommunityBlockView,
-  DeleteAccount,
+  DeleteAccountResponse,
   GetSiteResponse,
   ListingType,
   LoginResponse,
   PersonBlockView,
-  SaveUserSettings,
   SortType,
-  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 {
   Choice,
   capitalizeFirstLetter,
@@ -28,12 +21,11 @@ import {
   debounce,
   elementUrl,
   emDash,
-  enableNsfw,
   fetchCommunities,
   fetchThemeList,
   fetchUsers,
-  getLanguages,
   myAuth,
+  myAuthRequired,
   personToChoice,
   relTags,
   setIsoData,
@@ -43,8 +35,6 @@ import {
   toast,
   updateCommunityBlock,
   updatePersonBlock,
-  wsClient,
-  wsSubscribe,
 } from "../../utils";
 import { HtmlTags } from "../common/html-tags";
 import { Icon, Spinner } from "../common/icon";
@@ -54,10 +44,14 @@ 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";
 
 interface SettingsState {
+  saveRes: RequestState<LoginResponse>;
+  changePasswordRes: RequestState<LoginResponse>;
+  deleteAccountRes: RequestState<DeleteAccountResponse>;
   // TODO redo these forms
   saveUserSettingsForm: {
     show_nsfw?: boolean;
@@ -93,9 +87,6 @@ interface SettingsState {
   communityBlocks: CommunityBlockView[];
   currentTab: string;
   themeList: string[];
-  saveUserSettingsLoading: boolean;
-  changePasswordLoading: boolean;
-  deleteAccountLoading: boolean;
   deleteAccountShowConfirm: boolean;
   siteRes: GetSiteResponse;
   searchCommunityLoading: boolean;
@@ -119,7 +110,7 @@ const Filter = ({
   onChange: (choice: Choice) => void;
   loading: boolean;
 }) => (
-  <div className="form-group row">
+  <div className="mb-3 row">
     <label
       className="col-md-4 col-form-label"
       htmlFor={`block-${filterType}-filter`}
@@ -142,13 +133,12 @@ const Filter = ({
 
 export class Settings extends Component<any, SettingsState> {
   private isoData = setIsoData(this.context);
-  private subscription?: Subscription;
   state: SettingsState = {
+    saveRes: { state: "empty" },
+    deleteAccountRes: { state: "empty" },
+    changePasswordRes: { state: "empty" },
     saveUserSettingsForm: {},
     changePasswordForm: {},
-    saveUserSettingsLoading: false,
-    changePasswordLoading: false,
-    deleteAccountLoading: false,
     deleteAccountShowConfirm: false,
     deleteAccountForm: {},
     personBlocks: [],
@@ -176,9 +166,11 @@ export class Settings extends Component<any, SettingsState> {
 
     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.parseMessage = this.parseMessage.bind(this);
-    this.subscription = wsSubscribe(this.parseMessage);
+    this.handleBlockPerson = this.handleBlockPerson.bind(this);
+    this.handleBlockCommunity = this.handleBlockCommunity.bind(this);
 
     const mui = UserService.Instance.myUserInfo;
     if (mui) {
@@ -242,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-lg">
-        <>
-          <HtmlTags
-            title={this.documentTitle}
-            path={this.context.router.route.match.url}
-            description={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>
     );
   }
@@ -334,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>
@@ -350,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"
@@ -369,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"
@@ -388,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"))
@@ -491,17 +464,17 @@ export class Settings extends Component<any, SettingsState> {
   }
 
   saveUserSettingsHtmlForm() {
-    let selectedLangs = this.state.saveUserSettingsForm.discussion_languages;
+    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"
@@ -514,7 +487,7 @@ export class Settings extends Component<any, SettingsState> {
               />
             </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>
@@ -529,7 +502,7 @@ export class Settings extends Component<any, SettingsState> {
               />
             </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>
@@ -545,13 +518,13 @@ export class Settings extends Component<any, SettingsState> {
               />
             </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"
@@ -563,8 +536,10 @@ export class Settings extends Component<any, SettingsState> {
               />
             </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")}
@@ -575,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")}
@@ -586,8 +563,8 @@ export class Settings extends Component<any, SettingsState> {
               />
             </div>
           </div>
-          <div className="form-group row">
-            <label className="col-sm-3" htmlFor="user-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">
@@ -595,7 +572,7 @@ export class Settings extends Component<any, SettingsState> {
                 id="user-language"
                 value={this.state.saveUserSettingsForm.interface_language}
                 onChange={linkEvent(this, this.handleInterfaceLangChange)}
-                className="custom-select w-auto"
+                className="form-select d-inline-block w-auto"
               >
                 <option disabled aria-hidden="true">
                   {i18n.t("interface_language")}
@@ -619,11 +596,12 @@ export class Settings extends Component<any, SettingsState> {
             siteLanguages={this.state.siteRes.discussion_languages}
             selectedLanguageIds={selectedLangs}
             multiple={true}
+            showLanguageWarning={true}
             showSite
             onChange={this.handleDiscussionLanguageChange}
           />
-          <div className="form-group row">
-            <label className="col-sm-3" htmlFor="user-theme">
+          <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">
@@ -631,7 +609,7 @@ export class Settings extends Component<any, SettingsState> {
                 id="user-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")}
@@ -645,8 +623,8 @@ 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_={
@@ -659,8 +637,10 @@ 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={
@@ -670,23 +650,21 @@ export class Settings extends Component<any, SettingsState> {
               />
             </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={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"
@@ -700,7 +678,7 @@ 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"
@@ -714,7 +692,7 @@ 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"
@@ -728,7 +706,7 @@ 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"
@@ -745,7 +723,7 @@ 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"
@@ -762,7 +740,7 @@ 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"
@@ -779,7 +757,7 @@ 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"
@@ -803,9 +781,9 @@ export class Settings extends Component<any, SettingsState> {
             </div>
           </div>
           {this.totpSection()}
-          <div className="form-group">
-            <button type="submit" className="btn btn-block btn-secondary mr-4">
-              {this.state.saveUserSettingsLoading ? (
+          <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"))
@@ -813,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
@@ -840,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"))
@@ -868,13 +846,13 @@ export class Settings extends Component<any, SettingsState> {
   }
 
   totpSection() {
-    let totpUrl =
+    const totpUrl =
       UserService.Instance.myUserInfo?.local_user_view.local_user.totp_2fa_url;
 
     return (
       <>
         {!totpUrl && (
-          <div className="form-group">
+          <div className="input-group mb-3">
             <div className="form-check">
               <input
                 className="form-check-input"
@@ -897,7 +875,7 @@ export class Settings extends Component<any, SettingsState> {
                 {i18n.t("two_factor_link")}
               </a>
             </div>
-            <div className="form-group">
+            <div className="input-group mb-3">
               <div className="form-check">
                 <input
                   className="form-check-input"
@@ -925,9 +903,7 @@ export class Settings extends Component<any, SettingsState> {
     const searchPersonOptions: Choice[] = [];
 
     if (text.length > 0) {
-      searchPersonOptions.push(
-        ...(await fetchUsers(text)).users.map(personToChoice)
-      );
+      searchPersonOptions.push(...(await fetchUsers(text)).map(personToChoice));
     }
 
     this.setState({
@@ -943,7 +919,7 @@ export class Settings extends Component<any, SettingsState> {
 
     if (text.length > 0) {
       searchCommunityOptions.push(
-        ...(await fetchCommunities(text)).communities.map(communityToChoice)
+        ...(await fetchCommunities(text)).map(communityToChoice)
       );
     }
 
@@ -953,137 +929,146 @@ export class Settings extends Component<any, SettingsState> {
     });
   });
 
-  handleBlockPerson({ value }: Choice) {
-    const auth = myAuth();
-    if (auth && value !== "0") {
-      const blockUserForm: BlockPerson = {
+  async handleBlockPerson({ value }: Choice) {
+    if (value !== "0") {
+      const res = await HttpService.client.blockPerson({
         person_id: Number(value),
         block: true,
-        auth,
-      };
-
-      WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
+        auth: myAuthRequired(),
+      });
+      this.personBlock(res);
     }
   }
 
-  handleUnblockPerson(i: { ctx: Settings; recipientId: number }) {
-    const auth = myAuth();
-    if (auth) {
-      const blockUserForm: BlockPerson = {
-        person_id: i.recipientId,
-        block: false,
-        auth,
-      };
-      WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
-    }
+  async handleUnblockPerson({
+    ctx,
+    recipientId,
+  }: {
+    ctx: Settings;
+    recipientId: number;
+  }) {
+    const res = await HttpService.client.blockPerson({
+      person_id: recipientId,
+      block: false,
+      auth: myAuthRequired(),
+    });
+    ctx.personBlock(res);
   }
 
-  handleBlockCommunity({ value }: Choice) {
-    const auth = myAuth();
-    if (auth && value !== "0") {
-      const blockCommunityForm: BlockCommunity = {
+  async handleBlockCommunity({ value }: Choice) {
+    if (value !== "0") {
+      const res = await HttpService.client.blockCommunity({
         community_id: Number(value),
         block: true,
-        auth,
-      };
-      WebSocketService.Instance.send(
-        wsClient.blockCommunity(blockCommunityForm)
-      );
+        auth: myAuthRequired(),
+      });
+      this.communityBlock(res);
     }
   }
 
-  handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
+  async handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
     const auth = myAuth();
     if (auth) {
-      const blockCommunityForm: BlockCommunity = {
+      const res = await HttpService.client.blockCommunity({
         community_id: i.communityId,
         block: false,
-        auth,
-      };
-      WebSocketService.Instance.send(
-        wsClient.blockCommunity(blockCommunityForm)
-      );
+        auth: myAuthRequired(),
+      });
+      i.ctx.communityBlock(res);
     }
   }
 
   handleShowNsfwChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_nsfw = 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 = event.target.checked;
-    let mui = UserService.Instance.myUserInfo;
+    const mui = UserService.Instance.myUserInfo;
     if (mui) {
       mui.local_user_view.local_user.show_avatars = event.target.checked;
     }
-    i.setState(i.state);
+    i.setState(
+      s => ((s.saveUserSettingsForm.show_avatars = event.target.checked), s)
+    );
   }
 
   handleBotAccount(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.bot_account = 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 = 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 = 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 = event.target.checked;
-    i.setState(i.state);
+    i.setState(
+      s => (
+        (s.saveUserSettingsForm.show_new_post_notifs = event.target.checked), s
+      )
+    );
   }
 
   handleShowScoresChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_scores = event.target.checked;
-    let mui = UserService.Instance.myUserInfo;
+    const mui = UserService.Instance.myUserInfo;
     if (mui) {
       mui.local_user_view.local_user.show_scores = event.target.checked;
     }
-    i.setState(i.state);
+    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.
-    let checked: boolean | undefined = event.target.checked || undefined;
+    const checked: boolean | undefined = event.target.checked || undefined;
     if (checked) {
       toast(i18n.t("two_factor_setup_instructions"));
     }
-    i.state.saveUserSettingsForm.generate_totp_2fa = checked;
-    i.setState(i.state);
+    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.
-    let checked: boolean | undefined = !event.target.checked && undefined;
-    i.state.saveUserSettingsForm.generate_totp_2fa = checked;
-    i.setState(i.state);
+    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 =
-      event.target.checked;
-    i.setState(i.state);
+    i.setState(
+      s => (
+        (s.saveUserSettingsForm.send_notifications_to_email =
+          event.target.checked),
+        s
+      )
+    );
   }
 
   handleThemeChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.theme = event.target.value;
+    i.setState(s => ((s.saveUserSettingsForm.theme = event.target.value), s));
     setTheme(event.target.value, true);
-    i.setState(i.state);
   }
 
   handleInterfaceLangChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.interface_language = event.target.value;
-    i18n.changeLanguage(
-      getLanguages(i.state.saveUserSettingsForm.interface_language).at(0)
+    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);
   }
 
   handleDiscussionLanguageChange(val: number[]) {
@@ -1103,8 +1088,7 @@ export class Settings extends Component<any, SettingsState> {
   }
 
   handleEmailChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.email = event.target.value;
-    i.setState(i.state);
+    i.setState(s => ((s.saveUserSettingsForm.email = event.target.value), s));
   }
 
   handleBioChange(val: string) {
@@ -1128,90 +1112,100 @@ export class Settings extends Component<any, SettingsState> {
   }
 
   handleDisplayNameChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.display_name = 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 = 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 });
-    let auth = myAuth();
-    if (auth) {
-      let form: SaveUserSettings = { ...i.state.saveUserSettingsForm, auth };
-      WebSocketService.Instance.send(wsClient.saveUserSettings(form));
+    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);
     }
+
+    i.setState({ saveRes });
   }
 
-  handleChangePasswordSubmit(i: Settings, event: any) {
+  async handleChangePasswordSubmit(i: Settings, event: any) {
     event.preventDefault();
-    i.setState({ changePasswordLoading: true });
-    let auth = myAuth();
-    let pForm = i.state.changePasswordForm;
-    let new_password = pForm.new_password;
-    let new_password_verify = pForm.new_password_verify;
-    let old_password = pForm.old_password;
-    if (auth && new_password && old_password && new_password_verify) {
-      let form: ChangePassword = {
+    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,
-      };
+        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 });
-    let auth = myAuth();
-    let password = i.state.deleteAccountForm.password;
-    if (auth && password) {
-      let form: DeleteAccount = {
+  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,
-      };
-      WebSocketService.Instance.send(wsClient.deleteAccount(form));
+        auth: myAuthRequired(),
+      });
+      if (deleteAccountRes.state === "success") {
+        UserService.Instance.logout();
+        this.context.router.history.replace("/");
+      }
+
+      i.setState({ deleteAccountRes });
     }
   }
 
@@ -1219,48 +1213,20 @@ export class Settings extends Component<any, SettingsState> {
     i.ctx.setState({ currentTab: i.tab });
   }
 
-  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);
-      UserService.Instance.login(data);
-      location.reload();
-      this.setState({ saveUserSettingsLoading: false });
-      toast(i18n.t("saved"));
-      window.scrollTo(0, 0);
-    } else if (op == UserOperation.ChangePassword) {
-      let data = wsJsonToRes<LoginResponse>(msg);
-      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);
-      updatePersonBlock(data);
-      let mui = UserService.Instance.myUserInfo;
+  personBlock(res: RequestState<BlockPersonResponse>) {
+    if (res.state === "success") {
+      updatePersonBlock(res.data);
+      const mui = UserService.Instance.myUserInfo;
       if (mui) {
         this.setState({ personBlocks: mui.person_blocks });
       }
-    } else if (op == UserOperation.BlockCommunity) {
-      let data = wsJsonToRes<BlockCommunityResponse>(msg);
-      updateCommunityBlock(data);
-      let mui = UserService.Instance.myUserInfo;
+    }
+  }
+
+  communityBlock(res: RequestState<BlockCommunityResponse>) {
+    if (res.state === "success") {
+      updateCommunityBlock(res.data);
+      const mui = UserService.Instance.myUserInfo;
       if (mui) {
         this.setState({ communityBlocks: mui.community_blocks });
       }