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