15 import { capitalizeFirstLetter, debounce } from "@utils/helpers";
16 import { Choice } from "@utils/types";
17 import classNames from "classnames";
18 import { NoOptionI18nKeys } from "i18next";
19 import { Component, linkEvent } from "inferno";
21 BlockCommunityResponse,
24 DeleteAccountResponse,
30 } from "lemmy-js-client";
31 import { elementUrl, emDash, relTags } from "../../config";
32 import { UserService } from "../../services";
33 import { HttpService, RequestState } from "../../services/HttpService";
34 import { I18NextService, languages } from "../../services/I18NextService";
35 import { setupTippy } from "../../tippy";
36 import { toast } from "../../toast";
37 import { HtmlTags } from "../common/html-tags";
38 import { Icon, Spinner } from "../common/icon";
39 import { ImageUploadForm } from "../common/image-upload-form";
40 import { LanguageSelect } from "../common/language-select";
41 import { ListingTypeSelect } from "../common/listing-type-select";
42 import { MarkdownTextArea } from "../common/markdown-textarea";
43 import PasswordInput from "../common/password-input";
44 import { SearchableSelect } from "../common/searchable-select";
45 import { SortSelect } from "../common/sort-select";
46 import Tabs from "../common/tabs";
47 import { CommunityLink } from "../community/community-link";
48 import { PersonListing } from "./person-listing";
50 interface SettingsState {
51 saveRes: RequestState<LoginResponse>;
52 changePasswordRes: RequestState<LoginResponse>;
53 deleteAccountRes: RequestState<DeleteAccountResponse>;
54 // TODO redo these forms
55 saveUserSettingsForm: {
58 auto_expand?: boolean;
60 default_sort_type?: SortType;
61 default_listing_type?: ListingType;
62 interface_language?: string;
65 display_name?: string;
68 matrix_user_id?: string;
69 show_avatars?: boolean;
70 show_scores?: boolean;
71 send_notifications_to_email?: boolean;
72 bot_account?: boolean;
73 show_bot_accounts?: boolean;
74 show_read_posts?: boolean;
75 show_new_post_notifs?: boolean;
76 discussion_languages?: number[];
77 generate_totp_2fa?: boolean;
78 open_links_in_new_tab?: boolean;
81 new_password?: string;
82 new_password_verify?: string;
83 old_password?: string;
88 personBlocks: PersonBlockView[];
89 communityBlocks: CommunityBlockView[];
92 deleteAccountShowConfirm: boolean;
93 siteRes: GetSiteResponse;
94 searchCommunityLoading: boolean;
95 searchCommunityOptions: Choice[];
96 searchPersonLoading: boolean;
97 searchPersonOptions: Choice[];
100 type FilterType = "user" | "community";
109 filterType: FilterType;
111 onSearch: (text: string) => void;
112 onChange: (choice: Choice) => void;
115 <div className="mb-3 row">
117 className="col-md-4 col-form-label"
118 htmlFor={`block-${filterType}-filter`}
120 {I18NextService.i18n.t(`block_${filterType}` as NoOptionI18nKeys)}
122 <div className="col-md-8">
124 id={`block-${filterType}-filter`}
126 { label: emDash, value: "0", disabled: true } as Choice,
136 export class Settings extends Component<any, SettingsState> {
137 private isoData = setIsoData(this.context);
138 state: SettingsState = {
139 saveRes: { state: "empty" },
140 deleteAccountRes: { state: "empty" },
141 changePasswordRes: { state: "empty" },
142 saveUserSettingsForm: {},
143 changePasswordForm: {},
144 deleteAccountShowConfirm: false,
145 deleteAccountForm: {},
148 currentTab: "settings",
149 siteRes: this.isoData.site_res,
151 searchCommunityLoading: false,
152 searchCommunityOptions: [],
153 searchPersonLoading: false,
154 searchPersonOptions: [],
157 constructor(props: any, context: any) {
158 super(props, context);
160 this.handleSortTypeChange = this.handleSortTypeChange.bind(this);
161 this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
162 this.handleBioChange = this.handleBioChange.bind(this);
163 this.handleDiscussionLanguageChange =
164 this.handleDiscussionLanguageChange.bind(this);
166 this.handleAvatarUpload = this.handleAvatarUpload.bind(this);
167 this.handleAvatarRemove = this.handleAvatarRemove.bind(this);
169 this.handleBannerUpload = this.handleBannerUpload.bind(this);
170 this.handleBannerRemove = this.handleBannerRemove.bind(this);
171 this.userSettings = this.userSettings.bind(this);
172 this.blockCards = this.blockCards.bind(this);
174 this.handleBlockPerson = this.handleBlockPerson.bind(this);
175 this.handleBlockCommunity = this.handleBlockCommunity.bind(this);
177 const mui = UserService.Instance.myUserInfo;
186 default_listing_type,
192 show_new_post_notifs,
193 send_notifications_to_email,
204 } = mui.local_user_view;
208 personBlocks: mui.person_blocks,
209 communityBlocks: mui.community_blocks,
210 saveUserSettingsForm: {
211 ...this.state.saveUserSettingsForm,
215 theme: theme ?? "browser",
217 default_listing_type,
219 discussion_languages: mui.discussion_languages,
228 show_new_post_notifs,
231 send_notifications_to_email,
238 async componentDidMount() {
240 this.setState({ themeList: await fetchThemeList() });
243 get documentTitle(): string {
244 return I18NextService.i18n.t("settings");
249 <div className="person-settings container-lg">
251 title={this.documentTitle}
252 path={this.context.router.route.match.url}
253 description={this.documentTitle}
254 image={this.state.saveUserSettingsForm.avatar}
260 label: I18NextService.i18n.t("settings"),
261 getNode: this.userSettings,
265 label: I18NextService.i18n.t("blocks"),
266 getNode: this.blockCards,
274 userSettings(isSelected: boolean) {
277 className={classNames("tab-pane show", {
281 id="settings-tab-pane"
283 <div className="row">
284 <div className="col-12 col-md-6">
285 <div className="card border-secondary mb-3">
286 <div className="card-body">{this.saveUserSettingsHtmlForm()}</div>
289 <div className="col-12 col-md-6">
290 <div className="card border-secondary mb-3">
291 <div className="card-body">{this.changePasswordHtmlForm()}</div>
299 blockCards(isSelected: boolean) {
302 className={classNames("tab-pane", {
308 <div className="row">
309 <div className="col-12 col-md-6">
310 <div className="card border-secondary mb-3">
311 <div className="card-body">{this.blockUserCard()}</div>
314 <div className="col-12 col-md-6">
315 <div className="card border-secondary mb-3">
316 <div className="card-body">{this.blockCommunityCard()}</div>
324 changePasswordHtmlForm() {
327 <h2 className="h5">{I18NextService.i18n.t("change_password")}</h2>
328 <form onSubmit={linkEvent(this, this.handleChangePasswordSubmit)}>
329 <div className="mb-3">
332 value={this.state.changePasswordForm.new_password}
333 onInput={linkEvent(this, this.handleNewPasswordChange)}
335 label={I18NextService.i18n.t("new_password")}
339 <div className="mb-3">
341 id="verify-new-password"
342 value={this.state.changePasswordForm.new_password_verify}
343 onInput={linkEvent(this, this.handleNewPasswordVerifyChange)}
344 label={I18NextService.i18n.t("verify_password")}
348 <div className="mb-3">
350 id="user-old-password"
351 value={this.state.changePasswordForm.old_password}
352 onInput={linkEvent(this, this.handleOldPasswordChange)}
353 label={I18NextService.i18n.t("old_password")}
356 <div className="input-group mb-3">
359 className="btn d-block btn-secondary me-4 w-100"
361 {this.state.changePasswordRes.state === "loading" ? (
364 capitalizeFirstLetter(I18NextService.i18n.t("save"))
374 const { searchPersonLoading, searchPersonOptions } = this.state;
380 loading={searchPersonLoading}
381 onChange={this.handleBlockPerson}
382 onSearch={this.handlePersonSearch}
383 options={searchPersonOptions}
385 {this.blockedUsersList()}
393 <h2 className="h5">{I18NextService.i18n.t("blocked_users")}</h2>
394 <ul className="list-unstyled mb-0">
395 {this.state.personBlocks.map(pb => (
396 <li key={pb.target.id}>
398 <PersonListing person={pb.target} />
400 className="btn btn-sm"
402 { ctx: this, recipientId: pb.target.id },
403 this.handleUnblockPerson,
405 data-tippy-content={I18NextService.i18n.t("unblock_user")}
407 <Icon icon="x" classes="icon-inline" />
417 blockCommunityCard() {
418 const { searchCommunityLoading, searchCommunityOptions } = this.state;
423 filterType="community"
424 loading={searchCommunityLoading}
425 onChange={this.handleBlockCommunity}
426 onSearch={this.handleCommunitySearch}
427 options={searchCommunityOptions}
429 {this.blockedCommunitiesList()}
434 blockedCommunitiesList() {
437 <h2 className="h5">{I18NextService.i18n.t("blocked_communities")}</h2>
438 <ul className="list-unstyled mb-0">
439 {this.state.communityBlocks.map(cb => (
440 <li key={cb.community.id}>
442 <CommunityLink community={cb.community} />
444 className="btn btn-sm"
446 { ctx: this, communityId: cb.community.id },
447 this.handleUnblockCommunity,
449 data-tippy-content={I18NextService.i18n.t(
453 <Icon icon="x" classes="icon-inline" />
463 saveUserSettingsHtmlForm() {
464 const selectedLangs = this.state.saveUserSettingsForm.discussion_languages;
468 <h2 className="h5">{I18NextService.i18n.t("settings")}</h2>
469 <form onSubmit={linkEvent(this, this.handleSaveSettingsSubmit)}>
470 <div className="mb-3 row">
471 <label className="col-sm-3 col-form-label" htmlFor="display-name">
472 {I18NextService.i18n.t("display_name")}
474 <div className="col-sm-9">
478 className="form-control"
479 placeholder={I18NextService.i18n.t("optional")}
480 value={this.state.saveUserSettingsForm.display_name}
481 onInput={linkEvent(this, this.handleDisplayNameChange)}
482 pattern="^(?!@)(.+)$"
487 <div className="mb-3 row">
488 <label className="col-sm-3 col-form-label" htmlFor="user-bio">
489 {I18NextService.i18n.t("bio")}
491 <div className="col-sm-9">
493 initialContent={this.state.saveUserSettingsForm.bio}
494 onContentChange={this.handleBioChange}
496 hideNavigationWarnings
497 allLanguages={this.state.siteRes.all_languages}
498 siteLanguages={this.state.siteRes.discussion_languages}
502 <div className="mb-3 row">
503 <label className="col-sm-3 col-form-label" htmlFor="user-email">
504 {I18NextService.i18n.t("email")}
506 <div className="col-sm-9">
510 className="form-control"
511 placeholder={I18NextService.i18n.t("optional")}
512 value={this.state.saveUserSettingsForm.email}
513 onInput={linkEvent(this, this.handleEmailChange)}
518 <div className="mb-3 row">
519 <label className="col-sm-3 col-form-label" htmlFor="matrix-user-id">
520 <a href={elementUrl} rel={relTags}>
521 {I18NextService.i18n.t("matrix_user_id")}
524 <div className="col-sm-9">
528 className="form-control"
529 placeholder="@user:example.com"
530 value={this.state.saveUserSettingsForm.matrix_user_id}
531 onInput={linkEvent(this, this.handleMatrixUserIdChange)}
532 pattern="^@[A-Za-z0-9._=-]+:[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
536 <div className="mb-3 row">
537 <label className="col-sm-3 col-form-label">
538 {I18NextService.i18n.t("avatar")}
540 <div className="col-sm-9">
542 uploadTitle={I18NextService.i18n.t("upload_avatar")}
543 imageSrc={this.state.saveUserSettingsForm.avatar}
544 onUpload={this.handleAvatarUpload}
545 onRemove={this.handleAvatarRemove}
550 <div className="mb-3 row">
551 <label className="col-sm-3 col-form-label">
552 {I18NextService.i18n.t("banner")}
554 <div className="col-sm-9">
556 uploadTitle={I18NextService.i18n.t("upload_banner")}
557 imageSrc={this.state.saveUserSettingsForm.banner}
558 onUpload={this.handleBannerUpload}
559 onRemove={this.handleBannerRemove}
563 <div className="mb-3 row">
564 <label className="col-sm-3 form-label" htmlFor="user-language">
565 {I18NextService.i18n.t("interface_language")}
567 <div className="col-sm-9">
570 value={this.state.saveUserSettingsForm.interface_language}
571 onChange={linkEvent(this, this.handleInterfaceLangChange)}
572 className="form-select d-inline-block w-auto"
574 <option disabled aria-hidden="true">
575 {I18NextService.i18n.t("interface_language")}
577 <option value="browser">
578 {I18NextService.i18n.t("browser_default")}
580 <option disabled aria-hidden="true">
584 .sort((a, b) => a.code.localeCompare(b.code))
586 <option key={lang.code} value={lang.code}>
594 allLanguages={this.state.siteRes.all_languages}
595 siteLanguages={this.state.siteRes.discussion_languages}
596 selectedLanguageIds={selectedLangs}
598 showLanguageWarning={true}
601 onChange={this.handleDiscussionLanguageChange}
603 <div className="mb-3 row">
604 <label className="col-sm-3 col-form-label" htmlFor="user-theme">
605 {I18NextService.i18n.t("theme")}
607 <div className="col-sm-9">
610 value={this.state.saveUserSettingsForm.theme}
611 onChange={linkEvent(this, this.handleThemeChange)}
612 className="form-select d-inline-block w-auto"
614 <option disabled aria-hidden="true">
615 {I18NextService.i18n.t("theme")}
617 <option value="browser">
618 {I18NextService.i18n.t("browser_default")}
620 <option value="browser-compact">
621 {I18NextService.i18n.t("browser_default_compact")}
623 {this.state.themeList.map(theme => (
624 <option key={theme} value={theme}>
631 <form className="mb-3 row">
632 <label className="col-sm-3 col-form-label">
633 {I18NextService.i18n.t("type")}
635 <div className="col-sm-9">
638 this.state.saveUserSettingsForm.default_listing_type ??
641 showLocal={showLocal(this.isoData)}
643 onChange={this.handleListingTypeChange}
647 <form className="mb-3 row">
648 <label className="col-sm-3 col-form-label">
649 {I18NextService.i18n.t("sort_type")}
651 <div className="col-sm-9">
654 this.state.saveUserSettingsForm.default_sort_type ?? "Active"
656 onChange={this.handleSortTypeChange}
660 <div className="input-group mb-3">
661 <div className="form-check">
663 className="form-check-input"
666 checked={this.state.saveUserSettingsForm.show_nsfw}
667 onChange={linkEvent(this, this.handleShowNsfwChange)}
669 <label className="form-check-label" htmlFor="user-show-nsfw">
670 {I18NextService.i18n.t("show_nsfw")}
674 <div className="input-group mb-3">
675 <div className="form-check">
677 className="form-check-input"
680 checked={this.state.saveUserSettingsForm.blur_nsfw}
681 onChange={linkEvent(this, this.handleBlurNsfwChange)}
683 <label className="form-check-label" htmlFor="user-blur-nsfw">
684 {I18NextService.i18n.t("blur_nsfw")}
688 <div className="input-group mb-3">
689 <div className="form-check">
691 className="form-check-input"
692 id="user-auto-expand"
694 checked={this.state.saveUserSettingsForm.auto_expand}
695 onChange={linkEvent(this, this.handleAutoExpandChange)}
697 <label className="form-check-label" htmlFor="user-auto-expand">
698 {I18NextService.i18n.t("auto_expand")}
702 <div className="input-group mb-3">
703 <div className="form-check">
705 className="form-check-input"
706 id="user-show-scores"
708 checked={this.state.saveUserSettingsForm.show_scores}
709 onChange={linkEvent(this, this.handleShowScoresChange)}
711 <label className="form-check-label" htmlFor="user-show-scores">
712 {I18NextService.i18n.t("show_scores")}
716 <div className="input-group mb-3">
717 <div className="form-check">
719 className="form-check-input"
720 id="user-show-avatars"
722 checked={this.state.saveUserSettingsForm.show_avatars}
723 onChange={linkEvent(this, this.handleShowAvatarsChange)}
725 <label className="form-check-label" htmlFor="user-show-avatars">
726 {I18NextService.i18n.t("show_avatars")}
730 <div className="input-group mb-3">
731 <div className="form-check">
733 className="form-check-input"
734 id="user-bot-account"
736 checked={this.state.saveUserSettingsForm.bot_account}
737 onChange={linkEvent(this, this.handleBotAccount)}
739 <label className="form-check-label" htmlFor="user-bot-account">
740 {I18NextService.i18n.t("bot_account")}
744 <div className="input-group mb-3">
745 <div className="form-check">
747 className="form-check-input"
748 id="user-show-bot-accounts"
750 checked={this.state.saveUserSettingsForm.show_bot_accounts}
751 onChange={linkEvent(this, this.handleShowBotAccounts)}
754 className="form-check-label"
755 htmlFor="user-show-bot-accounts"
757 {I18NextService.i18n.t("show_bot_accounts")}
761 <div className="input-group mb-3">
762 <div className="form-check">
764 className="form-check-input"
765 id="user-show-read-posts"
767 checked={this.state.saveUserSettingsForm.show_read_posts}
768 onChange={linkEvent(this, this.handleReadPosts)}
771 className="form-check-label"
772 htmlFor="user-show-read-posts"
774 {I18NextService.i18n.t("show_read_posts")}
778 <div className="input-group mb-3">
779 <div className="form-check">
781 className="form-check-input"
782 id="user-show-new-post-notifs"
784 checked={this.state.saveUserSettingsForm.show_new_post_notifs}
785 onChange={linkEvent(this, this.handleShowNewPostNotifs)}
788 className="form-check-label"
789 htmlFor="user-show-new-post-notifs"
791 {I18NextService.i18n.t("show_new_post_notifs")}
795 <div className="input-group mb-3">
796 <div className="form-check">
798 className="form-check-input"
799 id="user-send-notifications-to-email"
801 disabled={!this.state.saveUserSettingsForm.email}
803 this.state.saveUserSettingsForm.send_notifications_to_email
807 this.handleSendNotificationsToEmailChange,
811 className="form-check-label"
812 htmlFor="user-send-notifications-to-email"
814 {I18NextService.i18n.t("send_notifications_to_email")}
818 <div className="input-group mb-3">
819 <div className="form-check">
821 className="form-check-input"
822 id="user-open-links-in-new-tab"
824 checked={this.state.saveUserSettingsForm.open_links_in_new_tab}
825 onChange={linkEvent(this, this.handleOpenInNewTab)}
828 className="form-check-label"
829 htmlFor="user-open-links-in-new-tab"
831 {I18NextService.i18n.t("open_links_in_new_tab")}
836 <div className="input-group mb-3">
837 <button type="submit" className="btn d-block btn-secondary me-4">
838 {this.state.saveRes.state === "loading" ? (
841 capitalizeFirstLetter(I18NextService.i18n.t("save"))
848 onSubmit={linkEvent(this, this.handleDeleteAccount)}
852 className="btn d-block btn-danger"
855 this.handleDeleteAccountShowConfirmToggle,
858 {I18NextService.i18n.t("delete_account")}
860 {this.state.deleteAccountShowConfirm && (
863 className="my-2 alert alert-danger d-block"
865 htmlFor="password-delete-account"
867 {I18NextService.i18n.t("delete_account_confirm")}
870 id="password-delete-account"
871 value={this.state.deleteAccountForm.password}
874 this.handleDeleteAccountPasswordChange,
880 className="btn btn-danger me-4"
881 disabled={!this.state.deleteAccountForm.password}
883 {this.state.deleteAccountRes.state === "loading" ? (
886 capitalizeFirstLetter(I18NextService.i18n.t("delete"))
890 className="btn btn-secondary"
894 this.handleDeleteAccountShowConfirmToggle,
897 {I18NextService.i18n.t("cancel")}
909 UserService.Instance.myUserInfo?.local_user_view.local_user.totp_2fa_url;
914 <div className="input-group mb-3">
915 <div className="form-check">
917 className="form-check-input"
918 id="user-generate-totp"
920 checked={this.state.saveUserSettingsForm.generate_totp_2fa}
921 onChange={linkEvent(this, this.handleGenerateTotp)}
923 <label className="form-check-label" htmlFor="user-generate-totp">
924 {I18NextService.i18n.t("set_up_two_factor")}
933 <a className="btn btn-secondary mb-2" href={totpUrl}>
934 {I18NextService.i18n.t("two_factor_link")}
937 <div className="input-group mb-3">
938 <div className="form-check">
940 className="form-check-input"
941 id="user-remove-totp"
944 this.state.saveUserSettingsForm.generate_totp_2fa === false
946 onChange={linkEvent(this, this.handleRemoveTotp)}
948 <label className="form-check-label" htmlFor="user-remove-totp">
949 {I18NextService.i18n.t("remove_two_factor")}
959 handlePersonSearch = debounce(async (text: string) => {
960 this.setState({ searchPersonLoading: true });
962 const searchPersonOptions: Choice[] = [];
964 if (text.length > 0) {
965 searchPersonOptions.push(...(await fetchUsers(text)).map(personToChoice));
969 searchPersonLoading: false,
974 handleCommunitySearch = debounce(async (text: string) => {
975 this.setState({ searchCommunityLoading: true });
977 const searchCommunityOptions: Choice[] = [];
979 if (text.length > 0) {
980 searchCommunityOptions.push(
981 ...(await fetchCommunities(text)).map(communityToChoice),
986 searchCommunityLoading: false,
987 searchCommunityOptions,
991 async handleBlockPerson({ value }: Choice) {
993 const res = await HttpService.client.blockPerson({
994 person_id: Number(value),
996 auth: myAuthRequired(),
998 this.personBlock(res);
1002 async handleUnblockPerson({
1007 recipientId: number;
1009 const res = await HttpService.client.blockPerson({
1010 person_id: recipientId,
1012 auth: myAuthRequired(),
1014 ctx.personBlock(res);
1017 async handleBlockCommunity({ value }: Choice) {
1018 if (value !== "0") {
1019 const res = await HttpService.client.blockCommunity({
1020 community_id: Number(value),
1022 auth: myAuthRequired(),
1024 this.communityBlock(res);
1028 async handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
1029 const auth = myAuth();
1031 const res = await HttpService.client.blockCommunity({
1032 community_id: i.communityId,
1034 auth: myAuthRequired(),
1036 i.ctx.communityBlock(res);
1040 handleShowNsfwChange(i: Settings, event: any) {
1042 s => ((s.saveUserSettingsForm.show_nsfw = event.target.checked), s),
1046 handleBlurNsfwChange(i: Settings, event: any) {
1048 s => ((s.saveUserSettingsForm.blur_nsfw = event.target.checked), s),
1052 handleAutoExpandChange(i: Settings, event: any) {
1054 s => ((s.saveUserSettingsForm.auto_expand = event.target.checked), s),
1058 handleShowAvatarsChange(i: Settings, event: any) {
1059 const mui = UserService.Instance.myUserInfo;
1061 mui.local_user_view.local_user.show_avatars = event.target.checked;
1064 s => ((s.saveUserSettingsForm.show_avatars = event.target.checked), s),
1068 handleBotAccount(i: Settings, event: any) {
1070 s => ((s.saveUserSettingsForm.bot_account = event.target.checked), s),
1074 handleShowBotAccounts(i: Settings, event: any) {
1077 (s.saveUserSettingsForm.show_bot_accounts = event.target.checked), s
1082 handleReadPosts(i: Settings, event: any) {
1084 s => ((s.saveUserSettingsForm.show_read_posts = event.target.checked), s),
1088 handleShowNewPostNotifs(i: Settings, event: any) {
1091 (s.saveUserSettingsForm.show_new_post_notifs = event.target.checked), s
1096 handleOpenInNewTab(i: Settings, event: any) {
1099 (s.saveUserSettingsForm.open_links_in_new_tab = event.target.checked), s
1104 handleShowScoresChange(i: Settings, event: any) {
1105 const mui = UserService.Instance.myUserInfo;
1107 mui.local_user_view.local_user.show_scores = event.target.checked;
1110 s => ((s.saveUserSettingsForm.show_scores = event.target.checked), s),
1114 handleGenerateTotp(i: Settings, event: any) {
1115 // Coerce false to undefined here, so it won't generate it.
1116 const checked: boolean | undefined = event.target.checked || undefined;
1118 toast(I18NextService.i18n.t("two_factor_setup_instructions"));
1120 i.setState(s => ((s.saveUserSettingsForm.generate_totp_2fa = checked), s));
1123 handleRemoveTotp(i: Settings, event: any) {
1124 // Coerce true to undefined here, so it won't generate it.
1125 const checked: boolean | undefined = !event.target.checked && undefined;
1126 i.setState(s => ((s.saveUserSettingsForm.generate_totp_2fa = checked), s));
1129 handleSendNotificationsToEmailChange(i: Settings, event: any) {
1132 (s.saveUserSettingsForm.send_notifications_to_email =
1133 event.target.checked),
1139 handleThemeChange(i: Settings, event: any) {
1140 i.setState(s => ((s.saveUserSettingsForm.theme = event.target.value), s));
1141 setTheme(event.target.value, true);
1144 handleInterfaceLangChange(i: Settings, event: any) {
1145 const newLang = event.target.value ?? "browser";
1146 I18NextService.i18n.changeLanguage(
1147 newLang === "browser" ? navigator.languages : newLang,
1152 (s.saveUserSettingsForm.interface_language = event.target.value), s
1157 handleDiscussionLanguageChange(val: number[]) {
1159 s => ((s.saveUserSettingsForm.discussion_languages = val), s),
1163 handleSortTypeChange(val: SortType) {
1164 this.setState(s => ((s.saveUserSettingsForm.default_sort_type = val), s));
1167 handleListingTypeChange(val: ListingType) {
1169 s => ((s.saveUserSettingsForm.default_listing_type = val), s),
1173 handleEmailChange(i: Settings, event: any) {
1174 i.setState(s => ((s.saveUserSettingsForm.email = event.target.value), s));
1177 handleBioChange(val: string) {
1178 this.setState(s => ((s.saveUserSettingsForm.bio = val), s));
1181 handleAvatarUpload(url: string) {
1182 this.setState(s => ((s.saveUserSettingsForm.avatar = url), s));
1185 handleAvatarRemove() {
1186 this.setState(s => ((s.saveUserSettingsForm.avatar = ""), s));
1189 handleBannerUpload(url: string) {
1190 this.setState(s => ((s.saveUserSettingsForm.banner = url), s));
1193 handleBannerRemove() {
1194 this.setState(s => ((s.saveUserSettingsForm.banner = ""), s));
1197 handleDisplayNameChange(i: Settings, event: any) {
1199 s => ((s.saveUserSettingsForm.display_name = event.target.value), s),
1203 handleMatrixUserIdChange(i: Settings, event: any) {
1205 s => ((s.saveUserSettingsForm.matrix_user_id = event.target.value), s),
1209 handleNewPasswordChange(i: Settings, event: any) {
1210 const newPass: string | undefined =
1211 event.target.value === "" ? undefined : event.target.value;
1212 i.setState(s => ((s.changePasswordForm.new_password = newPass), s));
1215 handleNewPasswordVerifyChange(i: Settings, event: any) {
1216 const newPassVerify: string | undefined =
1217 event.target.value === "" ? undefined : event.target.value;
1219 s => ((s.changePasswordForm.new_password_verify = newPassVerify), s),
1223 handleOldPasswordChange(i: Settings, event: any) {
1224 const oldPass: string | undefined =
1225 event.target.value === "" ? undefined : event.target.value;
1226 i.setState(s => ((s.changePasswordForm.old_password = oldPass), s));
1229 async handleSaveSettingsSubmit(i: Settings, event: any) {
1230 event.preventDefault();
1231 i.setState({ saveRes: { state: "loading" } });
1233 const saveRes = await HttpService.client.saveUserSettings({
1234 ...i.state.saveUserSettingsForm,
1235 auth: myAuthRequired(),
1238 if (saveRes.state === "success") {
1239 UserService.Instance.login({
1243 toast(I18NextService.i18n.t("saved"));
1244 window.scrollTo(0, 0);
1247 i.setState({ saveRes });
1250 async handleChangePasswordSubmit(i: Settings, event: any) {
1251 event.preventDefault();
1252 const { new_password, new_password_verify, old_password } =
1253 i.state.changePasswordForm;
1255 if (new_password && old_password && new_password_verify) {
1256 i.setState({ changePasswordRes: { state: "loading" } });
1257 const changePasswordRes = await HttpService.client.changePassword({
1259 new_password_verify,
1261 auth: myAuthRequired(),
1263 if (changePasswordRes.state === "success") {
1264 UserService.Instance.login({
1265 res: changePasswordRes.data,
1268 window.scrollTo(0, 0);
1269 toast(I18NextService.i18n.t("password_changed"));
1272 i.setState({ changePasswordRes });
1276 handleDeleteAccountShowConfirmToggle(i: Settings) {
1277 i.setState({ deleteAccountShowConfirm: !i.state.deleteAccountShowConfirm });
1280 handleDeleteAccountPasswordChange(i: Settings, event: any) {
1281 i.setState(s => ((s.deleteAccountForm.password = event.target.value), s));
1284 async handleDeleteAccount(i: Settings, event: Event) {
1285 event.preventDefault();
1286 const password = i.state.deleteAccountForm.password;
1288 i.setState({ deleteAccountRes: { state: "loading" } });
1289 const deleteAccountRes = await HttpService.client.deleteAccount({
1291 auth: myAuthRequired(),
1293 if (deleteAccountRes.state === "success") {
1294 UserService.Instance.logout();
1295 this.context.router.history.replace("/");
1298 i.setState({ deleteAccountRes });
1302 handleSwitchTab(i: { ctx: Settings; tab: string }) {
1303 i.ctx.setState({ currentTab: i.tab });
1306 personBlock(res: RequestState<BlockPersonResponse>) {
1307 if (res.state === "success") {
1308 updatePersonBlock(res.data);
1309 const mui = UserService.Instance.myUserInfo;
1311 this.setState({ personBlocks: mui.person_blocks });
1316 communityBlock(res: RequestState<BlockCommunityResponse>) {
1317 if (res.state === "success") {
1318 updateCommunityBlock(res.data);
1319 const mui = UserService.Instance.myUserInfo;
1321 this.setState({ communityBlocks: mui.community_blocks });