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