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