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