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