]> Untitled Git - lemmy-ui.git/blobdiff - src/shared/components/person/settings.tsx
Add open links in new tab (#2032)
[lemmy-ui.git] / src / shared / components / person / settings.tsx
index 9acba57abb95dccaa5a995703737d8b75081c445..659c77c2081e9f675fbd05b2b9ae1d72f9a7da7e 100644 (file)
@@ -1,4 +1,19 @@
-import { debounce } from "@utils/helpers";
+import {
+  communityToChoice,
+  fetchCommunities,
+  fetchThemeList,
+  fetchUsers,
+  myAuth,
+  myAuthRequired,
+  personToChoice,
+  setIsoData,
+  setTheme,
+  showLocal,
+  updateCommunityBlock,
+  updatePersonBlock,
+} from "@utils/app";
+import { capitalizeFirstLetter, debounce } from "@utils/helpers";
+import { Choice } from "@utils/types";
 import classNames from "classnames";
 import { NoOptionI18nKeys } from "i18next";
 import { Component, linkEvent } from "inferno";
@@ -13,36 +28,19 @@ import {
   PersonBlockView,
   SortType,
 } from "lemmy-js-client";
-import { i18n, languages } from "../../i18next";
+import { elementUrl, emDash, relTags } from "../../config";
 import { UserService } from "../../services";
 import { HttpService, RequestState } from "../../services/HttpService";
-import {
-  Choice,
-  capitalizeFirstLetter,
-  communityToChoice,
-  elementUrl,
-  emDash,
-  fetchCommunities,
-  fetchThemeList,
-  fetchUsers,
-  myAuth,
-  myAuthRequired,
-  personToChoice,
-  relTags,
-  setIsoData,
-  setTheme,
-  setupTippy,
-  showLocal,
-  toast,
-  updateCommunityBlock,
-  updatePersonBlock,
-} from "../../utils";
+import { I18NextService, languages } from "../../services/I18NextService";
+import { setupTippy } from "../../tippy";
+import { toast } from "../../toast";
 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 PasswordInput from "../common/password-input";
 import { SearchableSelect } from "../common/searchable-select";
 import { SortSelect } from "../common/sort-select";
 import Tabs from "../common/tabs";
@@ -75,6 +73,7 @@ interface SettingsState {
     show_new_post_notifs?: boolean;
     discussion_languages?: number[];
     generate_totp_2fa?: boolean;
+    open_links_in_new_tab?: boolean;
   };
   changePasswordForm: {
     new_password?: string;
@@ -116,7 +115,7 @@ const Filter = ({
       className="col-md-4 col-form-label"
       htmlFor={`block-${filterType}-filter`}
     >
-      {i18n.t(`block_${filterType}` as NoOptionI18nKeys)}
+      {I18NextService.i18n.t(`block_${filterType}` as NoOptionI18nKeys)}
     </label>
     <div className="col-md-8">
       <SearchableSelect
@@ -236,7 +235,7 @@ export class Settings extends Component<any, SettingsState> {
   }
 
   get documentTitle(): string {
-    return i18n.t("settings");
+    return I18NextService.i18n.t("settings");
   }
 
   render() {
@@ -252,12 +251,12 @@ export class Settings extends Component<any, SettingsState> {
           tabs={[
             {
               key: "settings",
-              label: i18n.t("settings"),
+              label: I18NextService.i18n.t("settings"),
               getNode: this.userSettings,
             },
             {
               key: "blocks",
-              label: i18n.t("blocks"),
+              label: I18NextService.i18n.t("blocks"),
               getNode: this.blockCards,
             },
           ]}
@@ -266,7 +265,7 @@ export class Settings extends Component<any, SettingsState> {
     );
   }
 
-  userSettings(isSelected) {
+  userSettings(isSelected: boolean) {
     return (
       <div
         className={classNames("tab-pane show", {
@@ -291,7 +290,7 @@ export class Settings extends Component<any, SettingsState> {
     );
   }
 
-  blockCards(isSelected) {
+  blockCards(isSelected: boolean) {
     return (
       <div
         className={classNames("tab-pane", {
@@ -319,61 +318,34 @@ export class Settings extends Component<any, SettingsState> {
   changePasswordHtmlForm() {
     return (
       <>
-        <h5>{i18n.t("change_password")}</h5>
+        <h2 className="h5">{I18NextService.i18n.t("change_password")}</h2>
         <form onSubmit={linkEvent(this, this.handleChangePasswordSubmit)}>
-          <div className="mb-3 row">
-            <label className="col-sm-5 col-form-label" htmlFor="user-password">
-              {i18n.t("new_password")}
-            </label>
-            <div className="col-sm-7">
-              <input
-                type="password"
-                id="user-password"
-                className="form-control"
-                value={this.state.changePasswordForm.new_password}
-                autoComplete="new-password"
-                maxLength={60}
-                onInput={linkEvent(this, this.handleNewPasswordChange)}
-              />
-            </div>
+          <div className="mb-3">
+            <PasswordInput
+              id="new-password"
+              value={this.state.changePasswordForm.new_password}
+              onInput={linkEvent(this, this.handleNewPasswordChange)}
+              showStrength
+              label={I18NextService.i18n.t("new_password")}
+              isNew
+            />
           </div>
-          <div className="mb-3 row">
-            <label
-              className="col-sm-5 col-form-label"
-              htmlFor="user-verify-password"
-            >
-              {i18n.t("verify_password")}
-            </label>
-            <div className="col-sm-7">
-              <input
-                type="password"
-                id="user-verify-password"
-                className="form-control"
-                value={this.state.changePasswordForm.new_password_verify}
-                autoComplete="new-password"
-                maxLength={60}
-                onInput={linkEvent(this, this.handleNewPasswordVerifyChange)}
-              />
-            </div>
+          <div className="mb-3">
+            <PasswordInput
+              id="verify-new-password"
+              value={this.state.changePasswordForm.new_password_verify}
+              onInput={linkEvent(this, this.handleNewPasswordVerifyChange)}
+              label={I18NextService.i18n.t("verify_password")}
+              isNew
+            />
           </div>
-          <div className="mb-3 row">
-            <label
-              className="col-sm-5 col-form-label"
-              htmlFor="user-old-password"
-            >
-              {i18n.t("old_password")}
-            </label>
-            <div className="col-sm-7">
-              <input
-                type="password"
-                id="user-old-password"
-                className="form-control"
-                value={this.state.changePasswordForm.old_password}
-                autoComplete="new-password"
-                maxLength={60}
-                onInput={linkEvent(this, this.handleOldPasswordChange)}
-              />
-            </div>
+          <div className="mb-3">
+            <PasswordInput
+              id="user-old-password"
+              value={this.state.changePasswordForm.old_password}
+              onInput={linkEvent(this, this.handleOldPasswordChange)}
+              label={I18NextService.i18n.t("old_password")}
+            />
           </div>
           <div className="input-group mb-3">
             <button
@@ -383,7 +355,7 @@ export class Settings extends Component<any, SettingsState> {
               {this.state.changePasswordRes.state === "loading" ? (
                 <Spinner />
               ) : (
-                capitalizeFirstLetter(i18n.t("save"))
+                capitalizeFirstLetter(I18NextService.i18n.t("save"))
               )}
             </button>
           </div>
@@ -412,7 +384,7 @@ export class Settings extends Component<any, SettingsState> {
   blockedUsersList() {
     return (
       <>
-        <h5>{i18n.t("blocked_users")}</h5>
+        <h2 className="h5">{I18NextService.i18n.t("blocked_users")}</h2>
         <ul className="list-unstyled mb-0">
           {this.state.personBlocks.map(pb => (
             <li key={pb.target.id}>
@@ -422,9 +394,9 @@ export class Settings extends Component<any, SettingsState> {
                   className="btn btn-sm"
                   onClick={linkEvent(
                     { ctx: this, recipientId: pb.target.id },
-                    this.handleUnblockPerson
+                    this.handleUnblockPerson,
                   )}
-                  data-tippy-content={i18n.t("unblock_user")}
+                  data-tippy-content={I18NextService.i18n.t("unblock_user")}
                 >
                   <Icon icon="x" classes="icon-inline" />
                 </button>
@@ -456,7 +428,7 @@ export class Settings extends Component<any, SettingsState> {
   blockedCommunitiesList() {
     return (
       <>
-        <h5>{i18n.t("blocked_communities")}</h5>
+        <h2 className="h5">{I18NextService.i18n.t("blocked_communities")}</h2>
         <ul className="list-unstyled mb-0">
           {this.state.communityBlocks.map(cb => (
             <li key={cb.community.id}>
@@ -466,9 +438,11 @@ export class Settings extends Component<any, SettingsState> {
                   className="btn btn-sm"
                   onClick={linkEvent(
                     { ctx: this, communityId: cb.community.id },
-                    this.handleUnblockCommunity
+                    this.handleUnblockCommunity,
+                  )}
+                  data-tippy-content={I18NextService.i18n.t(
+                    "unblock_community",
                   )}
-                  data-tippy-content={i18n.t("unblock_community")}
                 >
                   <Icon icon="x" classes="icon-inline" />
                 </button>
@@ -485,18 +459,18 @@ export class Settings extends Component<any, SettingsState> {
 
     return (
       <>
-        <h5>{i18n.t("settings")}</h5>
+        <h2 className="h5">{I18NextService.i18n.t("settings")}</h2>
         <form onSubmit={linkEvent(this, this.handleSaveSettingsSubmit)}>
           <div className="mb-3 row">
             <label className="col-sm-3 col-form-label" htmlFor="display-name">
-              {i18n.t("display_name")}
+              {I18NextService.i18n.t("display_name")}
             </label>
             <div className="col-sm-9">
               <input
                 id="display-name"
                 type="text"
                 className="form-control"
-                placeholder={i18n.t("optional")}
+                placeholder={I18NextService.i18n.t("optional")}
                 value={this.state.saveUserSettingsForm.display_name}
                 onInput={linkEvent(this, this.handleDisplayNameChange)}
                 pattern="^(?!@)(.+)$"
@@ -506,7 +480,7 @@ export class Settings extends Component<any, SettingsState> {
           </div>
           <div className="mb-3 row">
             <label className="col-sm-3 col-form-label" htmlFor="user-bio">
-              {i18n.t("bio")}
+              {I18NextService.i18n.t("bio")}
             </label>
             <div className="col-sm-9">
               <MarkdownTextArea
@@ -521,14 +495,14 @@ export class Settings extends Component<any, SettingsState> {
           </div>
           <div className="mb-3 row">
             <label className="col-sm-3 col-form-label" htmlFor="user-email">
-              {i18n.t("email")}
+              {I18NextService.i18n.t("email")}
             </label>
             <div className="col-sm-9">
               <input
                 type="email"
                 id="user-email"
                 className="form-control"
-                placeholder={i18n.t("optional")}
+                placeholder={I18NextService.i18n.t("optional")}
                 value={this.state.saveUserSettingsForm.email}
                 onInput={linkEvent(this, this.handleEmailChange)}
                 minLength={3}
@@ -538,7 +512,7 @@ export class Settings extends Component<any, SettingsState> {
           <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")}
+                {I18NextService.i18n.t("matrix_user_id")}
               </a>
             </label>
             <div className="col-sm-9">
@@ -555,11 +529,11 @@ export class Settings extends Component<any, SettingsState> {
           </div>
           <div className="mb-3 row">
             <label className="col-sm-3 col-form-label">
-              {i18n.t("avatar")}
+              {I18NextService.i18n.t("avatar")}
             </label>
             <div className="col-sm-9">
               <ImageUploadForm
-                uploadTitle={i18n.t("upload_avatar")}
+                uploadTitle={I18NextService.i18n.t("upload_avatar")}
                 imageSrc={this.state.saveUserSettingsForm.avatar}
                 onUpload={this.handleAvatarUpload}
                 onRemove={this.handleAvatarRemove}
@@ -569,11 +543,11 @@ export class Settings extends Component<any, SettingsState> {
           </div>
           <div className="mb-3 row">
             <label className="col-sm-3 col-form-label">
-              {i18n.t("banner")}
+              {I18NextService.i18n.t("banner")}
             </label>
             <div className="col-sm-9">
               <ImageUploadForm
-                uploadTitle={i18n.t("upload_banner")}
+                uploadTitle={I18NextService.i18n.t("upload_banner")}
                 imageSrc={this.state.saveUserSettingsForm.banner}
                 onUpload={this.handleBannerUpload}
                 onRemove={this.handleBannerRemove}
@@ -582,7 +556,7 @@ export class Settings extends Component<any, SettingsState> {
           </div>
           <div className="mb-3 row">
             <label className="col-sm-3 form-label" htmlFor="user-language">
-              {i18n.t("interface_language")}
+              {I18NextService.i18n.t("interface_language")}
             </label>
             <div className="col-sm-9">
               <select
@@ -592,9 +566,11 @@ export class Settings extends Component<any, SettingsState> {
                 className="form-select d-inline-block w-auto"
               >
                 <option disabled aria-hidden="true">
-                  {i18n.t("interface_language")}
+                  {I18NextService.i18n.t("interface_language")}
+                </option>
+                <option value="browser">
+                  {I18NextService.i18n.t("browser_default")}
                 </option>
-                <option value="browser">{i18n.t("browser_default")}</option>
                 <option disabled aria-hidden="true">
                   ──
                 </option>
@@ -614,12 +590,13 @@ export class Settings extends Component<any, SettingsState> {
             selectedLanguageIds={selectedLangs}
             multiple={true}
             showLanguageWarning={true}
+            showAll={true}
             showSite
             onChange={this.handleDiscussionLanguageChange}
           />
           <div className="mb-3 row">
             <label className="col-sm-3 col-form-label" htmlFor="user-theme">
-              {i18n.t("theme")}
+              {I18NextService.i18n.t("theme")}
             </label>
             <div className="col-sm-9">
               <select
@@ -629,9 +606,14 @@ export class Settings extends Component<any, SettingsState> {
                 className="form-select d-inline-block w-auto"
               >
                 <option disabled aria-hidden="true">
-                  {i18n.t("theme")}
+                  {I18NextService.i18n.t("theme")}
+                </option>
+                <option value="browser">
+                  {I18NextService.i18n.t("browser_default")}
+                </option>
+                <option value="browser-compact">
+                  {I18NextService.i18n.t("browser_default_compact")}
                 </option>
-                <option value="browser">{i18n.t("browser_default")}</option>
                 {this.state.themeList.map(theme => (
                   <option key={theme} value={theme}>
                     {theme}
@@ -641,7 +623,9 @@ export class Settings extends Component<any, SettingsState> {
             </div>
           </div>
           <form className="mb-3 row">
-            <label className="col-sm-3 col-form-label">{i18n.t("type")}</label>
+            <label className="col-sm-3 col-form-label">
+              {I18NextService.i18n.t("type")}
+            </label>
             <div className="col-sm-9">
               <ListingTypeSelect
                 type_={
@@ -656,7 +640,7 @@ export class Settings extends Component<any, SettingsState> {
           </form>
           <form className="mb-3 row">
             <label className="col-sm-3 col-form-label">
-              {i18n.t("sort_type")}
+              {I18NextService.i18n.t("sort_type")}
             </label>
             <div className="col-sm-9">
               <SortSelect
@@ -677,7 +661,7 @@ export class Settings extends Component<any, SettingsState> {
                 onChange={linkEvent(this, this.handleShowNsfwChange)}
               />
               <label className="form-check-label" htmlFor="user-show-nsfw">
-                {i18n.t("show_nsfw")}
+                {I18NextService.i18n.t("show_nsfw")}
               </label>
             </div>
           </div>
@@ -691,7 +675,7 @@ export class Settings extends Component<any, SettingsState> {
                 onChange={linkEvent(this, this.handleShowScoresChange)}
               />
               <label className="form-check-label" htmlFor="user-show-scores">
-                {i18n.t("show_scores")}
+                {I18NextService.i18n.t("show_scores")}
               </label>
             </div>
           </div>
@@ -705,7 +689,7 @@ export class Settings extends Component<any, SettingsState> {
                 onChange={linkEvent(this, this.handleShowAvatarsChange)}
               />
               <label className="form-check-label" htmlFor="user-show-avatars">
-                {i18n.t("show_avatars")}
+                {I18NextService.i18n.t("show_avatars")}
               </label>
             </div>
           </div>
@@ -719,7 +703,7 @@ export class Settings extends Component<any, SettingsState> {
                 onChange={linkEvent(this, this.handleBotAccount)}
               />
               <label className="form-check-label" htmlFor="user-bot-account">
-                {i18n.t("bot_account")}
+                {I18NextService.i18n.t("bot_account")}
               </label>
             </div>
           </div>
@@ -736,7 +720,7 @@ export class Settings extends Component<any, SettingsState> {
                 className="form-check-label"
                 htmlFor="user-show-bot-accounts"
               >
-                {i18n.t("show_bot_accounts")}
+                {I18NextService.i18n.t("show_bot_accounts")}
               </label>
             </div>
           </div>
@@ -753,7 +737,7 @@ export class Settings extends Component<any, SettingsState> {
                 className="form-check-label"
                 htmlFor="user-show-read-posts"
               >
-                {i18n.t("show_read_posts")}
+                {I18NextService.i18n.t("show_read_posts")}
               </label>
             </div>
           </div>
@@ -770,7 +754,7 @@ export class Settings extends Component<any, SettingsState> {
                 className="form-check-label"
                 htmlFor="user-show-new-post-notifs"
               >
-                {i18n.t("show_new_post_notifs")}
+                {I18NextService.i18n.t("show_new_post_notifs")}
               </label>
             </div>
           </div>
@@ -786,14 +770,31 @@ export class Settings extends Component<any, SettingsState> {
                 }
                 onChange={linkEvent(
                   this,
-                  this.handleSendNotificationsToEmailChange
+                  this.handleSendNotificationsToEmailChange,
                 )}
               />
               <label
                 className="form-check-label"
                 htmlFor="user-send-notifications-to-email"
               >
-                {i18n.t("send_notifications_to_email")}
+                {I18NextService.i18n.t("send_notifications_to_email")}
+              </label>
+            </div>
+          </div>
+          <div className="input-group mb-3">
+            <div className="form-check">
+              <input
+                className="form-check-input"
+                id="user-open-links-in-new-tab"
+                type="checkbox"
+                checked={this.state.saveUserSettingsForm.open_links_in_new_tab}
+                onChange={linkEvent(this, this.handleOpenInNewTab)}
+              />
+              <label
+                className="form-check-label"
+                htmlFor="user-open-links-in-new-tab"
+              >
+                {I18NextService.i18n.t("open_links_in_new_tab")}
               </label>
             </div>
           </div>
@@ -803,60 +804,67 @@ export class Settings extends Component<any, SettingsState> {
               {this.state.saveRes.state === "loading" ? (
                 <Spinner />
               ) : (
-                capitalizeFirstLetter(i18n.t("save"))
+                capitalizeFirstLetter(I18NextService.i18n.t("save"))
               )}
             </button>
           </div>
           <hr />
-          <div className="input-group mb-3">
+          <form
+            className="mb-3"
+            onSubmit={linkEvent(this, this.handleDeleteAccount)}
+          >
             <button
+              type="button"
               className="btn d-block btn-danger"
               onClick={linkEvent(
                 this,
-                this.handleDeleteAccountShowConfirmToggle
+                this.handleDeleteAccountShowConfirmToggle,
               )}
             >
-              {i18n.t("delete_account")}
+              {I18NextService.i18n.t("delete_account")}
             </button>
             {this.state.deleteAccountShowConfirm && (
               <>
-                <div className="my-2 alert alert-danger" role="alert">
-                  {i18n.t("delete_account_confirm")}
-                </div>
-                <input
-                  type="password"
+                <label
+                  className="my-2 alert alert-danger d-block"
+                  role="alert"
+                  htmlFor="password-delete-account"
+                >
+                  {I18NextService.i18n.t("delete_account_confirm")}
+                </label>
+                <PasswordInput
+                  id="password-delete-account"
                   value={this.state.deleteAccountForm.password}
-                  autoComplete="new-password"
-                  maxLength={60}
                   onInput={linkEvent(
                     this,
-                    this.handleDeleteAccountPasswordChange
+                    this.handleDeleteAccountPasswordChange,
                   )}
-                  className="form-control my-2"
+                  className="my-2"
                 />
                 <button
+                  type="submit"
                   className="btn btn-danger me-4"
                   disabled={!this.state.deleteAccountForm.password}
-                  onClick={linkEvent(this, this.handleDeleteAccount)}
                 >
                   {this.state.deleteAccountRes.state === "loading" ? (
                     <Spinner />
                   ) : (
-                    capitalizeFirstLetter(i18n.t("delete"))
+                    capitalizeFirstLetter(I18NextService.i18n.t("delete"))
                   )}
                 </button>
                 <button
                   className="btn btn-secondary"
+                  type="button"
                   onClick={linkEvent(
                     this,
-                    this.handleDeleteAccountShowConfirmToggle
+                    this.handleDeleteAccountShowConfirmToggle,
                   )}
                 >
-                  {i18n.t("cancel")}
+                  {I18NextService.i18n.t("cancel")}
                 </button>
               </>
             )}
-          </div>
+          </form>
         </form>
       </>
     );
@@ -879,7 +887,7 @@ export class Settings extends Component<any, SettingsState> {
                 onChange={linkEvent(this, this.handleGenerateTotp)}
               />
               <label className="form-check-label" htmlFor="user-generate-totp">
-                {i18n.t("set_up_two_factor")}
+                {I18NextService.i18n.t("set_up_two_factor")}
               </label>
             </div>
           </div>
@@ -889,7 +897,7 @@ export class Settings extends Component<any, SettingsState> {
           <>
             <div>
               <a className="btn btn-secondary mb-2" href={totpUrl}>
-                {i18n.t("two_factor_link")}
+                {I18NextService.i18n.t("two_factor_link")}
               </a>
             </div>
             <div className="input-group mb-3">
@@ -899,12 +907,12 @@ export class Settings extends Component<any, SettingsState> {
                   id="user-remove-totp"
                   type="checkbox"
                   checked={
-                    this.state.saveUserSettingsForm.generate_totp_2fa == false
+                    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")}
+                  {I18NextService.i18n.t("remove_two_factor")}
                 </label>
               </div>
             </div>
@@ -936,7 +944,7 @@ export class Settings extends Component<any, SettingsState> {
 
     if (text.length > 0) {
       searchCommunityOptions.push(
-        ...(await fetchCommunities(text)).map(communityToChoice)
+        ...(await fetchCommunities(text)).map(communityToChoice),
       );
     }
 
@@ -997,7 +1005,7 @@ export class Settings extends Component<any, SettingsState> {
 
   handleShowNsfwChange(i: Settings, event: any) {
     i.setState(
-      s => ((s.saveUserSettingsForm.show_nsfw = event.target.checked), s)
+      s => ((s.saveUserSettingsForm.show_nsfw = event.target.checked), s),
     );
   }
 
@@ -1007,13 +1015,13 @@ export class Settings extends Component<any, SettingsState> {
       mui.local_user_view.local_user.show_avatars = event.target.checked;
     }
     i.setState(
-      s => ((s.saveUserSettingsForm.show_avatars = event.target.checked), s)
+      s => ((s.saveUserSettingsForm.show_avatars = event.target.checked), s),
     );
   }
 
   handleBotAccount(i: Settings, event: any) {
     i.setState(
-      s => ((s.saveUserSettingsForm.bot_account = event.target.checked), s)
+      s => ((s.saveUserSettingsForm.bot_account = event.target.checked), s),
     );
   }
 
@@ -1021,13 +1029,13 @@ export class Settings extends Component<any, SettingsState> {
     i.setState(
       s => (
         (s.saveUserSettingsForm.show_bot_accounts = event.target.checked), s
-      )
+      ),
     );
   }
 
   handleReadPosts(i: Settings, event: any) {
     i.setState(
-      s => ((s.saveUserSettingsForm.show_read_posts = event.target.checked), s)
+      s => ((s.saveUserSettingsForm.show_read_posts = event.target.checked), s),
     );
   }
 
@@ -1035,7 +1043,15 @@ export class Settings extends Component<any, SettingsState> {
     i.setState(
       s => (
         (s.saveUserSettingsForm.show_new_post_notifs = event.target.checked), s
-      )
+      ),
+    );
+  }
+
+  handleOpenInNewTab(i: Settings, event: any) {
+    i.setState(
+      s => (
+        (s.saveUserSettingsForm.open_links_in_new_tab = event.target.checked), s
+      ),
     );
   }
 
@@ -1045,7 +1061,7 @@ export class Settings extends Component<any, SettingsState> {
       mui.local_user_view.local_user.show_scores = event.target.checked;
     }
     i.setState(
-      s => ((s.saveUserSettingsForm.show_scores = event.target.checked), s)
+      s => ((s.saveUserSettingsForm.show_scores = event.target.checked), s),
     );
   }
 
@@ -1053,7 +1069,7 @@ export class Settings extends Component<any, SettingsState> {
     // 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"));
+      toast(I18NextService.i18n.t("two_factor_setup_instructions"));
     }
     i.setState(s => ((s.saveUserSettingsForm.generate_totp_2fa = checked), s));
   }
@@ -1070,7 +1086,7 @@ export class Settings extends Component<any, SettingsState> {
         (s.saveUserSettingsForm.send_notifications_to_email =
           event.target.checked),
         s
-      )
+      ),
     );
   }
 
@@ -1081,16 +1097,20 @@ export class Settings extends Component<any, SettingsState> {
 
   handleInterfaceLangChange(i: Settings, event: any) {
     const newLang = event.target.value ?? "browser";
-    i18n.changeLanguage(newLang === "browser" ? navigator.languages : newLang);
+    I18NextService.i18n.changeLanguage(
+      newLang === "browser" ? navigator.languages : newLang,
+    );
 
     i.setState(
-      s => ((s.saveUserSettingsForm.interface_language = event.target.value), s)
+      s => (
+        (s.saveUserSettingsForm.interface_language = event.target.value), s
+      ),
     );
   }
 
   handleDiscussionLanguageChange(val: number[]) {
     this.setState(
-      s => ((s.saveUserSettingsForm.discussion_languages = val), s)
+      s => ((s.saveUserSettingsForm.discussion_languages = val), s),
     );
   }
 
@@ -1100,7 +1120,7 @@ export class Settings extends Component<any, SettingsState> {
 
   handleListingTypeChange(val: ListingType) {
     this.setState(
-      s => ((s.saveUserSettingsForm.default_listing_type = val), s)
+      s => ((s.saveUserSettingsForm.default_listing_type = val), s),
     );
   }
 
@@ -1130,33 +1150,33 @@ export class Settings extends Component<any, SettingsState> {
 
   handleDisplayNameChange(i: Settings, event: any) {
     i.setState(
-      s => ((s.saveUserSettingsForm.display_name = event.target.value), s)
+      s => ((s.saveUserSettingsForm.display_name = event.target.value), s),
     );
   }
 
   handleMatrixUserIdChange(i: Settings, event: any) {
     i.setState(
-      s => ((s.saveUserSettingsForm.matrix_user_id = event.target.value), s)
+      s => ((s.saveUserSettingsForm.matrix_user_id = event.target.value), s),
     );
   }
 
   handleNewPasswordChange(i: Settings, event: any) {
     const newPass: string | undefined =
-      event.target.value == "" ? undefined : event.target.value;
+      event.target.value === "" ? undefined : event.target.value;
     i.setState(s => ((s.changePasswordForm.new_password = newPass), s));
   }
 
   handleNewPasswordVerifyChange(i: Settings, event: any) {
     const newPassVerify: string | undefined =
-      event.target.value == "" ? undefined : event.target.value;
+      event.target.value === "" ? undefined : event.target.value;
     i.setState(
-      s => ((s.changePasswordForm.new_password_verify = newPassVerify), s)
+      s => ((s.changePasswordForm.new_password_verify = newPassVerify), s),
     );
   }
 
   handleOldPasswordChange(i: Settings, event: any) {
     const oldPass: string | undefined =
-      event.target.value == "" ? undefined : event.target.value;
+      event.target.value === "" ? undefined : event.target.value;
     i.setState(s => ((s.changePasswordForm.old_password = oldPass), s));
   }
 
@@ -1168,10 +1188,13 @@ export class Settings extends Component<any, SettingsState> {
       ...i.state.saveUserSettingsForm,
       auth: myAuthRequired(),
     });
+
     if (saveRes.state === "success") {
-      UserService.Instance.login(saveRes.data);
-      location.reload();
-      toast(i18n.t("saved"));
+      UserService.Instance.login({
+        res: saveRes.data,
+        showToast: false,
+      });
+      toast(I18NextService.i18n.t("saved"));
       window.scrollTo(0, 0);
     }
 
@@ -1192,9 +1215,12 @@ export class Settings extends Component<any, SettingsState> {
         auth: myAuthRequired(),
       });
       if (changePasswordRes.state === "success") {
-        UserService.Instance.login(changePasswordRes.data);
+        UserService.Instance.login({
+          res: changePasswordRes.data,
+          showToast: false,
+        });
         window.scrollTo(0, 0);
-        toast(i18n.t("password_changed"));
+        toast(I18NextService.i18n.t("password_changed"));
       }
 
       i.setState({ changePasswordRes });
@@ -1209,7 +1235,8 @@ export class Settings extends Component<any, SettingsState> {
     i.setState(s => ((s.deleteAccountForm.password = event.target.value), s));
   }
 
-  async handleDeleteAccount(i: Settings) {
+  async handleDeleteAccount(i: Settings, event: Event) {
+    event.preventDefault();
     const password = i.state.deleteAccountForm.password;
     if (password) {
       i.setState({ deleteAccountRes: { state: "loading" } });