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