summaryrefslogtreecommitdiff
path: root/web/source/settings/views/user
diff options
context:
space:
mode:
Diffstat (limited to 'web/source/settings/views/user')
-rw-r--r--web/source/settings/views/user/migration.tsx208
-rw-r--r--web/source/settings/views/user/profile.tsx279
-rw-r--r--web/source/settings/views/user/routes.tsx80
-rw-r--r--web/source/settings/views/user/settings.tsx169
4 files changed, 736 insertions, 0 deletions
diff --git a/web/source/settings/views/user/migration.tsx b/web/source/settings/views/user/migration.tsx
new file mode 100644
index 000000000..69aae6059
--- /dev/null
+++ b/web/source/settings/views/user/migration.tsx
@@ -0,0 +1,208 @@
+/*
+ GoToSocial
+ Copyright (C) GoToSocial Authors admin@gotosocial.org
+ SPDX-License-Identifier: AGPL-3.0-or-later
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import React from "react";
+
+import FormWithData from "../../lib/form/form-with-data";
+
+import { useVerifyCredentialsQuery } from "../../lib/query/oauth";
+import { useArrayInput, useTextInput } from "../../lib/form";
+import { TextInput } from "../../components/form/inputs";
+import useFormSubmit from "../../lib/form/submit";
+import MutationButton from "../../components/form/mutation-button";
+import { useAliasAccountMutation, useMoveAccountMutation } from "../../lib/query/user";
+import { FormContext, useWithFormContext } from "../../lib/form/context";
+import { store } from "../../redux/store";
+
+export default function UserMigration() {
+ return (
+ <FormWithData
+ dataQuery={useVerifyCredentialsQuery}
+ DataForm={UserMigrationForm}
+ />
+ );
+}
+
+function UserMigrationForm({ data: profile }) {
+ return (
+ <>
+ <h2>Account Migration Settings</h2>
+ <p>
+ The following settings allow you to <strong>alias</strong> your account to
+ another account elsewhere, or to <strong>move</strong> to another account.
+ </p>
+ <p>
+ Account <strong>aliasing</strong> is harmless and reversible; you can
+ set and unset up to five account aliases as many times as you wish.
+ </p>
+ <p>
+ The account <strong>move</strong> action, on the other
+ hand, has serious and irreversible consequences.
+ </p>
+ <p>
+ For more information on account migration, please see <a href="https://docs.gotosocial.org/en/latest/user_guide/settings/#migration" target="_blank" className="docslink" rel="noreferrer">the documentation</a>.
+ </p>
+ <AliasForm data={profile} />
+ <MoveForm data={profile} />
+ </>
+ );
+}
+
+function AliasForm({ data: profile }) {
+ const form = {
+ alsoKnownAs: useArrayInput("also_known_as_uris", {
+ source: profile,
+ valueSelector: (p) => (
+ p.source?.also_known_as_uris
+ ? p.source?.also_known_as_uris.map(entry => [entry])
+ : []
+ ),
+ length: 5,
+ }),
+ };
+
+ const [submitForm, result] = useFormSubmit(form, useAliasAccountMutation());
+
+ return (
+ <form className="user-migration-alias" onSubmit={submitForm}>
+ <div className="form-section-docs">
+ <h3>Alias Account</h3>
+ <a
+ href="https://docs.gotosocial.org/en/latest/user_guide/settings/#alias-account"
+ target="_blank"
+ className="docslink"
+ rel="noreferrer"
+ >
+ Learn more about account aliasing (opens in a new tab)
+ </a>
+ </div>
+ <AlsoKnownAsURIs
+ field={form.alsoKnownAs}
+ />
+ <MutationButton
+ disabled={false}
+ label="Save account aliases"
+ result={result}
+ />
+ </form>
+ );
+}
+
+function AlsoKnownAsURIs({ field: formField }) {
+ return (
+ <div className="aliases">
+ <FormContext.Provider value={formField.ctx}>
+ {formField.value.map((data, i) => (
+ <AlsoKnownAsURI
+ key={i}
+ index={i}
+ data={data}
+ />
+ ))}
+ </FormContext.Provider>
+ </div>
+ );
+}
+
+function AlsoKnownAsURI({ index, data }) {
+ const name = `${index}`;
+ const form = useWithFormContext(index, {
+ alsoKnownAsURI: useTextInput(
+ name,
+ // Only one field per entry.
+ { defaultValue: data[0] ?? "" },
+ ),
+ });
+
+ return (
+ <TextInput
+ label={`Alias #${index+1}`}
+ field={form.alsoKnownAsURI}
+ placeholder={`https://example.org/users/my_other_account_${index+1}`}
+ />
+ );
+}
+
+function MoveForm({ data: profile }) {
+ let urlStr = store.getState().oauth.instanceUrl ?? "";
+ let url = new URL(urlStr);
+
+ const form = {
+ movedToURI: useTextInput("moved_to_uri", {
+ source: profile,
+ valueSelector: (p) => p.moved?.url },
+ ),
+ password: useTextInput("password"),
+ };
+
+ const [submitForm, result] = useFormSubmit(form, useMoveAccountMutation(), {
+ changedOnly: false,
+ });
+
+ return (
+ <form className="user-migration-move" onSubmit={submitForm}>
+ <div className="form-section-docs">
+ <h3>Move Account</h3>
+ <p>
+ For a move to be successful, you must have already set an alias from the
+ target account back to the account you're moving from (ie., this account),
+ using the settings panel of the instance on which the target account resides.
+ To do this, provide the following details to the other instance:
+ </p>
+ <dl className="migration-details">
+ <div>
+ <dt>Account handle/username:</dt>
+ <dd>@{profile.acct}@{url.host}</dd>
+ </div>
+ <div>
+ <dt>Account URI:</dt>
+ <dd>{urlStr}/users/{profile.username}</dd>
+ </div>
+ </dl>
+ <br/>
+ <a
+ href="https://docs.gotosocial.org/en/latest/user_guide/settings/#move-account"
+ target="_blank"
+ className="docslink"
+ rel="noreferrer"
+ >
+ Learn more about moving your account (opens in a new tab)
+ </a>
+ </div>
+ <TextInput
+ disabled={false}
+ field={form.movedToURI}
+ label="Move target URI"
+ placeholder="https://example.org/users/my_new_account"
+ />
+ <TextInput
+ disabled={false}
+ type="password"
+ name="password"
+ field={form.password}
+ label="Current account password"
+ />
+ <MutationButton
+ disabled={false}
+ label="Confirm account move"
+ result={result}
+ />
+ </form>
+ );
+}
diff --git a/web/source/settings/views/user/profile.tsx b/web/source/settings/views/user/profile.tsx
new file mode 100644
index 000000000..08cd74bda
--- /dev/null
+++ b/web/source/settings/views/user/profile.tsx
@@ -0,0 +1,279 @@
+/*
+ GoToSocial
+ Copyright (C) GoToSocial Authors admin@gotosocial.org
+ SPDX-License-Identifier: AGPL-3.0-or-later
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import React from "react";
+
+import {
+ useTextInput,
+ useFileInput,
+ useBoolInput,
+ useFieldArrayInput,
+ useRadioInput
+} from "../../lib/form";
+
+import useFormSubmit from "../../lib/form/submit";
+import { useWithFormContext, FormContext } from "../../lib/form/context";
+
+import {
+ TextInput,
+ TextArea,
+ FileInput,
+ Checkbox,
+ RadioGroup
+} from "../../components/form/inputs";
+
+import FormWithData from "../../lib/form/form-with-data";
+import FakeProfile from "../../components/fake-profile";
+import MutationButton from "../../components/form/mutation-button";
+
+import { useAccountThemesQuery, useInstanceV1Query } from "../../lib/query";
+import { useUpdateCredentialsMutation } from "../../lib/query/user";
+import { useVerifyCredentialsQuery } from "../../lib/query/oauth";
+
+export default function UserProfile() {
+ return (
+ <FormWithData
+ dataQuery={useVerifyCredentialsQuery}
+ DataForm={UserProfileForm}
+ />
+ );
+}
+
+function UserProfileForm({ data: profile }) {
+ /*
+ User profile update form keys
+ - bool bot
+ - bool locked
+ - string display_name
+ - string note
+ - file avatar
+ - file header
+ - bool enable_rss
+ - bool hide_collections
+ - string custom_css (if enabled)
+ - string theme
+ */
+
+ const { data: instance } = useInstanceV1Query();
+ const instanceConfig = React.useMemo(() => {
+ return {
+ allowCustomCSS: instance?.configuration?.accounts?.allow_custom_css === true,
+ maxPinnedFields: instance?.configuration?.accounts?.max_profile_fields ?? 6
+ };
+ }, [instance]);
+
+ // Parse out available theme options into nice format.
+ const { data: themes } = useAccountThemesQuery();
+ let themeOptions = { "": "Default" };
+ themes?.forEach((theme) => {
+ let key = theme.file_name;
+ let value = theme.title;
+ if (theme.description) {
+ value += " - " + theme.description;
+ }
+ themeOptions[key] = value;
+ });
+
+ const form = {
+ avatar: useFileInput("avatar", { withPreview: true }),
+ header: useFileInput("header", { withPreview: true }),
+ displayName: useTextInput("display_name", { source: profile }),
+ note: useTextInput("note", { source: profile, valueSelector: (p) => p.source?.note }),
+ bot: useBoolInput("bot", { source: profile }),
+ locked: useBoolInput("locked", { source: profile }),
+ discoverable: useBoolInput("discoverable", { source: profile}),
+ enableRSS: useBoolInput("enable_rss", { source: profile }),
+ hideCollections: useBoolInput("hide_collections", { source: profile }),
+ fields: useFieldArrayInput("fields_attributes", {
+ defaultValue: profile?.source?.fields,
+ length: instanceConfig.maxPinnedFields
+ }),
+ customCSS: useTextInput("custom_css", { source: profile, nosubmit: !instanceConfig.allowCustomCSS }),
+ theme: useRadioInput("theme", {
+ source: profile,
+ options: themeOptions,
+ }),
+ };
+
+ const [submitForm, result] = useFormSubmit(form, useUpdateCredentialsMutation(), {
+ changedOnly: true,
+ onFinish: () => {
+ form.avatar.reset();
+ form.header.reset();
+ }
+ });
+
+ return (
+ <form className="user-profile" onSubmit={submitForm}>
+ <h1>Profile</h1>
+ <div className="overview">
+ <FakeProfile
+ avatar={form.avatar.previewValue ?? profile.avatar}
+ header={form.header.previewValue ?? profile.header}
+ display_name={form.displayName.value ?? profile.username}
+ username={profile.username}
+ role={profile.role}
+ />
+ <div className="files">
+ <div>
+ <FileInput
+ label="Header"
+ field={form.header}
+ accept="image/*"
+ />
+ </div>
+ <div>
+ <FileInput
+ label="Avatar"
+ field={form.avatar}
+ accept="image/*"
+ />
+ </div>
+ </div>
+
+ <div className="theme">
+ <div>
+ <b id="theme-label">Theme</b>
+ <br/>
+ <span>After choosing theme and saving, <a href={profile.url} target="_blank">open your profile</a> and refresh to see changes.</span>
+ </div>
+ <RadioGroup
+ aria-labelledby="theme-label"
+ field={form.theme}
+ />
+ </div>
+ </div>
+
+ <div className="form-section-docs">
+ <h3>Basic Information</h3>
+ <a
+ href="https://docs.gotosocial.org/en/latest/user_guide/settings/#basic-information"
+ target="_blank"
+ className="docslink"
+ rel="noreferrer"
+ >
+ Learn more about these settings (opens in a new tab)
+ </a>
+ </div>
+ <TextInput
+ field={form.displayName}
+ label="Display name"
+ placeholder="A GoToSocial user"
+ />
+ <TextArea
+ field={form.note}
+ label="Bio"
+ placeholder="Just trying out GoToSocial, my pronouns are they/them and I like sloths."
+ rows={8}
+ />
+ <b>Profile fields</b>
+ <ProfileFields
+ field={form.fields}
+ />
+
+ <div className="form-section-docs">
+ <h3>Visibility and privacy</h3>
+ <a
+ href="https://docs.gotosocial.org/en/latest/user_guide/settings/#visibility-and-privacy"
+ target="_blank"
+ className="docslink"
+ rel="noreferrer"
+ >
+ Learn more about these settings (opens in a new tab)
+ </a>
+ </div>
+ <Checkbox
+ field={form.locked}
+ label="Manually approve follow requests"
+ />
+ <Checkbox
+ field={form.discoverable}
+ label="Mark account as discoverable by search engines and directories"
+ />
+ <Checkbox
+ field={form.enableRSS}
+ label="Enable RSS feed of Public posts"
+ />
+ <Checkbox
+ field={form.hideCollections}
+ label="Hide who you follow / are followed by"
+ />
+
+ <div className="form-section-docs">
+ <h3>Advanced</h3>
+ <a
+ href="https://docs.gotosocial.org/en/latest/user_guide/settings/#advanced"
+ target="_blank"
+ className="docslink"
+ rel="noreferrer"
+ >
+ Learn more about these settings (opens in a new tab)
+ </a>
+ </div>
+ <TextArea
+ field={form.customCSS}
+ label={`Custom CSS` + (!instanceConfig.allowCustomCSS ? ` (not enabled on this instance)` : ``)}
+ className="monospace"
+ rows={8}
+ disabled={!instanceConfig.allowCustomCSS}
+ />
+ <MutationButton
+ disabled={false}
+ label="Save profile info"
+ result={result}
+ />
+ </form>
+ );
+}
+
+function ProfileFields({ field: formField }) {
+ return (
+ <div className="fields">
+ <FormContext.Provider value={formField.ctx}>
+ {formField.value.map((data, i) => (
+ <Field
+ key={i}
+ index={i}
+ data={data}
+ />
+ ))}
+ </FormContext.Provider>
+ </div>
+ );
+}
+
+function Field({ index, data }) {
+ const form = useWithFormContext(index, {
+ name: useTextInput("name", { defaultValue: data.name }),
+ value: useTextInput("value", { defaultValue: data.value })
+ });
+
+ return (
+ <div className="entry">
+ <TextInput
+ field={form.name}
+ placeholder="Name"
+ />
+ <TextInput
+ field={form.value}
+ placeholder="Value"
+ />
+ </div>
+ );
+}
diff --git a/web/source/settings/views/user/routes.tsx b/web/source/settings/views/user/routes.tsx
new file mode 100644
index 000000000..76ac50bc2
--- /dev/null
+++ b/web/source/settings/views/user/routes.tsx
@@ -0,0 +1,80 @@
+/*
+ GoToSocial
+ Copyright (C) GoToSocial Authors admin@gotosocial.org
+ SPDX-License-Identifier: AGPL-3.0-or-later
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import { MenuItem } from "../../lib/navigation/menu";
+import React from "react";
+import { BaseUrlContext, useBaseUrl } from "../../lib/navigation/util";
+import UserProfile from "./profile";
+import UserSettings from "./settings";
+import UserMigration from "./migration";
+import { Redirect, Route, Router, Switch } from "wouter";
+
+/**
+ *
+ * Basic user menu. Profile + accounts
+ * settings, post settings, migration.
+ */
+export function UserMenu() {
+ return (
+ <MenuItem
+ name="User"
+ itemUrl="user"
+ defaultChild="profile"
+ >
+ {/* Profile */}
+ <MenuItem
+ name="Profile"
+ itemUrl="profile"
+ icon="fa-user"
+ />
+ {/* Settings */}
+ <MenuItem
+ name="Settings"
+ itemUrl="settings"
+ icon="fa-cogs"
+ />
+ {/* Migration */}
+ <MenuItem
+ name="Migration"
+ itemUrl="migration"
+ icon="fa-exchange"
+ />
+ </MenuItem>
+ );
+}
+
+export function UserRouter() {
+ const baseUrl = useBaseUrl();
+ const thisBase = "/user";
+ const absBase = baseUrl + thisBase;
+
+ return (
+ <BaseUrlContext.Provider value={absBase}>
+ <Router base={thisBase}>
+ <Switch>
+ <Route path="/profile" component={UserProfile} />
+ <Route path="/settings" component={UserSettings} />
+ <Route path="/migration" component={UserMigration} />
+ {/* Fallback component */}
+ <Route><Redirect to="/profile" /></Route>
+ </Switch>
+ </Router>
+ </BaseUrlContext.Provider>
+ );
+}
diff --git a/web/source/settings/views/user/settings.tsx b/web/source/settings/views/user/settings.tsx
new file mode 100644
index 000000000..2827cc53f
--- /dev/null
+++ b/web/source/settings/views/user/settings.tsx
@@ -0,0 +1,169 @@
+/*
+ GoToSocial
+ Copyright (C) GoToSocial Authors admin@gotosocial.org
+ SPDX-License-Identifier: AGPL-3.0-or-later
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import React from "react";
+import query from "../../lib/query";
+import { useTextInput, useBoolInput } from "../../lib/form";
+import useFormSubmit from "../../lib/form/submit";
+import { Select, TextInput, Checkbox } from "../../components/form/inputs";
+import FormWithData from "../../lib/form/form-with-data";
+import Languages from "../../components/languages";
+import MutationButton from "../../components/form/mutation-button";
+
+export default function UserSettings() {
+ return (
+ <FormWithData
+ dataQuery={query.useVerifyCredentialsQuery}
+ DataForm={UserSettingsForm}
+ />
+ );
+}
+
+function UserSettingsForm({ data }) {
+ /* form keys
+ - string source[privacy]
+ - bool source[sensitive]
+ - string source[language]
+ - string source[status_content_type]
+ */
+
+ const form = {
+ defaultPrivacy: useTextInput("source[privacy]", { source: data, defaultValue: "unlisted" }),
+ isSensitive: useBoolInput("source[sensitive]", { source: data }),
+ language: useTextInput("source[language]", { source: data, valueSelector: (s) => s.source.language?.toUpperCase() ?? "EN" }),
+ statusContentType: useTextInput("source[status_content_type]", { source: data, defaultValue: "text/plain" }),
+ };
+
+ const [submitForm, result] = useFormSubmit(form, query.useUpdateCredentialsMutation());
+
+ return (
+ <>
+ <h1>Account Settings</h1>
+ <form className="user-settings" onSubmit={submitForm}>
+ <div className="form-section-docs">
+ <h3>Post Settings</h3>
+ <a
+ href="https://docs.gotosocial.org/en/latest/user_guide/posts"
+ target="_blank"
+ className="docslink"
+ rel="noreferrer"
+ >
+ Learn more about these settings (opens in a new tab)
+ </a>
+ </div>
+ <Select field={form.language} label="Default post language" options={
+ <Languages />
+ }>
+ </Select>
+ <Select field={form.defaultPrivacy} label="Default post privacy" options={
+ <>
+ <option value="private">Private / followers-only</option>
+ <option value="unlisted">Unlisted</option>
+ <option value="public">Public</option>
+ </>
+ }>
+ </Select>
+ <Select field={form.statusContentType} label="Default post (and bio) format" options={
+ <>
+ <option value="text/plain">Plain (default)</option>
+ <option value="text/markdown">Markdown</option>
+ </>
+ }>
+ </Select>
+ <Checkbox
+ field={form.isSensitive}
+ label="Mark my posts as sensitive by default"
+ />
+ <MutationButton
+ disabled={false}
+ label="Save settings"
+ result={result}
+ />
+ </form>
+ <PasswordChange />
+ </>
+ );
+}
+
+function PasswordChange() {
+ const form = {
+ oldPassword: useTextInput("old_password"),
+ newPassword: useTextInput("new_password", {
+ validator(val) {
+ if (val != "" && val == form.oldPassword.value) {
+ return "New password same as old password";
+ }
+ return "";
+ }
+ })
+ };
+
+ const verifyNewPassword = useTextInput("verifyNewPassword", {
+ validator(val) {
+ if (val != "" && val != form.newPassword.value) {
+ return "Passwords do not match";
+ }
+ return "";
+ }
+ });
+
+ const [submitForm, result] = useFormSubmit(form, query.usePasswordChangeMutation());
+
+ return (
+ <form className="change-password" onSubmit={submitForm}>
+ <div className="form-section-docs">
+ <h3>Change Password</h3>
+ <a
+ href="https://docs.gotosocial.org/en/latest/user_guide/settings/#password-change"
+ target="_blank"
+ className="docslink"
+ rel="noreferrer"
+ >
+ Learn more about this (opens in a new tab)
+ </a>
+ </div>
+ <TextInput
+ type="password"
+ name="password"
+ field={form.oldPassword}
+ label="Current password"
+ autoComplete="current-password"
+ />
+ <TextInput
+ type="password"
+ name="newPassword"
+ field={form.newPassword}
+ label="New password"
+ autoComplete="new-password"
+ />
+ <TextInput
+ type="password"
+ name="confirmNewPassword"
+ field={verifyNewPassword}
+ label="Confirm new password"
+ autoComplete="new-password"
+ />
+ <MutationButton
+ disabled={false}
+ label="Change password"
+ result={result}
+ />
+ </form>
+ );
+}