]> Untitled Git - lemmy-ui.git/blobdiff - src/shared/components/person/settings.tsx
Merge branch 'main' into rate-limiting-tab
[lemmy-ui.git] / src / shared / components / person / settings.tsx
index 9c1e06ad0087e0e14cba14a38d2afc76157f8591..a335d21cc13d0d4da24fd03af468057b2590d215 100644 (file)
@@ -1,4 +1,4 @@
-import { None, Option, Some } from "@sniptt/monads";
+import { NoOptionI18nKeys } from "i18next";
 import { Component, linkEvent } from "inferno";
 import {
   BlockCommunity,
@@ -7,16 +7,13 @@ import {
   BlockPersonResponse,
   ChangePassword,
   CommunityBlockView,
-  CommunityView,
   DeleteAccount,
   GetSiteResponse,
   ListingType,
   LoginResponse,
   PersonBlockView,
-  PersonViewSafe,
   SaveUserSettings,
   SortType,
-  toUndefined,
   UserOperation,
   wsJsonToRes,
   wsUserOp,
@@ -25,20 +22,18 @@ import { Subscription } from "rxjs";
 import { i18n, languages } from "../../i18next";
 import { UserService, WebSocketService } from "../../services";
 import {
-  auth,
+  Choice,
   capitalizeFirstLetter,
-  choicesConfig,
-  communitySelectName,
   communityToChoice,
   debounce,
   elementUrl,
+  emDash,
   enableNsfw,
   fetchCommunities,
   fetchThemeList,
   fetchUsers,
   getLanguages,
-  isBrowser,
-  personSelectName,
+  myAuth,
   personToChoice,
   relTags,
   setIsoData,
@@ -57,24 +52,46 @@ 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;
+  // 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;
@@ -82,63 +99,73 @@ interface SettingsState {
   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="form-group 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,
-      interface_language: None,
-      discussion_languages: 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,
-    }),
+  private subscription?: Subscription;
+  state: SettingsState = {
+    saveUserSettingsForm: {},
+    changePasswordForm: {},
     saveUserSettingsLoading: false,
     changePasswordLoading: false,
     deleteAccountLoading: false,
     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);
@@ -150,40 +177,64 @@ 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);
 
-    if (UserService.Instance.myUserInfo.isSome()) {
-      let mui = UserService.Instance.myUserInfo.unwrap();
-      let luv = mui.local_user_view;
+    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.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),
-          interface_language: Some(luv.local_user.interface_language),
-          discussion_languages: Some(mui.discussion_languages.map(l => l.id)),
-          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,
         },
       };
     }
@@ -195,7 +246,7 @@ export class Settings extends Component<any, SettingsState> {
   }
 
   componentWillUnmount() {
-    this.subscription.unsubscribe();
+    this.subscription?.unsubscribe();
   }
 
   get documentTitle(): string {
@@ -205,44 +256,26 @@ export class Settings extends Component<any, SettingsState> {
   render() {
     return (
       <div className="container-lg">
-        <>
-          <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()}
-        </>
+        <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>
     );
   }
@@ -355,9 +388,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>
     );
@@ -390,40 +431,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>
     );
@@ -456,33 +475,6 @@ 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() {
     let selectedLangs = this.state.saveUserSettingsForm.discussion_languages;
 
@@ -500,9 +492,7 @@ export class Settings extends Component<any, SettingsState> {
                 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}
@@ -516,13 +506,11 @@ export class Settings extends Component<any, SettingsState> {
             <div className="col-sm-9">
               <MarkdownTextArea
                 initialContent={this.state.saveUserSettingsForm.bio}
-                initialLanguageId={None}
                 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>
@@ -536,7 +524,7 @@ 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}
               />
@@ -554,9 +542,7 @@ export class Settings extends Component<any, SettingsState> {
                 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,}$"
               />
@@ -592,9 +578,7 @@ export class Settings extends Component<any, SettingsState> {
             <div className="col-sm-9">
               <select
                 id="user-language"
-                value={toUndefined(
-                  this.state.saveUserSettingsForm.interface_language
-                )}
+                value={this.state.saveUserSettingsForm.interface_language}
                 onChange={linkEvent(this, this.handleInterfaceLangChange)}
                 className="custom-select w-auto"
               >
@@ -617,8 +601,10 @@ export class Settings extends Component<any, SettingsState> {
           </div>
           <LanguageSelect
             allLanguages={this.state.siteRes.all_languages}
+            siteLanguages={this.state.siteRes.discussion_languages}
             selectedLanguageIds={selectedLangs}
             multiple={true}
+            showSite
             onChange={this.handleDiscussionLanguageChange}
           />
           <div className="form-group row">
@@ -628,7 +614,7 @@ export class Settings extends Component<any, SettingsState> {
             <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"
               >
@@ -649,11 +635,8 @@ export class Settings extends Component<any, SettingsState> {
             <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
@@ -666,11 +649,7 @@ export class Settings extends Component<any, SettingsState> {
             <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}
               />
@@ -683,9 +662,7 @@ export class Settings extends Component<any, SettingsState> {
                   className="form-check-input"
                   id="user-show-nsfw"
                   type="checkbox"
-                  checked={toUndefined(
-                    this.state.saveUserSettingsForm.show_nsfw
-                  )}
+                  checked={this.state.saveUserSettingsForm.show_nsfw}
                   onChange={linkEvent(this, this.handleShowNsfwChange)}
                 />
                 <label className="form-check-label" htmlFor="user-show-nsfw">
@@ -700,9 +677,7 @@ export class Settings extends Component<any, SettingsState> {
                 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">
@@ -716,9 +691,7 @@ export class Settings extends Component<any, SettingsState> {
                 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">
@@ -732,9 +705,7 @@ export class Settings extends Component<any, SettingsState> {
                 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">
@@ -748,9 +719,7 @@ export class Settings extends Component<any, SettingsState> {
                 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
@@ -767,9 +736,7 @@ export class Settings extends Component<any, SettingsState> {
                 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
@@ -786,9 +753,7 @@ export class Settings extends Component<any, SettingsState> {
                 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
@@ -806,9 +771,9 @@ export class Settings extends Component<any, SettingsState> {
                 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
@@ -822,6 +787,7 @@ export class Settings extends Component<any, SettingsState> {
               </label>
             </div>
           </div>
+          {this.totpSection()}
           <div className="form-group">
             <button type="submit" className="btn btn-block btn-secondary mr-4">
               {this.state.saveUserSettingsLoading ? (
@@ -886,102 +852,125 @@ 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() {
+    let totpUrl =
+      UserService.Instance.myUserInfo?.local_user_view.local_user.totp_2fa_url;
+
+    return (
+      <>
+        {!totpUrl && (
+          <div className="form-group">
+            <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="form-group">
+              <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)).users.map(personToChoice)
+      );
+    }
+
+    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)).communities.map(communityToChoice)
+      );
     }
-  }
 
-  handleBlockPerson(personId: number) {
-    if (personId != 0) {
-      let blockUserForm = new BlockPerson({
-        person_id: personId,
+    this.setState({
+      searchCommunityLoading: false,
+      searchCommunityOptions,
+    });
+  });
+
+  handleBlockPerson({ value }: Choice) {
+    const auth = myAuth();
+    if (auth && value !== "0") {
+      const blockUserForm: BlockPerson = {
+        person_id: Number(value),
         block: true,
-        auth: auth().unwrap(),
-      });
+        auth,
+      };
+
       WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
     }
   }
 
   handleUnblockPerson(i: { ctx: Settings; recipientId: number }) {
-    let blockUserForm = new BlockPerson({
-      person_id: i.recipientId,
-      block: false,
-      auth: auth().unwrap(),
-    });
-    WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
+    const auth = myAuth();
+    if (auth) {
+      const blockUserForm: BlockPerson = {
+        person_id: i.recipientId,
+        block: false,
+        auth,
+      };
+      WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
+    }
   }
 
-  handleBlockCommunity(community_id: number) {
-    if (community_id != 0) {
-      let blockCommunityForm = new BlockCommunity({
-        community_id,
+  handleBlockCommunity({ value }: Choice) {
+    const auth = myAuth();
+    if (auth && value !== "0") {
+      const blockCommunityForm: BlockCommunity = {
+        community_id: Number(value),
         block: true,
-        auth: auth().unwrap(),
-      });
+        auth,
+      };
       WebSocketService.Instance.send(
         wsClient.blockCommunity(blockCommunityForm)
       );
@@ -989,142 +978,147 @@ export class Settings extends Component<any, SettingsState> {
   }
 
   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));
+    const auth = myAuth();
+    if (auth) {
+      const blockCommunityForm: BlockCommunity = {
+        community_id: i.communityId,
+        block: false,
+        auth,
+      };
+      WebSocketService.Instance.send(
+        wsClient.blockCommunity(blockCommunityForm)
+      );
+    }
   }
 
   handleShowNsfwChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_nsfw = Some(event.target.checked);
+    i.state.saveUserSettingsForm.show_nsfw = event.target.checked;
     i.setState(i.state);
   }
 
   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.state.saveUserSettingsForm.show_avatars = event.target.checked;
+    let mui = UserService.Instance.myUserInfo;
+    if (mui) {
+      mui.local_user_view.local_user.show_avatars = event.target.checked;
+    }
     i.setState(i.state);
   }
 
   handleBotAccount(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.bot_account = Some(event.target.checked);
+    i.state.saveUserSettingsForm.bot_account = event.target.checked;
     i.setState(i.state);
   }
 
   handleShowBotAccounts(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_bot_accounts = Some(event.target.checked);
+    i.state.saveUserSettingsForm.show_bot_accounts = event.target.checked;
     i.setState(i.state);
   }
 
   handleReadPosts(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_read_posts = Some(event.target.checked);
+    i.state.saveUserSettingsForm.show_read_posts = event.target.checked;
     i.setState(i.state);
   }
 
   handleShowNewPostNotifs(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.show_new_post_notifs = Some(
-      event.target.checked
-    );
+    i.state.saveUserSettingsForm.show_new_post_notifs = event.target.checked;
     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.state.saveUserSettingsForm.show_scores = event.target.checked;
+    let mui = UserService.Instance.myUserInfo;
+    if (mui) {
+      mui.local_user_view.local_user.show_scores = event.target.checked;
+    }
+    i.setState(i.state);
+  }
+
+  handleGenerateTotp(i: Settings, event: any) {
+    // Coerce false to undefined here, so it won't generate it.
+    let 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);
+  }
+
+  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);
   }
 
   handleSendNotificationsToEmailChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.send_notifications_to_email = Some(
-      event.target.checked
-    );
+    i.state.saveUserSettingsForm.send_notifications_to_email =
+      event.target.checked;
     i.setState(i.state);
   }
 
   handleThemeChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.theme = Some(event.target.value);
+    i.state.saveUserSettingsForm.theme = event.target.value;
     setTheme(event.target.value, true);
     i.setState(i.state);
   }
 
   handleInterfaceLangChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.interface_language = Some(event.target.value);
+    i.state.saveUserSettingsForm.interface_language = event.target.value;
     i18n.changeLanguage(
-      getLanguages(i.state.saveUserSettingsForm.interface_language.unwrap())[0]
+      getLanguages(i.state.saveUserSettingsForm.interface_language).at(0)
     );
     i.setState(i.state);
   }
 
   handleDiscussionLanguageChange(val: number[]) {
     this.setState(
-      s => ((s.saveUserSettingsForm.discussion_languages = Some(val)), s)
+      s => ((s.saveUserSettingsForm.discussion_languages = val), s)
     );
   }
 
   handleSortTypeChange(val: SortType) {
-    this.setState(
-      s => (
-        (s.saveUserSettingsForm.default_sort_type = Some(
-          Object.keys(SortType).indexOf(val)
-        )),
-        s
-      )
-    );
+    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.state.saveUserSettingsForm.email = event.target.value;
     i.setState(i.state);
   }
 
   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.state.saveUserSettingsForm.display_name = event.target.value;
     i.setState(i.state);
   }
 
   handleMatrixUserIdChange(i: Settings, event: any) {
-    i.state.saveUserSettingsForm.matrix_user_id = Some(event.target.value);
+    i.state.saveUserSettingsForm.matrix_user_id = event.target.value;
     i.setState(i.state);
   }
 
@@ -1155,20 +1149,31 @@ export class Settings extends Component<any, SettingsState> {
   handleSaveSettingsSubmit(i: Settings, event: any) {
     event.preventDefault();
     i.setState({ saveUserSettingsLoading: true });
-    i.setState(s => ((s.saveUserSettingsForm.auth = auth().unwrap()), s));
-
-    let form = new SaveUserSettings({ ...i.state.saveUserSettingsForm });
-    WebSocketService.Instance.send(wsClient.saveUserSettings(form));
+    let auth = myAuth();
+    if (auth) {
+      let form: SaveUserSettings = { ...i.state.saveUserSettingsForm, auth };
+      WebSocketService.Instance.send(wsClient.saveUserSettings(form));
+    }
   }
 
   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 });
+    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 = {
+        new_password,
+        new_password_verify,
+        old_password,
+        auth,
+      };
 
-    WebSocketService.Instance.send(wsClient.changePassword(form));
+      WebSocketService.Instance.send(wsClient.changePassword(form));
+    }
   }
 
   handleDeleteAccountShowConfirmToggle(i: Settings, event: any) {
@@ -1184,20 +1189,19 @@ export class Settings extends Component<any, SettingsState> {
   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 });
-
-    WebSocketService.Instance.send(wsClient.deleteAccount(form));
+    let auth = myAuth();
+    let password = i.state.deleteAccountForm.password;
+    if (auth && password) {
+      let form: DeleteAccount = {
+        password,
+        auth,
+      };
+      WebSocketService.Instance.send(wsClient.deleteAccount(form));
+    }
   }
 
   handleSwitchTab(i: { ctx: Settings; tab: string }) {
     i.ctx.setState({ currentTab: i.tab });
-
-    if (i.ctx.state.currentTab == "blocks") {
-      i.ctx.setupBlockPersonChoices();
-      i.ctx.setupBlockCommunityChoices();
-    }
   }
 
   parseMessage(msg: any) {
@@ -1212,14 +1216,11 @@ export class Settings extends Component<any, SettingsState> {
       toast(i18n.t(msg.error), "danger");
       return;
     } else if (op == UserOperation.SaveUserSettings) {
-      let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
-      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, LoginResponse);
+      let data = wsJsonToRes<LoginResponse>(msg);
       UserService.Instance.login(data);
       this.setState({ changePasswordLoading: false });
       window.scrollTo(0, 0);
@@ -1232,20 +1233,19 @@ export class Settings extends Component<any, SettingsState> {
       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,
-      });
+      let data = wsJsonToRes<BlockPersonResponse>(msg);
+      updatePersonBlock(data);
+      let mui = UserService.Instance.myUserInfo;
+      if (mui) {
+        this.setState({ personBlocks: mui.person_blocks });
+      }
     } else if (op == UserOperation.BlockCommunity) {
-      let data = wsJsonToRes<BlockCommunityResponse>(
-        msg,
-        BlockCommunityResponse
-      );
-      updateCommunityBlock(data).match({
-        some: blocks => this.setState({ communityBlocks: blocks }),
-        none: void 0,
-      });
+      let data = wsJsonToRes<BlockCommunityResponse>(msg);
+      updateCommunityBlock(data);
+      let mui = UserService.Instance.myUserInfo;
+      if (mui) {
+        this.setState({ communityBlocks: mui.community_blocks });
+      }
     }
   }
 }