]> Untitled Git - lemmy-ui.git/blob - src/shared/components/person/settings.tsx
Merge branch 'main' into rate-limiting-tab
[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     let 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             showSite
608             onChange={this.handleDiscussionLanguageChange}
609           />
610           <div className="form-group row">
611             <label className="col-sm-3" htmlFor="user-theme">
612               {i18n.t("theme")}
613             </label>
614             <div className="col-sm-9">
615               <select
616                 id="user-theme"
617                 value={this.state.saveUserSettingsForm.theme}
618                 onChange={linkEvent(this, this.handleThemeChange)}
619                 className="custom-select w-auto"
620               >
621                 <option disabled aria-hidden="true">
622                   {i18n.t("theme")}
623                 </option>
624                 <option value="browser">{i18n.t("browser_default")}</option>
625                 {this.state.themeList.map(theme => (
626                   <option key={theme} value={theme}>
627                     {theme}
628                   </option>
629                 ))}
630               </select>
631             </div>
632           </div>
633           <form className="form-group row">
634             <label className="col-sm-3">{i18n.t("type")}</label>
635             <div className="col-sm-9">
636               <ListingTypeSelect
637                 type_={
638                   this.state.saveUserSettingsForm.default_listing_type ??
639                   "Local"
640                 }
641                 showLocal={showLocal(this.isoData)}
642                 showSubscribed
643                 onChange={this.handleListingTypeChange}
644               />
645             </div>
646           </form>
647           <form className="form-group row">
648             <label className="col-sm-3">{i18n.t("sort_type")}</label>
649             <div className="col-sm-9">
650               <SortSelect
651                 sort={
652                   this.state.saveUserSettingsForm.default_sort_type ?? "Active"
653                 }
654                 onChange={this.handleSortTypeChange}
655               />
656             </div>
657           </form>
658           {enableNsfw(this.state.siteRes) && (
659             <div className="form-group">
660               <div className="form-check">
661                 <input
662                   className="form-check-input"
663                   id="user-show-nsfw"
664                   type="checkbox"
665                   checked={this.state.saveUserSettingsForm.show_nsfw}
666                   onChange={linkEvent(this, this.handleShowNsfwChange)}
667                 />
668                 <label className="form-check-label" htmlFor="user-show-nsfw">
669                   {i18n.t("show_nsfw")}
670                 </label>
671               </div>
672             </div>
673           )}
674           <div className="form-group">
675             <div className="form-check">
676               <input
677                 className="form-check-input"
678                 id="user-show-scores"
679                 type="checkbox"
680                 checked={this.state.saveUserSettingsForm.show_scores}
681                 onChange={linkEvent(this, this.handleShowScoresChange)}
682               />
683               <label className="form-check-label" htmlFor="user-show-scores">
684                 {i18n.t("show_scores")}
685               </label>
686             </div>
687           </div>
688           <div className="form-group">
689             <div className="form-check">
690               <input
691                 className="form-check-input"
692                 id="user-show-avatars"
693                 type="checkbox"
694                 checked={this.state.saveUserSettingsForm.show_avatars}
695                 onChange={linkEvent(this, this.handleShowAvatarsChange)}
696               />
697               <label className="form-check-label" htmlFor="user-show-avatars">
698                 {i18n.t("show_avatars")}
699               </label>
700             </div>
701           </div>
702           <div className="form-group">
703             <div className="form-check">
704               <input
705                 className="form-check-input"
706                 id="user-bot-account"
707                 type="checkbox"
708                 checked={this.state.saveUserSettingsForm.bot_account}
709                 onChange={linkEvent(this, this.handleBotAccount)}
710               />
711               <label className="form-check-label" htmlFor="user-bot-account">
712                 {i18n.t("bot_account")}
713               </label>
714             </div>
715           </div>
716           <div className="form-group">
717             <div className="form-check">
718               <input
719                 className="form-check-input"
720                 id="user-show-bot-accounts"
721                 type="checkbox"
722                 checked={this.state.saveUserSettingsForm.show_bot_accounts}
723                 onChange={linkEvent(this, this.handleShowBotAccounts)}
724               />
725               <label
726                 className="form-check-label"
727                 htmlFor="user-show-bot-accounts"
728               >
729                 {i18n.t("show_bot_accounts")}
730               </label>
731             </div>
732           </div>
733           <div className="form-group">
734             <div className="form-check">
735               <input
736                 className="form-check-input"
737                 id="user-show-read-posts"
738                 type="checkbox"
739                 checked={this.state.saveUserSettingsForm.show_read_posts}
740                 onChange={linkEvent(this, this.handleReadPosts)}
741               />
742               <label
743                 className="form-check-label"
744                 htmlFor="user-show-read-posts"
745               >
746                 {i18n.t("show_read_posts")}
747               </label>
748             </div>
749           </div>
750           <div className="form-group">
751             <div className="form-check">
752               <input
753                 className="form-check-input"
754                 id="user-show-new-post-notifs"
755                 type="checkbox"
756                 checked={this.state.saveUserSettingsForm.show_new_post_notifs}
757                 onChange={linkEvent(this, this.handleShowNewPostNotifs)}
758               />
759               <label
760                 className="form-check-label"
761                 htmlFor="user-show-new-post-notifs"
762               >
763                 {i18n.t("show_new_post_notifs")}
764               </label>
765             </div>
766           </div>
767           <div className="form-group">
768             <div className="form-check">
769               <input
770                 className="form-check-input"
771                 id="user-send-notifications-to-email"
772                 type="checkbox"
773                 disabled={!this.state.saveUserSettingsForm.email}
774                 checked={
775                   this.state.saveUserSettingsForm.send_notifications_to_email
776                 }
777                 onChange={linkEvent(
778                   this,
779                   this.handleSendNotificationsToEmailChange
780                 )}
781               />
782               <label
783                 className="form-check-label"
784                 htmlFor="user-send-notifications-to-email"
785               >
786                 {i18n.t("send_notifications_to_email")}
787               </label>
788             </div>
789           </div>
790           {this.totpSection()}
791           <div className="form-group">
792             <button type="submit" className="btn btn-block btn-secondary mr-4">
793               {this.state.saveUserSettingsLoading ? (
794                 <Spinner />
795               ) : (
796                 capitalizeFirstLetter(i18n.t("save"))
797               )}
798             </button>
799           </div>
800           <hr />
801           <div className="form-group">
802             <button
803               className="btn btn-block btn-danger"
804               onClick={linkEvent(
805                 this,
806                 this.handleDeleteAccountShowConfirmToggle
807               )}
808             >
809               {i18n.t("delete_account")}
810             </button>
811             {this.state.deleteAccountShowConfirm && (
812               <>
813                 <div className="my-2 alert alert-danger" role="alert">
814                   {i18n.t("delete_account_confirm")}
815                 </div>
816                 <input
817                   type="password"
818                   value={this.state.deleteAccountForm.password}
819                   autoComplete="new-password"
820                   maxLength={60}
821                   onInput={linkEvent(
822                     this,
823                     this.handleDeleteAccountPasswordChange
824                   )}
825                   className="form-control my-2"
826                 />
827                 <button
828                   className="btn btn-danger mr-4"
829                   disabled={!this.state.deleteAccountForm.password}
830                   onClick={linkEvent(this, this.handleDeleteAccount)}
831                 >
832                   {this.state.deleteAccountLoading ? (
833                     <Spinner />
834                   ) : (
835                     capitalizeFirstLetter(i18n.t("delete"))
836                   )}
837                 </button>
838                 <button
839                   className="btn btn-secondary"
840                   onClick={linkEvent(
841                     this,
842                     this.handleDeleteAccountShowConfirmToggle
843                   )}
844                 >
845                   {i18n.t("cancel")}
846                 </button>
847               </>
848             )}
849           </div>
850         </form>
851       </>
852     );
853   }
854
855   totpSection() {
856     let totpUrl =
857       UserService.Instance.myUserInfo?.local_user_view.local_user.totp_2fa_url;
858
859     return (
860       <>
861         {!totpUrl && (
862           <div className="form-group">
863             <div className="form-check">
864               <input
865                 className="form-check-input"
866                 id="user-generate-totp"
867                 type="checkbox"
868                 checked={this.state.saveUserSettingsForm.generate_totp_2fa}
869                 onChange={linkEvent(this, this.handleGenerateTotp)}
870               />
871               <label className="form-check-label" htmlFor="user-generate-totp">
872                 {i18n.t("set_up_two_factor")}
873               </label>
874             </div>
875           </div>
876         )}
877
878         {totpUrl && (
879           <>
880             <div>
881               <a className="btn btn-secondary mb-2" href={totpUrl}>
882                 {i18n.t("two_factor_link")}
883               </a>
884             </div>
885             <div className="form-group">
886               <div className="form-check">
887                 <input
888                   className="form-check-input"
889                   id="user-remove-totp"
890                   type="checkbox"
891                   checked={
892                     this.state.saveUserSettingsForm.generate_totp_2fa == false
893                   }
894                   onChange={linkEvent(this, this.handleRemoveTotp)}
895                 />
896                 <label className="form-check-label" htmlFor="user-remove-totp">
897                   {i18n.t("remove_two_factor")}
898                 </label>
899               </div>
900             </div>
901           </>
902         )}
903       </>
904     );
905   }
906
907   handlePersonSearch = debounce(async (text: string) => {
908     this.setState({ searchPersonLoading: true });
909
910     const searchPersonOptions: Choice[] = [];
911
912     if (text.length > 0) {
913       searchPersonOptions.push(
914         ...(await fetchUsers(text)).users.map(personToChoice)
915       );
916     }
917
918     this.setState({
919       searchPersonLoading: false,
920       searchPersonOptions,
921     });
922   });
923
924   handleCommunitySearch = debounce(async (text: string) => {
925     this.setState({ searchCommunityLoading: true });
926
927     const searchCommunityOptions: Choice[] = [];
928
929     if (text.length > 0) {
930       searchCommunityOptions.push(
931         ...(await fetchCommunities(text)).communities.map(communityToChoice)
932       );
933     }
934
935     this.setState({
936       searchCommunityLoading: false,
937       searchCommunityOptions,
938     });
939   });
940
941   handleBlockPerson({ value }: Choice) {
942     const auth = myAuth();
943     if (auth && value !== "0") {
944       const blockUserForm: BlockPerson = {
945         person_id: Number(value),
946         block: true,
947         auth,
948       };
949
950       WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
951     }
952   }
953
954   handleUnblockPerson(i: { ctx: Settings; recipientId: number }) {
955     const auth = myAuth();
956     if (auth) {
957       const blockUserForm: BlockPerson = {
958         person_id: i.recipientId,
959         block: false,
960         auth,
961       };
962       WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
963     }
964   }
965
966   handleBlockCommunity({ value }: Choice) {
967     const auth = myAuth();
968     if (auth && value !== "0") {
969       const blockCommunityForm: BlockCommunity = {
970         community_id: Number(value),
971         block: true,
972         auth,
973       };
974       WebSocketService.Instance.send(
975         wsClient.blockCommunity(blockCommunityForm)
976       );
977     }
978   }
979
980   handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
981     const auth = myAuth();
982     if (auth) {
983       const blockCommunityForm: BlockCommunity = {
984         community_id: i.communityId,
985         block: false,
986         auth,
987       };
988       WebSocketService.Instance.send(
989         wsClient.blockCommunity(blockCommunityForm)
990       );
991     }
992   }
993
994   handleShowNsfwChange(i: Settings, event: any) {
995     i.state.saveUserSettingsForm.show_nsfw = event.target.checked;
996     i.setState(i.state);
997   }
998
999   handleShowAvatarsChange(i: Settings, event: any) {
1000     i.state.saveUserSettingsForm.show_avatars = event.target.checked;
1001     let mui = UserService.Instance.myUserInfo;
1002     if (mui) {
1003       mui.local_user_view.local_user.show_avatars = event.target.checked;
1004     }
1005     i.setState(i.state);
1006   }
1007
1008   handleBotAccount(i: Settings, event: any) {
1009     i.state.saveUserSettingsForm.bot_account = event.target.checked;
1010     i.setState(i.state);
1011   }
1012
1013   handleShowBotAccounts(i: Settings, event: any) {
1014     i.state.saveUserSettingsForm.show_bot_accounts = event.target.checked;
1015     i.setState(i.state);
1016   }
1017
1018   handleReadPosts(i: Settings, event: any) {
1019     i.state.saveUserSettingsForm.show_read_posts = event.target.checked;
1020     i.setState(i.state);
1021   }
1022
1023   handleShowNewPostNotifs(i: Settings, event: any) {
1024     i.state.saveUserSettingsForm.show_new_post_notifs = event.target.checked;
1025     i.setState(i.state);
1026   }
1027
1028   handleShowScoresChange(i: Settings, event: any) {
1029     i.state.saveUserSettingsForm.show_scores = event.target.checked;
1030     let mui = UserService.Instance.myUserInfo;
1031     if (mui) {
1032       mui.local_user_view.local_user.show_scores = event.target.checked;
1033     }
1034     i.setState(i.state);
1035   }
1036
1037   handleGenerateTotp(i: Settings, event: any) {
1038     // Coerce false to undefined here, so it won't generate it.
1039     let checked: boolean | undefined = event.target.checked || undefined;
1040     if (checked) {
1041       toast(i18n.t("two_factor_setup_instructions"));
1042     }
1043     i.state.saveUserSettingsForm.generate_totp_2fa = checked;
1044     i.setState(i.state);
1045   }
1046
1047   handleRemoveTotp(i: Settings, event: any) {
1048     // Coerce true to undefined here, so it won't generate it.
1049     let checked: boolean | undefined = !event.target.checked && undefined;
1050     i.state.saveUserSettingsForm.generate_totp_2fa = checked;
1051     i.setState(i.state);
1052   }
1053
1054   handleSendNotificationsToEmailChange(i: Settings, event: any) {
1055     i.state.saveUserSettingsForm.send_notifications_to_email =
1056       event.target.checked;
1057     i.setState(i.state);
1058   }
1059
1060   handleThemeChange(i: Settings, event: any) {
1061     i.state.saveUserSettingsForm.theme = event.target.value;
1062     setTheme(event.target.value, true);
1063     i.setState(i.state);
1064   }
1065
1066   handleInterfaceLangChange(i: Settings, event: any) {
1067     i.state.saveUserSettingsForm.interface_language = event.target.value;
1068     i18n.changeLanguage(
1069       getLanguages(i.state.saveUserSettingsForm.interface_language).at(0)
1070     );
1071     i.setState(i.state);
1072   }
1073
1074   handleDiscussionLanguageChange(val: number[]) {
1075     this.setState(
1076       s => ((s.saveUserSettingsForm.discussion_languages = val), s)
1077     );
1078   }
1079
1080   handleSortTypeChange(val: SortType) {
1081     this.setState(s => ((s.saveUserSettingsForm.default_sort_type = val), s));
1082   }
1083
1084   handleListingTypeChange(val: ListingType) {
1085     this.setState(
1086       s => ((s.saveUserSettingsForm.default_listing_type = val), s)
1087     );
1088   }
1089
1090   handleEmailChange(i: Settings, event: any) {
1091     i.state.saveUserSettingsForm.email = event.target.value;
1092     i.setState(i.state);
1093   }
1094
1095   handleBioChange(val: string) {
1096     this.setState(s => ((s.saveUserSettingsForm.bio = val), s));
1097   }
1098
1099   handleAvatarUpload(url: string) {
1100     this.setState(s => ((s.saveUserSettingsForm.avatar = url), s));
1101   }
1102
1103   handleAvatarRemove() {
1104     this.setState(s => ((s.saveUserSettingsForm.avatar = ""), s));
1105   }
1106
1107   handleBannerUpload(url: string) {
1108     this.setState(s => ((s.saveUserSettingsForm.banner = url), s));
1109   }
1110
1111   handleBannerRemove() {
1112     this.setState(s => ((s.saveUserSettingsForm.banner = ""), s));
1113   }
1114
1115   handleDisplayNameChange(i: Settings, event: any) {
1116     i.state.saveUserSettingsForm.display_name = event.target.value;
1117     i.setState(i.state);
1118   }
1119
1120   handleMatrixUserIdChange(i: Settings, event: any) {
1121     i.state.saveUserSettingsForm.matrix_user_id = event.target.value;
1122     i.setState(i.state);
1123   }
1124
1125   handleNewPasswordChange(i: Settings, event: any) {
1126     i.state.changePasswordForm.new_password = event.target.value;
1127     if (i.state.changePasswordForm.new_password == "") {
1128       i.state.changePasswordForm.new_password = undefined;
1129     }
1130     i.setState(i.state);
1131   }
1132
1133   handleNewPasswordVerifyChange(i: Settings, event: any) {
1134     i.state.changePasswordForm.new_password_verify = event.target.value;
1135     if (i.state.changePasswordForm.new_password_verify == "") {
1136       i.state.changePasswordForm.new_password_verify = undefined;
1137     }
1138     i.setState(i.state);
1139   }
1140
1141   handleOldPasswordChange(i: Settings, event: any) {
1142     i.state.changePasswordForm.old_password = event.target.value;
1143     if (i.state.changePasswordForm.old_password == "") {
1144       i.state.changePasswordForm.old_password = undefined;
1145     }
1146     i.setState(i.state);
1147   }
1148
1149   handleSaveSettingsSubmit(i: Settings, event: any) {
1150     event.preventDefault();
1151     i.setState({ saveUserSettingsLoading: true });
1152     let auth = myAuth();
1153     if (auth) {
1154       let form: SaveUserSettings = { ...i.state.saveUserSettingsForm, auth };
1155       WebSocketService.Instance.send(wsClient.saveUserSettings(form));
1156     }
1157   }
1158
1159   handleChangePasswordSubmit(i: Settings, event: any) {
1160     event.preventDefault();
1161     i.setState({ changePasswordLoading: true });
1162     let auth = myAuth();
1163     let pForm = i.state.changePasswordForm;
1164     let new_password = pForm.new_password;
1165     let new_password_verify = pForm.new_password_verify;
1166     let old_password = pForm.old_password;
1167     if (auth && new_password && old_password && new_password_verify) {
1168       let form: ChangePassword = {
1169         new_password,
1170         new_password_verify,
1171         old_password,
1172         auth,
1173       };
1174
1175       WebSocketService.Instance.send(wsClient.changePassword(form));
1176     }
1177   }
1178
1179   handleDeleteAccountShowConfirmToggle(i: Settings, event: any) {
1180     event.preventDefault();
1181     i.setState({ deleteAccountShowConfirm: !i.state.deleteAccountShowConfirm });
1182   }
1183
1184   handleDeleteAccountPasswordChange(i: Settings, event: any) {
1185     i.state.deleteAccountForm.password = event.target.value;
1186     i.setState(i.state);
1187   }
1188
1189   handleDeleteAccount(i: Settings, event: any) {
1190     event.preventDefault();
1191     i.setState({ deleteAccountLoading: true });
1192     let auth = myAuth();
1193     let password = i.state.deleteAccountForm.password;
1194     if (auth && password) {
1195       let form: DeleteAccount = {
1196         password,
1197         auth,
1198       };
1199       WebSocketService.Instance.send(wsClient.deleteAccount(form));
1200     }
1201   }
1202
1203   handleSwitchTab(i: { ctx: Settings; tab: string }) {
1204     i.ctx.setState({ currentTab: i.tab });
1205   }
1206
1207   parseMessage(msg: any) {
1208     let op = wsUserOp(msg);
1209     console.log(msg);
1210     if (msg.error) {
1211       this.setState({
1212         saveUserSettingsLoading: false,
1213         changePasswordLoading: false,
1214         deleteAccountLoading: false,
1215       });
1216       toast(i18n.t(msg.error), "danger");
1217       return;
1218     } else if (op == UserOperation.SaveUserSettings) {
1219       this.setState({ saveUserSettingsLoading: false });
1220       toast(i18n.t("saved"));
1221       window.scrollTo(0, 0);
1222     } else if (op == UserOperation.ChangePassword) {
1223       let data = wsJsonToRes<LoginResponse>(msg);
1224       UserService.Instance.login(data);
1225       this.setState({ changePasswordLoading: false });
1226       window.scrollTo(0, 0);
1227       toast(i18n.t("password_changed"));
1228     } else if (op == UserOperation.DeleteAccount) {
1229       this.setState({
1230         deleteAccountLoading: false,
1231         deleteAccountShowConfirm: false,
1232       });
1233       UserService.Instance.logout();
1234       window.location.href = "/";
1235     } else if (op == UserOperation.BlockPerson) {
1236       let data = wsJsonToRes<BlockPersonResponse>(msg);
1237       updatePersonBlock(data);
1238       let mui = UserService.Instance.myUserInfo;
1239       if (mui) {
1240         this.setState({ personBlocks: mui.person_blocks });
1241       }
1242     } else if (op == UserOperation.BlockCommunity) {
1243       let data = wsJsonToRes<BlockCommunityResponse>(msg);
1244       updateCommunityBlock(data);
1245       let mui = UserService.Instance.myUserInfo;
1246       if (mui) {
1247         this.setState({ communityBlocks: mui.community_blocks });
1248       }
1249     }
1250   }
1251 }