]> Untitled Git - lemmy-ui.git/blob - src/shared/components/person/settings.tsx
5f149dfe2960b569f17262c9874cd3a9819175db
[lemmy-ui.git] / src / shared / components / person / settings.tsx
1 import {
2   communityToChoice,
3   fetchCommunities,
4   fetchThemeList,
5   fetchUsers,
6   myAuth,
7   myAuthRequired,
8   personToChoice,
9   setIsoData,
10   setTheme,
11   showLocal,
12   updateCommunityBlock,
13   updatePersonBlock,
14 } from "@utils/app";
15 import { capitalizeFirstLetter, debounce } from "@utils/helpers";
16 import { Choice } from "@utils/types";
17 import classNames from "classnames";
18 import { NoOptionI18nKeys } from "i18next";
19 import { Component, linkEvent } from "inferno";
20 import {
21   BlockCommunityResponse,
22   BlockPersonResponse,
23   CommunityBlockView,
24   DeleteAccountResponse,
25   GetSiteResponse,
26   ListingType,
27   LoginResponse,
28   PersonBlockView,
29   SortType,
30 } from "lemmy-js-client";
31 import { elementUrl, emDash, relTags } from "../../config";
32 import { i18n, languages } from "../../i18next";
33 import { UserService } from "../../services";
34 import { HttpService, RequestState } from "../../services/HttpService";
35 import { setupTippy } from "../../tippy";
36 import { toast } from "../../toast";
37 import { HtmlTags } from "../common/html-tags";
38 import { Icon, Spinner } from "../common/icon";
39 import { ImageUploadForm } from "../common/image-upload-form";
40 import { LanguageSelect } from "../common/language-select";
41 import { ListingTypeSelect } from "../common/listing-type-select";
42 import { MarkdownTextArea } from "../common/markdown-textarea";
43 import { SearchableSelect } from "../common/searchable-select";
44 import { SortSelect } from "../common/sort-select";
45 import Tabs from "../common/tabs";
46 import { CommunityLink } from "../community/community-link";
47 import { PersonListing } from "./person-listing";
48
49 interface SettingsState {
50   saveRes: RequestState<LoginResponse>;
51   changePasswordRes: RequestState<LoginResponse>;
52   deleteAccountRes: RequestState<DeleteAccountResponse>;
53   // TODO redo these forms
54   saveUserSettingsForm: {
55     show_nsfw?: boolean;
56     theme?: string;
57     default_sort_type?: SortType;
58     default_listing_type?: ListingType;
59     interface_language?: string;
60     avatar?: string;
61     banner?: string;
62     display_name?: string;
63     email?: string;
64     bio?: string;
65     matrix_user_id?: string;
66     show_avatars?: boolean;
67     show_scores?: boolean;
68     send_notifications_to_email?: boolean;
69     bot_account?: boolean;
70     show_bot_accounts?: boolean;
71     show_read_posts?: boolean;
72     show_new_post_notifs?: boolean;
73     discussion_languages?: number[];
74     generate_totp_2fa?: boolean;
75   };
76   changePasswordForm: {
77     new_password?: string;
78     new_password_verify?: string;
79     old_password?: string;
80   };
81   deleteAccountForm: {
82     password?: string;
83   };
84   personBlocks: PersonBlockView[];
85   communityBlocks: CommunityBlockView[];
86   currentTab: string;
87   themeList: string[];
88   deleteAccountShowConfirm: boolean;
89   siteRes: GetSiteResponse;
90   searchCommunityLoading: boolean;
91   searchCommunityOptions: Choice[];
92   searchPersonLoading: boolean;
93   searchPersonOptions: Choice[];
94 }
95
96 type FilterType = "user" | "community";
97
98 const Filter = ({
99   filterType,
100   options,
101   onChange,
102   onSearch,
103   loading,
104 }: {
105   filterType: FilterType;
106   options: Choice[];
107   onSearch: (text: string) => void;
108   onChange: (choice: Choice) => void;
109   loading: boolean;
110 }) => (
111   <div className="mb-3 row">
112     <label
113       className="col-md-4 col-form-label"
114       htmlFor={`block-${filterType}-filter`}
115     >
116       {i18n.t(`block_${filterType}` as NoOptionI18nKeys)}
117     </label>
118     <div className="col-md-8">
119       <SearchableSelect
120         id={`block-${filterType}-filter`}
121         options={[
122           { label: emDash, value: "0", disabled: true } as Choice,
123         ].concat(options)}
124         loading={loading}
125         onChange={onChange}
126         onSearch={onSearch}
127       />
128     </div>
129   </div>
130 );
131
132 export class Settings extends Component<any, SettingsState> {
133   private isoData = setIsoData(this.context);
134   state: SettingsState = {
135     saveRes: { state: "empty" },
136     deleteAccountRes: { state: "empty" },
137     changePasswordRes: { state: "empty" },
138     saveUserSettingsForm: {},
139     changePasswordForm: {},
140     deleteAccountShowConfirm: false,
141     deleteAccountForm: {},
142     personBlocks: [],
143     communityBlocks: [],
144     currentTab: "settings",
145     siteRes: this.isoData.site_res,
146     themeList: [],
147     searchCommunityLoading: false,
148     searchCommunityOptions: [],
149     searchPersonLoading: false,
150     searchPersonOptions: [],
151   };
152
153   constructor(props: any, context: any) {
154     super(props, context);
155
156     this.handleSortTypeChange = this.handleSortTypeChange.bind(this);
157     this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
158     this.handleBioChange = this.handleBioChange.bind(this);
159     this.handleDiscussionLanguageChange =
160       this.handleDiscussionLanguageChange.bind(this);
161
162     this.handleAvatarUpload = this.handleAvatarUpload.bind(this);
163     this.handleAvatarRemove = this.handleAvatarRemove.bind(this);
164
165     this.handleBannerUpload = this.handleBannerUpload.bind(this);
166     this.handleBannerRemove = this.handleBannerRemove.bind(this);
167     this.userSettings = this.userSettings.bind(this);
168     this.blockCards = this.blockCards.bind(this);
169
170     this.handleBlockPerson = this.handleBlockPerson.bind(this);
171     this.handleBlockCommunity = this.handleBlockCommunity.bind(this);
172
173     const mui = UserService.Instance.myUserInfo;
174     if (mui) {
175       const {
176         local_user: {
177           show_nsfw,
178           theme,
179           default_sort_type,
180           default_listing_type,
181           interface_language,
182           show_avatars,
183           show_bot_accounts,
184           show_scores,
185           show_read_posts,
186           show_new_post_notifs,
187           send_notifications_to_email,
188           email,
189         },
190         person: {
191           avatar,
192           banner,
193           display_name,
194           bot_account,
195           bio,
196           matrix_user_id,
197         },
198       } = mui.local_user_view;
199
200       this.state = {
201         ...this.state,
202         personBlocks: mui.person_blocks,
203         communityBlocks: mui.community_blocks,
204         saveUserSettingsForm: {
205           ...this.state.saveUserSettingsForm,
206           show_nsfw,
207           theme: theme ?? "browser",
208           default_sort_type,
209           default_listing_type,
210           interface_language,
211           discussion_languages: mui.discussion_languages,
212           avatar,
213           banner,
214           display_name,
215           show_avatars,
216           bot_account,
217           show_bot_accounts,
218           show_scores,
219           show_read_posts,
220           show_new_post_notifs,
221           email,
222           bio,
223           send_notifications_to_email,
224           matrix_user_id,
225         },
226       };
227     }
228   }
229
230   async componentDidMount() {
231     setupTippy();
232     this.setState({ themeList: await fetchThemeList() });
233   }
234
235   get documentTitle(): string {
236     return i18n.t("settings");
237   }
238
239   render() {
240     return (
241       <div className="person-settings container-lg">
242         <HtmlTags
243           title={this.documentTitle}
244           path={this.context.router.route.match.url}
245           description={this.documentTitle}
246           image={this.state.saveUserSettingsForm.avatar}
247         />
248         <Tabs
249           tabs={[
250             {
251               key: "settings",
252               label: i18n.t("settings"),
253               getNode: this.userSettings,
254             },
255             {
256               key: "blocks",
257               label: i18n.t("blocks"),
258               getNode: this.blockCards,
259             },
260           ]}
261         />
262       </div>
263     );
264   }
265
266   userSettings(isSelected) {
267     return (
268       <div
269         className={classNames("tab-pane show", {
270           active: isSelected,
271         })}
272         role="tabpanel"
273         id="settings-tab-pane"
274       >
275         <div className="row">
276           <div className="col-12 col-md-6">
277             <div className="card border-secondary mb-3">
278               <div className="card-body">{this.saveUserSettingsHtmlForm()}</div>
279             </div>
280           </div>
281           <div className="col-12 col-md-6">
282             <div className="card border-secondary mb-3">
283               <div className="card-body">{this.changePasswordHtmlForm()}</div>
284             </div>
285           </div>
286         </div>
287       </div>
288     );
289   }
290
291   blockCards(isSelected) {
292     return (
293       <div
294         className={classNames("tab-pane", {
295           active: isSelected,
296         })}
297         role="tabpanel"
298         id="blocks-tab-pane"
299       >
300         <div className="row">
301           <div className="col-12 col-md-6">
302             <div className="card border-secondary mb-3">
303               <div className="card-body">{this.blockUserCard()}</div>
304             </div>
305           </div>
306           <div className="col-12 col-md-6">
307             <div className="card border-secondary mb-3">
308               <div className="card-body">{this.blockCommunityCard()}</div>
309             </div>
310           </div>
311         </div>
312       </div>
313     );
314   }
315
316   changePasswordHtmlForm() {
317     return (
318       <>
319         <h5>{i18n.t("change_password")}</h5>
320         <form onSubmit={linkEvent(this, this.handleChangePasswordSubmit)}>
321           <div className="mb-3 row">
322             <label className="col-sm-5 col-form-label" htmlFor="user-password">
323               {i18n.t("new_password")}
324             </label>
325             <div className="col-sm-7">
326               <input
327                 type="password"
328                 id="user-password"
329                 className="form-control"
330                 value={this.state.changePasswordForm.new_password}
331                 autoComplete="new-password"
332                 maxLength={60}
333                 onInput={linkEvent(this, this.handleNewPasswordChange)}
334               />
335             </div>
336           </div>
337           <div className="mb-3 row">
338             <label
339               className="col-sm-5 col-form-label"
340               htmlFor="user-verify-password"
341             >
342               {i18n.t("verify_password")}
343             </label>
344             <div className="col-sm-7">
345               <input
346                 type="password"
347                 id="user-verify-password"
348                 className="form-control"
349                 value={this.state.changePasswordForm.new_password_verify}
350                 autoComplete="new-password"
351                 maxLength={60}
352                 onInput={linkEvent(this, this.handleNewPasswordVerifyChange)}
353               />
354             </div>
355           </div>
356           <div className="mb-3 row">
357             <label
358               className="col-sm-5 col-form-label"
359               htmlFor="user-old-password"
360             >
361               {i18n.t("old_password")}
362             </label>
363             <div className="col-sm-7">
364               <input
365                 type="password"
366                 id="user-old-password"
367                 className="form-control"
368                 value={this.state.changePasswordForm.old_password}
369                 autoComplete="new-password"
370                 maxLength={60}
371                 onInput={linkEvent(this, this.handleOldPasswordChange)}
372               />
373             </div>
374           </div>
375           <div className="input-group mb-3">
376             <button
377               type="submit"
378               className="btn d-block btn-secondary me-4 w-100"
379             >
380               {this.state.changePasswordRes.state === "loading" ? (
381                 <Spinner />
382               ) : (
383                 capitalizeFirstLetter(i18n.t("save"))
384               )}
385             </button>
386           </div>
387         </form>
388       </>
389     );
390   }
391
392   blockUserCard() {
393     const { searchPersonLoading, searchPersonOptions } = this.state;
394
395     return (
396       <div>
397         <Filter
398           filterType="user"
399           loading={searchPersonLoading}
400           onChange={this.handleBlockPerson}
401           onSearch={this.handlePersonSearch}
402           options={searchPersonOptions}
403         />
404         {this.blockedUsersList()}
405       </div>
406     );
407   }
408
409   blockedUsersList() {
410     return (
411       <>
412         <h5>{i18n.t("blocked_users")}</h5>
413         <ul className="list-unstyled mb-0">
414           {this.state.personBlocks.map(pb => (
415             <li key={pb.target.id}>
416               <span>
417                 <PersonListing person={pb.target} />
418                 <button
419                   className="btn btn-sm"
420                   onClick={linkEvent(
421                     { ctx: this, recipientId: pb.target.id },
422                     this.handleUnblockPerson
423                   )}
424                   data-tippy-content={i18n.t("unblock_user")}
425                 >
426                   <Icon icon="x" classes="icon-inline" />
427                 </button>
428               </span>
429             </li>
430           ))}
431         </ul>
432       </>
433     );
434   }
435
436   blockCommunityCard() {
437     const { searchCommunityLoading, searchCommunityOptions } = this.state;
438
439     return (
440       <div>
441         <Filter
442           filterType="community"
443           loading={searchCommunityLoading}
444           onChange={this.handleBlockCommunity}
445           onSearch={this.handleCommunitySearch}
446           options={searchCommunityOptions}
447         />
448         {this.blockedCommunitiesList()}
449       </div>
450     );
451   }
452
453   blockedCommunitiesList() {
454     return (
455       <>
456         <h5>{i18n.t("blocked_communities")}</h5>
457         <ul className="list-unstyled mb-0">
458           {this.state.communityBlocks.map(cb => (
459             <li key={cb.community.id}>
460               <span>
461                 <CommunityLink community={cb.community} />
462                 <button
463                   className="btn btn-sm"
464                   onClick={linkEvent(
465                     { ctx: this, communityId: cb.community.id },
466                     this.handleUnblockCommunity
467                   )}
468                   data-tippy-content={i18n.t("unblock_community")}
469                 >
470                   <Icon icon="x" classes="icon-inline" />
471                 </button>
472               </span>
473             </li>
474           ))}
475         </ul>
476       </>
477     );
478   }
479
480   saveUserSettingsHtmlForm() {
481     const selectedLangs = this.state.saveUserSettingsForm.discussion_languages;
482
483     return (
484       <>
485         <h5>{i18n.t("settings")}</h5>
486         <form onSubmit={linkEvent(this, this.handleSaveSettingsSubmit)}>
487           <div className="mb-3 row">
488             <label className="col-sm-3 col-form-label" htmlFor="display-name">
489               {i18n.t("display_name")}
490             </label>
491             <div className="col-sm-9">
492               <input
493                 id="display-name"
494                 type="text"
495                 className="form-control"
496                 placeholder={i18n.t("optional")}
497                 value={this.state.saveUserSettingsForm.display_name}
498                 onInput={linkEvent(this, this.handleDisplayNameChange)}
499                 pattern="^(?!@)(.+)$"
500                 minLength={3}
501               />
502             </div>
503           </div>
504           <div className="mb-3 row">
505             <label className="col-sm-3 col-form-label" htmlFor="user-bio">
506               {i18n.t("bio")}
507             </label>
508             <div className="col-sm-9">
509               <MarkdownTextArea
510                 initialContent={this.state.saveUserSettingsForm.bio}
511                 onContentChange={this.handleBioChange}
512                 maxLength={300}
513                 hideNavigationWarnings
514                 allLanguages={this.state.siteRes.all_languages}
515                 siteLanguages={this.state.siteRes.discussion_languages}
516               />
517             </div>
518           </div>
519           <div className="mb-3 row">
520             <label className="col-sm-3 col-form-label" htmlFor="user-email">
521               {i18n.t("email")}
522             </label>
523             <div className="col-sm-9">
524               <input
525                 type="email"
526                 id="user-email"
527                 className="form-control"
528                 placeholder={i18n.t("optional")}
529                 value={this.state.saveUserSettingsForm.email}
530                 onInput={linkEvent(this, this.handleEmailChange)}
531                 minLength={3}
532               />
533             </div>
534           </div>
535           <div className="mb-3 row">
536             <label className="col-sm-3 col-form-label" htmlFor="matrix-user-id">
537               <a href={elementUrl} rel={relTags}>
538                 {i18n.t("matrix_user_id")}
539               </a>
540             </label>
541             <div className="col-sm-9">
542               <input
543                 id="matrix-user-id"
544                 type="text"
545                 className="form-control"
546                 placeholder="@user:example.com"
547                 value={this.state.saveUserSettingsForm.matrix_user_id}
548                 onInput={linkEvent(this, this.handleMatrixUserIdChange)}
549                 pattern="^@[A-Za-z0-9._=-]+:[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
550               />
551             </div>
552           </div>
553           <div className="mb-3 row">
554             <label className="col-sm-3 col-form-label">
555               {i18n.t("avatar")}
556             </label>
557             <div className="col-sm-9">
558               <ImageUploadForm
559                 uploadTitle={i18n.t("upload_avatar")}
560                 imageSrc={this.state.saveUserSettingsForm.avatar}
561                 onUpload={this.handleAvatarUpload}
562                 onRemove={this.handleAvatarRemove}
563                 rounded
564               />
565             </div>
566           </div>
567           <div className="mb-3 row">
568             <label className="col-sm-3 col-form-label">
569               {i18n.t("banner")}
570             </label>
571             <div className="col-sm-9">
572               <ImageUploadForm
573                 uploadTitle={i18n.t("upload_banner")}
574                 imageSrc={this.state.saveUserSettingsForm.banner}
575                 onUpload={this.handleBannerUpload}
576                 onRemove={this.handleBannerRemove}
577               />
578             </div>
579           </div>
580           <div className="mb-3 row">
581             <label className="col-sm-3 form-label" htmlFor="user-language">
582               {i18n.t("interface_language")}
583             </label>
584             <div className="col-sm-9">
585               <select
586                 id="user-language"
587                 value={this.state.saveUserSettingsForm.interface_language}
588                 onChange={linkEvent(this, this.handleInterfaceLangChange)}
589                 className="form-select d-inline-block w-auto"
590               >
591                 <option disabled aria-hidden="true">
592                   {i18n.t("interface_language")}
593                 </option>
594                 <option value="browser">{i18n.t("browser_default")}</option>
595                 <option disabled aria-hidden="true">
596                   â”€â”€
597                 </option>
598                 {languages
599                   .sort((a, b) => a.code.localeCompare(b.code))
600                   .map(lang => (
601                     <option key={lang.code} value={lang.code}>
602                       {lang.name}
603                     </option>
604                   ))}
605               </select>
606             </div>
607           </div>
608           <LanguageSelect
609             allLanguages={this.state.siteRes.all_languages}
610             siteLanguages={this.state.siteRes.discussion_languages}
611             selectedLanguageIds={selectedLangs}
612             multiple={true}
613             showLanguageWarning={true}
614             showSite
615             onChange={this.handleDiscussionLanguageChange}
616           />
617           <div className="mb-3 row">
618             <label className="col-sm-3 col-form-label" htmlFor="user-theme">
619               {i18n.t("theme")}
620             </label>
621             <div className="col-sm-9">
622               <select
623                 id="user-theme"
624                 value={this.state.saveUserSettingsForm.theme}
625                 onChange={linkEvent(this, this.handleThemeChange)}
626                 className="form-select d-inline-block w-auto"
627               >
628                 <option disabled aria-hidden="true">
629                   {i18n.t("theme")}
630                 </option>
631                 <option value="browser">{i18n.t("browser_default")}</option>
632                 {this.state.themeList.map(theme => (
633                   <option key={theme} value={theme}>
634                     {theme}
635                   </option>
636                 ))}
637               </select>
638             </div>
639           </div>
640           <form className="mb-3 row">
641             <label className="col-sm-3 col-form-label">{i18n.t("type")}</label>
642             <div className="col-sm-9">
643               <ListingTypeSelect
644                 type_={
645                   this.state.saveUserSettingsForm.default_listing_type ??
646                   "Local"
647                 }
648                 showLocal={showLocal(this.isoData)}
649                 showSubscribed
650                 onChange={this.handleListingTypeChange}
651               />
652             </div>
653           </form>
654           <form className="mb-3 row">
655             <label className="col-sm-3 col-form-label">
656               {i18n.t("sort_type")}
657             </label>
658             <div className="col-sm-9">
659               <SortSelect
660                 sort={
661                   this.state.saveUserSettingsForm.default_sort_type ?? "Active"
662                 }
663                 onChange={this.handleSortTypeChange}
664               />
665             </div>
666           </form>
667           <div className="input-group mb-3">
668             <div className="form-check">
669               <input
670                 className="form-check-input"
671                 id="user-show-nsfw"
672                 type="checkbox"
673                 checked={this.state.saveUserSettingsForm.show_nsfw}
674                 onChange={linkEvent(this, this.handleShowNsfwChange)}
675               />
676               <label className="form-check-label" htmlFor="user-show-nsfw">
677                 {i18n.t("show_nsfw")}
678               </label>
679             </div>
680           </div>
681           <div className="input-group mb-3">
682             <div className="form-check">
683               <input
684                 className="form-check-input"
685                 id="user-show-scores"
686                 type="checkbox"
687                 checked={this.state.saveUserSettingsForm.show_scores}
688                 onChange={linkEvent(this, this.handleShowScoresChange)}
689               />
690               <label className="form-check-label" htmlFor="user-show-scores">
691                 {i18n.t("show_scores")}
692               </label>
693             </div>
694           </div>
695           <div className="input-group mb-3">
696             <div className="form-check">
697               <input
698                 className="form-check-input"
699                 id="user-show-avatars"
700                 type="checkbox"
701                 checked={this.state.saveUserSettingsForm.show_avatars}
702                 onChange={linkEvent(this, this.handleShowAvatarsChange)}
703               />
704               <label className="form-check-label" htmlFor="user-show-avatars">
705                 {i18n.t("show_avatars")}
706               </label>
707             </div>
708           </div>
709           <div className="input-group mb-3">
710             <div className="form-check">
711               <input
712                 className="form-check-input"
713                 id="user-bot-account"
714                 type="checkbox"
715                 checked={this.state.saveUserSettingsForm.bot_account}
716                 onChange={linkEvent(this, this.handleBotAccount)}
717               />
718               <label className="form-check-label" htmlFor="user-bot-account">
719                 {i18n.t("bot_account")}
720               </label>
721             </div>
722           </div>
723           <div className="input-group mb-3">
724             <div className="form-check">
725               <input
726                 className="form-check-input"
727                 id="user-show-bot-accounts"
728                 type="checkbox"
729                 checked={this.state.saveUserSettingsForm.show_bot_accounts}
730                 onChange={linkEvent(this, this.handleShowBotAccounts)}
731               />
732               <label
733                 className="form-check-label"
734                 htmlFor="user-show-bot-accounts"
735               >
736                 {i18n.t("show_bot_accounts")}
737               </label>
738             </div>
739           </div>
740           <div className="input-group mb-3">
741             <div className="form-check">
742               <input
743                 className="form-check-input"
744                 id="user-show-read-posts"
745                 type="checkbox"
746                 checked={this.state.saveUserSettingsForm.show_read_posts}
747                 onChange={linkEvent(this, this.handleReadPosts)}
748               />
749               <label
750                 className="form-check-label"
751                 htmlFor="user-show-read-posts"
752               >
753                 {i18n.t("show_read_posts")}
754               </label>
755             </div>
756           </div>
757           <div className="input-group mb-3">
758             <div className="form-check">
759               <input
760                 className="form-check-input"
761                 id="user-show-new-post-notifs"
762                 type="checkbox"
763                 checked={this.state.saveUserSettingsForm.show_new_post_notifs}
764                 onChange={linkEvent(this, this.handleShowNewPostNotifs)}
765               />
766               <label
767                 className="form-check-label"
768                 htmlFor="user-show-new-post-notifs"
769               >
770                 {i18n.t("show_new_post_notifs")}
771               </label>
772             </div>
773           </div>
774           <div className="input-group mb-3">
775             <div className="form-check">
776               <input
777                 className="form-check-input"
778                 id="user-send-notifications-to-email"
779                 type="checkbox"
780                 disabled={!this.state.saveUserSettingsForm.email}
781                 checked={
782                   this.state.saveUserSettingsForm.send_notifications_to_email
783                 }
784                 onChange={linkEvent(
785                   this,
786                   this.handleSendNotificationsToEmailChange
787                 )}
788               />
789               <label
790                 className="form-check-label"
791                 htmlFor="user-send-notifications-to-email"
792               >
793                 {i18n.t("send_notifications_to_email")}
794               </label>
795             </div>
796           </div>
797           {this.totpSection()}
798           <div className="input-group mb-3">
799             <button type="submit" className="btn d-block btn-secondary me-4">
800               {this.state.saveRes.state === "loading" ? (
801                 <Spinner />
802               ) : (
803                 capitalizeFirstLetter(i18n.t("save"))
804               )}
805             </button>
806           </div>
807           <hr />
808           <div className="input-group mb-3">
809             <button
810               className="btn d-block btn-danger"
811               onClick={linkEvent(
812                 this,
813                 this.handleDeleteAccountShowConfirmToggle
814               )}
815             >
816               {i18n.t("delete_account")}
817             </button>
818             {this.state.deleteAccountShowConfirm && (
819               <>
820                 <div className="my-2 alert alert-danger" role="alert">
821                   {i18n.t("delete_account_confirm")}
822                 </div>
823                 <input
824                   type="password"
825                   value={this.state.deleteAccountForm.password}
826                   autoComplete="new-password"
827                   maxLength={60}
828                   onInput={linkEvent(
829                     this,
830                     this.handleDeleteAccountPasswordChange
831                   )}
832                   className="form-control my-2"
833                 />
834                 <button
835                   className="btn btn-danger me-4"
836                   disabled={!this.state.deleteAccountForm.password}
837                   onClick={linkEvent(this, this.handleDeleteAccount)}
838                 >
839                   {this.state.deleteAccountRes.state === "loading" ? (
840                     <Spinner />
841                   ) : (
842                     capitalizeFirstLetter(i18n.t("delete"))
843                   )}
844                 </button>
845                 <button
846                   className="btn btn-secondary"
847                   onClick={linkEvent(
848                     this,
849                     this.handleDeleteAccountShowConfirmToggle
850                   )}
851                 >
852                   {i18n.t("cancel")}
853                 </button>
854               </>
855             )}
856           </div>
857         </form>
858       </>
859     );
860   }
861
862   totpSection() {
863     const totpUrl =
864       UserService.Instance.myUserInfo?.local_user_view.local_user.totp_2fa_url;
865
866     return (
867       <>
868         {!totpUrl && (
869           <div className="input-group mb-3">
870             <div className="form-check">
871               <input
872                 className="form-check-input"
873                 id="user-generate-totp"
874                 type="checkbox"
875                 checked={this.state.saveUserSettingsForm.generate_totp_2fa}
876                 onChange={linkEvent(this, this.handleGenerateTotp)}
877               />
878               <label className="form-check-label" htmlFor="user-generate-totp">
879                 {i18n.t("set_up_two_factor")}
880               </label>
881             </div>
882           </div>
883         )}
884
885         {totpUrl && (
886           <>
887             <div>
888               <a className="btn btn-secondary mb-2" href={totpUrl}>
889                 {i18n.t("two_factor_link")}
890               </a>
891             </div>
892             <div className="input-group mb-3">
893               <div className="form-check">
894                 <input
895                   className="form-check-input"
896                   id="user-remove-totp"
897                   type="checkbox"
898                   checked={
899                     this.state.saveUserSettingsForm.generate_totp_2fa == false
900                   }
901                   onChange={linkEvent(this, this.handleRemoveTotp)}
902                 />
903                 <label className="form-check-label" htmlFor="user-remove-totp">
904                   {i18n.t("remove_two_factor")}
905                 </label>
906               </div>
907             </div>
908           </>
909         )}
910       </>
911     );
912   }
913
914   handlePersonSearch = debounce(async (text: string) => {
915     this.setState({ searchPersonLoading: true });
916
917     const searchPersonOptions: Choice[] = [];
918
919     if (text.length > 0) {
920       searchPersonOptions.push(...(await fetchUsers(text)).map(personToChoice));
921     }
922
923     this.setState({
924       searchPersonLoading: false,
925       searchPersonOptions,
926     });
927   });
928
929   handleCommunitySearch = debounce(async (text: string) => {
930     this.setState({ searchCommunityLoading: true });
931
932     const searchCommunityOptions: Choice[] = [];
933
934     if (text.length > 0) {
935       searchCommunityOptions.push(
936         ...(await fetchCommunities(text)).map(communityToChoice)
937       );
938     }
939
940     this.setState({
941       searchCommunityLoading: false,
942       searchCommunityOptions,
943     });
944   });
945
946   async handleBlockPerson({ value }: Choice) {
947     if (value !== "0") {
948       const res = await HttpService.client.blockPerson({
949         person_id: Number(value),
950         block: true,
951         auth: myAuthRequired(),
952       });
953       this.personBlock(res);
954     }
955   }
956
957   async handleUnblockPerson({
958     ctx,
959     recipientId,
960   }: {
961     ctx: Settings;
962     recipientId: number;
963   }) {
964     const res = await HttpService.client.blockPerson({
965       person_id: recipientId,
966       block: false,
967       auth: myAuthRequired(),
968     });
969     ctx.personBlock(res);
970   }
971
972   async handleBlockCommunity({ value }: Choice) {
973     if (value !== "0") {
974       const res = await HttpService.client.blockCommunity({
975         community_id: Number(value),
976         block: true,
977         auth: myAuthRequired(),
978       });
979       this.communityBlock(res);
980     }
981   }
982
983   async handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
984     const auth = myAuth();
985     if (auth) {
986       const res = await HttpService.client.blockCommunity({
987         community_id: i.communityId,
988         block: false,
989         auth: myAuthRequired(),
990       });
991       i.ctx.communityBlock(res);
992     }
993   }
994
995   handleShowNsfwChange(i: Settings, event: any) {
996     i.setState(
997       s => ((s.saveUserSettingsForm.show_nsfw = event.target.checked), s)
998     );
999   }
1000
1001   handleShowAvatarsChange(i: Settings, event: any) {
1002     const mui = UserService.Instance.myUserInfo;
1003     if (mui) {
1004       mui.local_user_view.local_user.show_avatars = event.target.checked;
1005     }
1006     i.setState(
1007       s => ((s.saveUserSettingsForm.show_avatars = event.target.checked), s)
1008     );
1009   }
1010
1011   handleBotAccount(i: Settings, event: any) {
1012     i.setState(
1013       s => ((s.saveUserSettingsForm.bot_account = event.target.checked), s)
1014     );
1015   }
1016
1017   handleShowBotAccounts(i: Settings, event: any) {
1018     i.setState(
1019       s => (
1020         (s.saveUserSettingsForm.show_bot_accounts = event.target.checked), s
1021       )
1022     );
1023   }
1024
1025   handleReadPosts(i: Settings, event: any) {
1026     i.setState(
1027       s => ((s.saveUserSettingsForm.show_read_posts = event.target.checked), s)
1028     );
1029   }
1030
1031   handleShowNewPostNotifs(i: Settings, event: any) {
1032     i.setState(
1033       s => (
1034         (s.saveUserSettingsForm.show_new_post_notifs = event.target.checked), s
1035       )
1036     );
1037   }
1038
1039   handleShowScoresChange(i: Settings, event: any) {
1040     const mui = UserService.Instance.myUserInfo;
1041     if (mui) {
1042       mui.local_user_view.local_user.show_scores = event.target.checked;
1043     }
1044     i.setState(
1045       s => ((s.saveUserSettingsForm.show_scores = event.target.checked), s)
1046     );
1047   }
1048
1049   handleGenerateTotp(i: Settings, event: any) {
1050     // Coerce false to undefined here, so it won't generate it.
1051     const checked: boolean | undefined = event.target.checked || undefined;
1052     if (checked) {
1053       toast(i18n.t("two_factor_setup_instructions"));
1054     }
1055     i.setState(s => ((s.saveUserSettingsForm.generate_totp_2fa = checked), s));
1056   }
1057
1058   handleRemoveTotp(i: Settings, event: any) {
1059     // Coerce true to undefined here, so it won't generate it.
1060     const checked: boolean | undefined = !event.target.checked && undefined;
1061     i.setState(s => ((s.saveUserSettingsForm.generate_totp_2fa = checked), s));
1062   }
1063
1064   handleSendNotificationsToEmailChange(i: Settings, event: any) {
1065     i.setState(
1066       s => (
1067         (s.saveUserSettingsForm.send_notifications_to_email =
1068           event.target.checked),
1069         s
1070       )
1071     );
1072   }
1073
1074   handleThemeChange(i: Settings, event: any) {
1075     i.setState(s => ((s.saveUserSettingsForm.theme = event.target.value), s));
1076     setTheme(event.target.value, true);
1077   }
1078
1079   handleInterfaceLangChange(i: Settings, event: any) {
1080     const newLang = event.target.value ?? "browser";
1081     i18n.changeLanguage(newLang === "browser" ? navigator.languages : newLang);
1082
1083     i.setState(
1084       s => ((s.saveUserSettingsForm.interface_language = event.target.value), s)
1085     );
1086   }
1087
1088   handleDiscussionLanguageChange(val: number[]) {
1089     this.setState(
1090       s => ((s.saveUserSettingsForm.discussion_languages = val), s)
1091     );
1092   }
1093
1094   handleSortTypeChange(val: SortType) {
1095     this.setState(s => ((s.saveUserSettingsForm.default_sort_type = val), s));
1096   }
1097
1098   handleListingTypeChange(val: ListingType) {
1099     this.setState(
1100       s => ((s.saveUserSettingsForm.default_listing_type = val), s)
1101     );
1102   }
1103
1104   handleEmailChange(i: Settings, event: any) {
1105     i.setState(s => ((s.saveUserSettingsForm.email = event.target.value), s));
1106   }
1107
1108   handleBioChange(val: string) {
1109     this.setState(s => ((s.saveUserSettingsForm.bio = val), s));
1110   }
1111
1112   handleAvatarUpload(url: string) {
1113     this.setState(s => ((s.saveUserSettingsForm.avatar = url), s));
1114   }
1115
1116   handleAvatarRemove() {
1117     this.setState(s => ((s.saveUserSettingsForm.avatar = ""), s));
1118   }
1119
1120   handleBannerUpload(url: string) {
1121     this.setState(s => ((s.saveUserSettingsForm.banner = url), s));
1122   }
1123
1124   handleBannerRemove() {
1125     this.setState(s => ((s.saveUserSettingsForm.banner = ""), s));
1126   }
1127
1128   handleDisplayNameChange(i: Settings, event: any) {
1129     i.setState(
1130       s => ((s.saveUserSettingsForm.display_name = event.target.value), s)
1131     );
1132   }
1133
1134   handleMatrixUserIdChange(i: Settings, event: any) {
1135     i.setState(
1136       s => ((s.saveUserSettingsForm.matrix_user_id = event.target.value), s)
1137     );
1138   }
1139
1140   handleNewPasswordChange(i: Settings, event: any) {
1141     const newPass: string | undefined =
1142       event.target.value == "" ? undefined : event.target.value;
1143     i.setState(s => ((s.changePasswordForm.new_password = newPass), s));
1144   }
1145
1146   handleNewPasswordVerifyChange(i: Settings, event: any) {
1147     const newPassVerify: string | undefined =
1148       event.target.value == "" ? undefined : event.target.value;
1149     i.setState(
1150       s => ((s.changePasswordForm.new_password_verify = newPassVerify), s)
1151     );
1152   }
1153
1154   handleOldPasswordChange(i: Settings, event: any) {
1155     const oldPass: string | undefined =
1156       event.target.value == "" ? undefined : event.target.value;
1157     i.setState(s => ((s.changePasswordForm.old_password = oldPass), s));
1158   }
1159
1160   async handleSaveSettingsSubmit(i: Settings, event: any) {
1161     event.preventDefault();
1162     i.setState({ saveRes: { state: "loading" } });
1163
1164     const saveRes = await HttpService.client.saveUserSettings({
1165       ...i.state.saveUserSettingsForm,
1166       auth: myAuthRequired(),
1167     });
1168     if (saveRes.state === "success") {
1169       UserService.Instance.login(saveRes.data);
1170       location.reload();
1171       toast(i18n.t("saved"));
1172       window.scrollTo(0, 0);
1173     }
1174
1175     i.setState({ saveRes });
1176   }
1177
1178   async handleChangePasswordSubmit(i: Settings, event: any) {
1179     event.preventDefault();
1180     const { new_password, new_password_verify, old_password } =
1181       i.state.changePasswordForm;
1182
1183     if (new_password && old_password && new_password_verify) {
1184       i.setState({ changePasswordRes: { state: "loading" } });
1185       const changePasswordRes = await HttpService.client.changePassword({
1186         new_password,
1187         new_password_verify,
1188         old_password,
1189         auth: myAuthRequired(),
1190       });
1191       if (changePasswordRes.state === "success") {
1192         UserService.Instance.login(changePasswordRes.data);
1193         window.scrollTo(0, 0);
1194         toast(i18n.t("password_changed"));
1195       }
1196
1197       i.setState({ changePasswordRes });
1198     }
1199   }
1200
1201   handleDeleteAccountShowConfirmToggle(i: Settings) {
1202     i.setState({ deleteAccountShowConfirm: !i.state.deleteAccountShowConfirm });
1203   }
1204
1205   handleDeleteAccountPasswordChange(i: Settings, event: any) {
1206     i.setState(s => ((s.deleteAccountForm.password = event.target.value), s));
1207   }
1208
1209   async handleDeleteAccount(i: Settings) {
1210     const password = i.state.deleteAccountForm.password;
1211     if (password) {
1212       i.setState({ deleteAccountRes: { state: "loading" } });
1213       const deleteAccountRes = await HttpService.client.deleteAccount({
1214         password,
1215         auth: myAuthRequired(),
1216       });
1217       if (deleteAccountRes.state === "success") {
1218         UserService.Instance.logout();
1219         this.context.router.history.replace("/");
1220       }
1221
1222       i.setState({ deleteAccountRes });
1223     }
1224   }
1225
1226   handleSwitchTab(i: { ctx: Settings; tab: string }) {
1227     i.ctx.setState({ currentTab: i.tab });
1228   }
1229
1230   personBlock(res: RequestState<BlockPersonResponse>) {
1231     if (res.state === "success") {
1232       updatePersonBlock(res.data);
1233       const mui = UserService.Instance.myUserInfo;
1234       if (mui) {
1235         this.setState({ personBlocks: mui.person_blocks });
1236       }
1237     }
1238   }
1239
1240   communityBlock(res: RequestState<BlockCommunityResponse>) {
1241     if (res.state === "success") {
1242       updateCommunityBlock(res.data);
1243       const mui = UserService.Instance.myUserInfo;
1244       if (mui) {
1245         this.setState({ communityBlocks: mui.community_blocks });
1246       }
1247     }
1248   }
1249 }