import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import {
BlockCommunity,
BlockCommunityResponse,
BlockPerson,
BlockPersonResponse,
ChangePassword,
CommunityBlockView,
DeleteAccount,
GetSiteResponse,
ListingType,
LoginResponse,
PersonBlockView,
SaveUserSettings,
SortType,
UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client";
import { Subscription } from "rxjs";
import { i18n, languages } from "../../i18next";
import { UserService, WebSocketService } from "../../services";
import {
capitalizeFirstLetter,
Choice,
communityToChoice,
debounce,
elementUrl,
emDash,
enableNsfw,
fetchCommunities,
fetchThemeList,
fetchUsers,
getLanguages,
myAuth,
personToChoice,
relTags,
setIsoData,
setTheme,
setupTippy,
showLocal,
toast,
updateCommunityBlock,
updatePersonBlock,
wsClient,
wsSubscribe,
} from "../../utils";
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 { SearchableSelect } from "../common/searchable-select";
import { SortSelect } from "../common/sort-select";
import { CommunityLink } from "../community/community-link";
import { PersonListing } from "./person-listing";
interface SettingsState {
// TODO redo these forms
saveUserSettingsForm: {
show_nsfw?: boolean;
theme?: string;
default_sort_type?: number;
default_listing_type?: number;
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;
};
changePasswordForm: {
new_password?: string;
new_password_verify?: string;
old_password?: string;
};
deleteAccountForm: {
password?: string;
};
personBlocks: PersonBlockView[];
communityBlocks: CommunityBlockView[];
currentTab: string;
themeList: string[];
saveUserSettingsLoading: boolean;
changePasswordLoading: boolean;
deleteAccountLoading: boolean;
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);
private subscription?: Subscription;
state: SettingsState = {
saveUserSettingsForm: {},
changePasswordForm: {},
saveUserSettingsLoading: false,
changePasswordLoading: false,
deleteAccountLoading: false,
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.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
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() });
}
componentWillUnmount() {
this.subscription?.unsubscribe();
}
get documentTitle(): string {
return i18n.t("settings");
}
render() {
return (
<>
-
-
{this.state.currentTab == "settings" && this.userSettings()}
{this.state.currentTab == "blocks" && this.blockCards()}
>
);
}
userSettings() {
return (
{this.saveUserSettingsHtmlForm()}
{this.changePasswordHtmlForm()}
);
}
blockCards() {
return (
{this.blockCommunityCard()}
);
}
changePasswordHtmlForm() {
return (
<>
{i18n.t("change_password")}
>
);
}
blockUserCard() {
const { searchPersonLoading, searchPersonOptions } = this.state;
return (
{this.blockedUsersList()}
);
}
blockedUsersList() {
return (
<>
{i18n.t("blocked_users")}
{this.state.personBlocks.map(pb => (
-
))}
>
);
}
blockCommunityCard() {
const { searchCommunityLoading, searchCommunityOptions } = this.state;
return (
{this.blockedCommunitiesList()}
);
}
blockedCommunitiesList() {
return (
<>
{i18n.t("blocked_communities")}
{this.state.communityBlocks.map(cb => (
-
))}
>
);
}
saveUserSettingsHtmlForm() {
let selectedLangs = this.state.saveUserSettingsForm.discussion_languages;
return (
<>
{i18n.t("settings")}
{enableNsfw(this.state.siteRes) && (
)}
{this.totpSection()}
{this.state.deleteAccountShowConfirm && (
<>
{i18n.t("delete_account_confirm")}
>
)}
>
);
}
totpSection() {
let 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)).users.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)).communities.map(communityToChoice)
);
}
this.setState({
searchCommunityLoading: false,
searchCommunityOptions,
});
});
handleBlockPerson({ value }: Choice) {
const auth = myAuth();
if (auth && value !== "0") {
const blockUserForm: BlockPerson = {
person_id: Number(value),
block: true,
auth,
};
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
}
}
handleUnblockPerson(i: { ctx: Settings; recipientId: number }) {
const auth = myAuth();
if (auth) {
const blockUserForm: BlockPerson = {
person_id: i.recipientId,
block: false,
auth,
};
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
}
}
handleBlockCommunity({ value }: Choice) {
const auth = myAuth();
if (auth && value !== "0") {
const blockCommunityForm: BlockCommunity = {
community_id: Number(value),
block: true,
auth,
};
WebSocketService.Instance.send(
wsClient.blockCommunity(blockCommunityForm)
);
}
}
handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
const auth = myAuth();
if (auth) {
const blockCommunityForm: BlockCommunity = {
community_id: i.communityId,
block: false,
auth,
};
WebSocketService.Instance.send(
wsClient.blockCommunity(blockCommunityForm)
);
}
}
handleShowNsfwChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_nsfw = event.target.checked;
i.setState(i.state);
}
handleShowAvatarsChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_avatars = event.target.checked;
let mui = UserService.Instance.myUserInfo;
if (mui) {
mui.local_user_view.local_user.show_avatars = event.target.checked;
}
i.setState(i.state);
}
handleBotAccount(i: Settings, event: any) {
i.state.saveUserSettingsForm.bot_account = event.target.checked;
i.setState(i.state);
}
handleShowBotAccounts(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_bot_accounts = event.target.checked;
i.setState(i.state);
}
handleReadPosts(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_read_posts = event.target.checked;
i.setState(i.state);
}
handleShowNewPostNotifs(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_new_post_notifs = event.target.checked;
i.setState(i.state);
}
handleShowScoresChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_scores = event.target.checked;
let mui = UserService.Instance.myUserInfo;
if (mui) {
mui.local_user_view.local_user.show_scores = event.target.checked;
}
i.setState(i.state);
}
handleGenerateTotp(i: Settings, event: any) {
// Coerce false to undefined here, so it won't generate it.
let checked: boolean | undefined = event.target.checked || undefined;
if (checked) {
toast(i18n.t("two_factor_setup_instructions"));
}
i.state.saveUserSettingsForm.generate_totp_2fa = checked;
i.setState(i.state);
}
handleRemoveTotp(i: Settings, event: any) {
// Coerce true to undefined here, so it won't generate it.
let checked: boolean | undefined = !event.target.checked && undefined;
i.state.saveUserSettingsForm.generate_totp_2fa = checked;
i.setState(i.state);
}
handleSendNotificationsToEmailChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.send_notifications_to_email =
event.target.checked;
i.setState(i.state);
}
handleThemeChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.theme = event.target.value;
setTheme(event.target.value, true);
i.setState(i.state);
}
handleInterfaceLangChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.interface_language = event.target.value;
i18n.changeLanguage(
getLanguages(i.state.saveUserSettingsForm.interface_language).at(0)
);
i.setState(i.state);
}
handleDiscussionLanguageChange(val: number[]) {
this.setState(
s => ((s.saveUserSettingsForm.discussion_languages = val), s)
);
}
handleSortTypeChange(val: SortType) {
this.setState(
s => (
(s.saveUserSettingsForm.default_sort_type =
Object.keys(SortType).indexOf(val)),
s
)
);
}
handleListingTypeChange(val: ListingType) {
this.setState(
s => (
(s.saveUserSettingsForm.default_listing_type =
Object.keys(ListingType).indexOf(val)),
s
)
);
}
handleEmailChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.email = event.target.value;
i.setState(i.state);
}
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.state.saveUserSettingsForm.display_name = event.target.value;
i.setState(i.state);
}
handleMatrixUserIdChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.matrix_user_id = event.target.value;
i.setState(i.state);
}
handleNewPasswordChange(i: Settings, event: any) {
i.state.changePasswordForm.new_password = event.target.value;
if (i.state.changePasswordForm.new_password == "") {
i.state.changePasswordForm.new_password = undefined;
}
i.setState(i.state);
}
handleNewPasswordVerifyChange(i: Settings, event: any) {
i.state.changePasswordForm.new_password_verify = event.target.value;
if (i.state.changePasswordForm.new_password_verify == "") {
i.state.changePasswordForm.new_password_verify = undefined;
}
i.setState(i.state);
}
handleOldPasswordChange(i: Settings, event: any) {
i.state.changePasswordForm.old_password = event.target.value;
if (i.state.changePasswordForm.old_password == "") {
i.state.changePasswordForm.old_password = undefined;
}
i.setState(i.state);
}
handleSaveSettingsSubmit(i: Settings, event: any) {
event.preventDefault();
i.setState({ saveUserSettingsLoading: true });
let auth = myAuth();
if (auth) {
let form: SaveUserSettings = { ...i.state.saveUserSettingsForm, auth };
WebSocketService.Instance.send(wsClient.saveUserSettings(form));
}
}
handleChangePasswordSubmit(i: Settings, event: any) {
event.preventDefault();
i.setState({ changePasswordLoading: true });
let auth = myAuth();
let pForm = i.state.changePasswordForm;
let new_password = pForm.new_password;
let new_password_verify = pForm.new_password_verify;
let old_password = pForm.old_password;
if (auth && new_password && old_password && new_password_verify) {
let form: ChangePassword = {
new_password,
new_password_verify,
old_password,
auth,
};
WebSocketService.Instance.send(wsClient.changePassword(form));
}
}
handleDeleteAccountShowConfirmToggle(i: Settings, event: any) {
event.preventDefault();
i.setState({ deleteAccountShowConfirm: !i.state.deleteAccountShowConfirm });
}
handleDeleteAccountPasswordChange(i: Settings, event: any) {
i.state.deleteAccountForm.password = event.target.value;
i.setState(i.state);
}
handleDeleteAccount(i: Settings, event: any) {
event.preventDefault();
i.setState({ deleteAccountLoading: true });
let auth = myAuth();
let password = i.state.deleteAccountForm.password;
if (auth && password) {
let form: DeleteAccount = {
password,
auth,
};
WebSocketService.Instance.send(wsClient.deleteAccount(form));
}
}
handleSwitchTab(i: { ctx: Settings; tab: string }) {
i.ctx.setState({ currentTab: i.tab });
}
parseMessage(msg: any) {
let op = wsUserOp(msg);
console.log(msg);
if (msg.error) {
this.setState({
saveUserSettingsLoading: false,
changePasswordLoading: false,
deleteAccountLoading: false,
});
toast(i18n.t(msg.error), "danger");
return;
} else if (op == UserOperation.SaveUserSettings) {
let data = wsJsonToRes(msg);
UserService.Instance.login(data);
location.reload();
this.setState({ saveUserSettingsLoading: false });
toast(i18n.t("saved"));
window.scrollTo(0, 0);
} else if (op == UserOperation.ChangePassword) {
let data = wsJsonToRes(msg);
UserService.Instance.login(data);
this.setState({ changePasswordLoading: false });
window.scrollTo(0, 0);
toast(i18n.t("password_changed"));
} else if (op == UserOperation.DeleteAccount) {
this.setState({
deleteAccountLoading: false,
deleteAccountShowConfirm: false,
});
UserService.Instance.logout();
window.location.href = "/";
} else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes(msg);
updatePersonBlock(data);
let mui = UserService.Instance.myUserInfo;
if (mui) {
this.setState({ personBlocks: mui.person_blocks });
}
} else if (op == UserOperation.BlockCommunity) {
let data = wsJsonToRes(msg);
updateCommunityBlock(data);
let mui = UserService.Instance.myUserInfo;
if (mui) {
this.setState({ communityBlocks: mui.community_blocks });
}
}
}
}