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