import { communityToChoice, fetchCommunities, fetchThemeList, fetchUsers, myAuth, myAuthRequired, personToChoice, setIsoData, setTheme, showLocal, updateCommunityBlock, updatePersonBlock, } from "@utils/app"; import { capitalizeFirstLetter, debounce } from "@utils/helpers"; import { Choice } from "@utils/types"; import classNames from "classnames"; import { NoOptionI18nKeys } from "i18next"; import { Component, linkEvent } from "inferno"; import { BlockCommunityResponse, BlockPersonResponse, CommunityBlockView, DeleteAccountResponse, GetSiteResponse, ListingType, LoginResponse, PersonBlockView, SortType, } from "lemmy-js-client"; import { elementUrl, emDash, relTags } from "../../config"; import { UserService } from "../../services"; import { HttpService, RequestState } from "../../services/HttpService"; import { I18NextService, languages } from "../../services/I18NextService"; import { setupTippy } from "../../tippy"; import { toast } from "../../toast"; import { HtmlTags } from "../common/html-tags"; import { Icon, Spinner } from "../common/icon"; import { ImageUploadForm } from "../common/image-upload-form"; import { LanguageSelect } from "../common/language-select"; import { ListingTypeSelect } from "../common/listing-type-select"; import { MarkdownTextArea } from "../common/markdown-textarea"; import PasswordInput from "../common/password-input"; import { SearchableSelect } from "../common/searchable-select"; import { SortSelect } from "../common/sort-select"; import Tabs from "../common/tabs"; import { CommunityLink } from "../community/community-link"; import { PersonListing } from "./person-listing"; interface SettingsState { saveRes: RequestState; changePasswordRes: RequestState; deleteAccountRes: RequestState; // TODO redo these forms saveUserSettingsForm: { show_nsfw?: boolean; theme?: string; default_sort_type?: SortType; default_listing_type?: ListingType; interface_language?: string; avatar?: string; banner?: string; display_name?: string; email?: string; bio?: string; matrix_user_id?: string; show_avatars?: boolean; show_scores?: boolean; send_notifications_to_email?: boolean; bot_account?: boolean; show_bot_accounts?: boolean; show_read_posts?: boolean; show_new_post_notifs?: boolean; discussion_languages?: number[]; generate_totp_2fa?: boolean; open_links_in_new_tab?: boolean; }; changePasswordForm: { new_password?: string; new_password_verify?: string; old_password?: string; }; deleteAccountForm: { password?: string; }; personBlocks: PersonBlockView[]; communityBlocks: CommunityBlockView[]; currentTab: string; themeList: string[]; deleteAccountShowConfirm: boolean; siteRes: GetSiteResponse; searchCommunityLoading: boolean; searchCommunityOptions: Choice[]; searchPersonLoading: boolean; searchPersonOptions: Choice[]; } type FilterType = "user" | "community"; const Filter = ({ filterType, options, onChange, onSearch, loading, }: { filterType: FilterType; options: Choice[]; onSearch: (text: string) => void; onChange: (choice: Choice) => void; loading: boolean; }) => (
); export class Settings extends Component { private isoData = setIsoData(this.context); state: SettingsState = { saveRes: { state: "empty" }, deleteAccountRes: { state: "empty" }, changePasswordRes: { state: "empty" }, saveUserSettingsForm: {}, changePasswordForm: {}, deleteAccountShowConfirm: false, deleteAccountForm: {}, personBlocks: [], communityBlocks: [], currentTab: "settings", siteRes: this.isoData.site_res, themeList: [], searchCommunityLoading: false, searchCommunityOptions: [], searchPersonLoading: false, searchPersonOptions: [], }; constructor(props: any, context: any) { super(props, context); this.handleSortTypeChange = this.handleSortTypeChange.bind(this); this.handleListingTypeChange = this.handleListingTypeChange.bind(this); this.handleBioChange = this.handleBioChange.bind(this); this.handleDiscussionLanguageChange = this.handleDiscussionLanguageChange.bind(this); this.handleAvatarUpload = this.handleAvatarUpload.bind(this); this.handleAvatarRemove = this.handleAvatarRemove.bind(this); this.handleBannerUpload = this.handleBannerUpload.bind(this); this.handleBannerRemove = this.handleBannerRemove.bind(this); this.userSettings = this.userSettings.bind(this); this.blockCards = this.blockCards.bind(this); this.handleBlockPerson = this.handleBlockPerson.bind(this); this.handleBlockCommunity = this.handleBlockCommunity.bind(this); const mui = UserService.Instance.myUserInfo; if (mui) { const { local_user: { show_nsfw, theme, default_sort_type, default_listing_type, interface_language, show_avatars, show_bot_accounts, show_scores, show_read_posts, show_new_post_notifs, send_notifications_to_email, email, }, person: { avatar, banner, display_name, bot_account, bio, matrix_user_id, }, } = mui.local_user_view; this.state = { ...this.state, personBlocks: mui.person_blocks, communityBlocks: mui.community_blocks, saveUserSettingsForm: { ...this.state.saveUserSettingsForm, show_nsfw, theme: theme ?? "browser", default_sort_type, default_listing_type, interface_language, discussion_languages: mui.discussion_languages, avatar, banner, display_name, show_avatars, bot_account, show_bot_accounts, show_scores, show_read_posts, show_new_post_notifs, email, bio, send_notifications_to_email, matrix_user_id, }, }; } } async componentDidMount() { setupTippy(); this.setState({ themeList: await fetchThemeList() }); } get documentTitle(): string { return I18NextService.i18n.t("settings"); } render() { return (
); } userSettings(isSelected: boolean) { return (
{this.saveUserSettingsHtmlForm()}
{this.changePasswordHtmlForm()}
); } blockCards(isSelected: boolean) { return (
{this.blockUserCard()}
{this.blockCommunityCard()}
); } changePasswordHtmlForm() { return ( <>

{I18NextService.i18n.t("change_password")}

); } blockUserCard() { const { searchPersonLoading, searchPersonOptions } = this.state; return (
{this.blockedUsersList()}
); } blockedUsersList() { return ( <>

{I18NextService.i18n.t("blocked_users")}

    {this.state.personBlocks.map(pb => (
  • ))}
); } blockCommunityCard() { const { searchCommunityLoading, searchCommunityOptions } = this.state; return (
{this.blockedCommunitiesList()}
); } blockedCommunitiesList() { return ( <>

{I18NextService.i18n.t("blocked_communities")}

    {this.state.communityBlocks.map(cb => (
  • ))}
); } saveUserSettingsHtmlForm() { const selectedLangs = this.state.saveUserSettingsForm.discussion_languages; return ( <>

{I18NextService.i18n.t("settings")}

{this.totpSection()}

{this.state.deleteAccountShowConfirm && ( <> )} ); } totpSection() { const totpUrl = UserService.Instance.myUserInfo?.local_user_view.local_user.totp_2fa_url; return ( <> {!totpUrl && (
)} {totpUrl && ( <>
)} ); } handlePersonSearch = debounce(async (text: string) => { this.setState({ searchPersonLoading: true }); const searchPersonOptions: Choice[] = []; if (text.length > 0) { searchPersonOptions.push(...(await fetchUsers(text)).map(personToChoice)); } this.setState({ searchPersonLoading: false, searchPersonOptions, }); }); handleCommunitySearch = debounce(async (text: string) => { this.setState({ searchCommunityLoading: true }); const searchCommunityOptions: Choice[] = []; if (text.length > 0) { searchCommunityOptions.push( ...(await fetchCommunities(text)).map(communityToChoice), ); } this.setState({ searchCommunityLoading: false, searchCommunityOptions, }); }); async handleBlockPerson({ value }: Choice) { if (value !== "0") { const res = await HttpService.client.blockPerson({ person_id: Number(value), block: true, auth: myAuthRequired(), }); this.personBlock(res); } } async handleUnblockPerson({ ctx, recipientId, }: { ctx: Settings; recipientId: number; }) { const res = await HttpService.client.blockPerson({ person_id: recipientId, block: false, auth: myAuthRequired(), }); ctx.personBlock(res); } async handleBlockCommunity({ value }: Choice) { if (value !== "0") { const res = await HttpService.client.blockCommunity({ community_id: Number(value), block: true, auth: myAuthRequired(), }); this.communityBlock(res); } } async handleUnblockCommunity(i: { ctx: Settings; communityId: number }) { const auth = myAuth(); if (auth) { const res = await HttpService.client.blockCommunity({ community_id: i.communityId, block: false, auth: myAuthRequired(), }); i.ctx.communityBlock(res); } } handleShowNsfwChange(i: Settings, event: any) { i.setState( s => ((s.saveUserSettingsForm.show_nsfw = event.target.checked), s), ); } handleShowAvatarsChange(i: Settings, event: any) { const mui = UserService.Instance.myUserInfo; if (mui) { mui.local_user_view.local_user.show_avatars = event.target.checked; } i.setState( s => ((s.saveUserSettingsForm.show_avatars = event.target.checked), s), ); } handleBotAccount(i: Settings, event: any) { i.setState( s => ((s.saveUserSettingsForm.bot_account = event.target.checked), s), ); } handleShowBotAccounts(i: Settings, event: any) { i.setState( s => ( (s.saveUserSettingsForm.show_bot_accounts = event.target.checked), s ), ); } handleReadPosts(i: Settings, event: any) { i.setState( s => ((s.saveUserSettingsForm.show_read_posts = event.target.checked), s), ); } handleShowNewPostNotifs(i: Settings, event: any) { i.setState( s => ( (s.saveUserSettingsForm.show_new_post_notifs = event.target.checked), s ), ); } handleOpenInNewTab(i: Settings, event: any) { i.setState( s => ( (s.saveUserSettingsForm.open_links_in_new_tab = event.target.checked), s ), ); } handleShowScoresChange(i: Settings, event: any) { const mui = UserService.Instance.myUserInfo; if (mui) { mui.local_user_view.local_user.show_scores = event.target.checked; } i.setState( s => ((s.saveUserSettingsForm.show_scores = event.target.checked), s), ); } handleGenerateTotp(i: Settings, event: any) { // Coerce false to undefined here, so it won't generate it. const checked: boolean | undefined = event.target.checked || undefined; if (checked) { toast(I18NextService.i18n.t("two_factor_setup_instructions")); } i.setState(s => ((s.saveUserSettingsForm.generate_totp_2fa = checked), s)); } handleRemoveTotp(i: Settings, event: any) { // Coerce true to undefined here, so it won't generate it. const checked: boolean | undefined = !event.target.checked && undefined; i.setState(s => ((s.saveUserSettingsForm.generate_totp_2fa = checked), s)); } handleSendNotificationsToEmailChange(i: Settings, event: any) { i.setState( s => ( (s.saveUserSettingsForm.send_notifications_to_email = event.target.checked), s ), ); } handleThemeChange(i: Settings, event: any) { i.setState(s => ((s.saveUserSettingsForm.theme = event.target.value), s)); setTheme(event.target.value, true); } handleInterfaceLangChange(i: Settings, event: any) { const newLang = event.target.value ?? "browser"; I18NextService.i18n.changeLanguage( newLang === "browser" ? navigator.languages : newLang, ); i.setState( s => ( (s.saveUserSettingsForm.interface_language = event.target.value), s ), ); } handleDiscussionLanguageChange(val: number[]) { this.setState( s => ((s.saveUserSettingsForm.discussion_languages = val), s), ); } handleSortTypeChange(val: SortType) { this.setState(s => ((s.saveUserSettingsForm.default_sort_type = val), s)); } handleListingTypeChange(val: ListingType) { this.setState( s => ((s.saveUserSettingsForm.default_listing_type = val), s), ); } handleEmailChange(i: Settings, event: any) { i.setState(s => ((s.saveUserSettingsForm.email = event.target.value), s)); } handleBioChange(val: string) { this.setState(s => ((s.saveUserSettingsForm.bio = val), s)); } handleAvatarUpload(url: string) { this.setState(s => ((s.saveUserSettingsForm.avatar = url), s)); } handleAvatarRemove() { this.setState(s => ((s.saveUserSettingsForm.avatar = ""), s)); } handleBannerUpload(url: string) { this.setState(s => ((s.saveUserSettingsForm.banner = url), s)); } handleBannerRemove() { this.setState(s => ((s.saveUserSettingsForm.banner = ""), s)); } handleDisplayNameChange(i: Settings, event: any) { i.setState( s => ((s.saveUserSettingsForm.display_name = event.target.value), s), ); } handleMatrixUserIdChange(i: Settings, event: any) { i.setState( s => ((s.saveUserSettingsForm.matrix_user_id = event.target.value), s), ); } handleNewPasswordChange(i: Settings, event: any) { const newPass: string | undefined = event.target.value === "" ? undefined : event.target.value; i.setState(s => ((s.changePasswordForm.new_password = newPass), s)); } handleNewPasswordVerifyChange(i: Settings, event: any) { const newPassVerify: string | undefined = event.target.value === "" ? undefined : event.target.value; i.setState( s => ((s.changePasswordForm.new_password_verify = newPassVerify), s), ); } handleOldPasswordChange(i: Settings, event: any) { const oldPass: string | undefined = event.target.value === "" ? undefined : event.target.value; i.setState(s => ((s.changePasswordForm.old_password = oldPass), s)); } async handleSaveSettingsSubmit(i: Settings, event: any) { event.preventDefault(); i.setState({ saveRes: { state: "loading" } }); const saveRes = await HttpService.client.saveUserSettings({ ...i.state.saveUserSettingsForm, auth: myAuthRequired(), }); if (saveRes.state === "success") { UserService.Instance.login({ res: saveRes.data, showToast: false, }); toast(I18NextService.i18n.t("saved")); window.scrollTo(0, 0); } i.setState({ saveRes }); } async handleChangePasswordSubmit(i: Settings, event: any) { event.preventDefault(); const { new_password, new_password_verify, old_password } = i.state.changePasswordForm; if (new_password && old_password && new_password_verify) { i.setState({ changePasswordRes: { state: "loading" } }); const changePasswordRes = await HttpService.client.changePassword({ new_password, new_password_verify, old_password, auth: myAuthRequired(), }); if (changePasswordRes.state === "success") { UserService.Instance.login({ res: changePasswordRes.data, showToast: false, }); window.scrollTo(0, 0); toast(I18NextService.i18n.t("password_changed")); } i.setState({ changePasswordRes }); } } handleDeleteAccountShowConfirmToggle(i: Settings) { i.setState({ deleteAccountShowConfirm: !i.state.deleteAccountShowConfirm }); } handleDeleteAccountPasswordChange(i: Settings, event: any) { i.setState(s => ((s.deleteAccountForm.password = event.target.value), s)); } async handleDeleteAccount(i: Settings, event: Event) { event.preventDefault(); const password = i.state.deleteAccountForm.password; if (password) { i.setState({ deleteAccountRes: { state: "loading" } }); const deleteAccountRes = await HttpService.client.deleteAccount({ password, auth: myAuthRequired(), }); if (deleteAccountRes.state === "success") { UserService.Instance.logout(); this.context.router.history.replace("/"); } i.setState({ deleteAccountRes }); } } handleSwitchTab(i: { ctx: Settings; tab: string }) { i.ctx.setState({ currentTab: i.tab }); } personBlock(res: RequestState) { if (res.state === "success") { updatePersonBlock(res.data); const mui = UserService.Instance.myUserInfo; if (mui) { this.setState({ personBlocks: mui.person_blocks }); } } } communityBlock(res: RequestState) { if (res.state === "success") { updateCommunityBlock(res.data); const mui = UserService.Instance.myUserInfo; if (mui) { this.setState({ communityBlocks: mui.community_blocks }); } } } }