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