[frogend] Settings refactor (#1318)

* yakshave new form field structure

* fully refactor user profile settings form

* use rtk query api for profile settings

* refactor user post settings

* refactor password change form

* refactor admin settings

* FormWithData structure for user forms

* admin actions refactor

* whitespace

* fix user settings data prop

* remove superfluous logging

* cleanup old code

* refactor federation/suspend (overview, detail)

* mostly abstracted (emoji) checkbox list

* refactor parse-from-toot

* refactor custom-emoji, progress on federation bulk

* loading icon styling to prevent big spinny

* refactor federation import-export interface

* cleanup old files

* [chore] Update/add license headers for 2023

* redux fixes

* text-field exports

* appease the linter

* refactor authentication with RTK Query

* fix login/logout state transition weirdness

* fixes/cleanup

* small linter-related fixes

* add eslint license header check, fix existing files

* remove old code, clarify comment

* clarify suspend on subdomains

* collapse if/else

* fa-fw width info comment
This commit is contained in:
f0x52 2023-01-18 14:45:14 +01:00 committed by GitHub
commit 9b139b6320
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 3129 additions and 2663 deletions

View file

@ -19,88 +19,126 @@
"use strict";
const React = require("react");
const Redux = require("react-redux");
const Submit = require("../components/submit");
const query = require("../lib/query");
const api = require("../lib/api");
const user = require("../redux/reducers/user").actions;
const submit = require("../lib/submit");
const {
useTextInput,
useFileInput,
useBoolInput
} = require("../lib/form");
const FakeProfile = require("../components/fake-profile");
const { formFields } = require("../components/form-fields");
const useFormSubmit = require("../lib/form/submit");
const {
TextInput,
TextArea,
Checkbox,
File
} = formFields(user.setProfileVal, (state) => state.user.profile);
FileInput,
Checkbox
} = require("../components/form/inputs");
const FormWithData = require("../lib/form/form-with-data");
const FakeProfile = require("../components/fake-profile");
const MutationButton = require("../components/form/mutation-button");
module.exports = function UserProfile() {
const dispatch = Redux.useDispatch();
const instance = Redux.useSelector(state => state.instances.current);
const allowCustomCSS = instance.configuration.accounts.allow_custom_css;
const [errorMsg, setError] = React.useState("");
const [statusMsg, setStatus] = React.useState("");
const saveProfile = submit(
() => dispatch(api.user.updateProfile()),
{setStatus, setError}
return (
<FormWithData
dataQuery={query.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
- string custom_css (if enabled)
*/
const { data: instance } = query.useInstanceQuery();
const allowCustomCSS = React.useMemo(() => {
return instance?.configuration?.accounts?.allow_custom_css === true;
}, [instance]);
const form = {
avatar: useFileInput("avatar", { withPreview: true }),
header: useFileInput("header", { withPreview: true }),
displayName: useTextInput("display_name", { defaultValue: profile.display_name }),
note: useTextInput("note", { defaultValue: profile.source?.note }),
customCSS: useTextInput("custom_css", { defaultValue: profile.custom_css }),
bot: useBoolInput("bot", { defaultValue: profile.bot }),
locked: useBoolInput("locked", { defaultValue: profile.locked }),
enableRSS: useBoolInput("enable_rss", { defaultValue: profile.enable_rss }),
};
const [submitForm, result] = useFormSubmit(form, query.useUpdateCredentialsMutation());
return (
<div className="user-profile">
<form className="user-profile" onSubmit={submitForm}>
<h1>Profile</h1>
<div className="overview">
<FakeProfile/>
<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>
<h3>Header</h3>
<File
id="header"
fileType="image/*"
<FileInput
field={form.header}
accept="image/*"
/>
</div>
<div>
<h3>Avatar</h3>
<File
id="avatar"
fileType="image/*"
<FileInput
field={form.avatar}
accept="image/*"
/>
</div>
</div>
</div>
<TextInput
id="display_name"
name="Name"
placeHolder="A GoToSocial user"
field={form.displayName}
label="Name"
placeholder="A GoToSocial user"
/>
<TextArea
id="source.note"
name="Bio"
placeHolder="Just trying out GoToSocial, my pronouns are they/them and I like sloths."
field={form.note}
label="Bio"
placeholder="Just trying out GoToSocial, my pronouns are they/them and I like sloths."
rows={8}
/>
<Checkbox
id="locked"
name="Manually approve follow requests"
field={form.locked}
label="Manually approve follow requests"
/>
<Checkbox
id="enable_rss"
name="Enable RSS feed of Public posts"
field={form.enableRSS}
label="Enable RSS feed of Public posts"
/>
{ !allowCustomCSS ? null :
{!allowCustomCSS ? null :
<TextArea
id="custom_css"
name="Custom CSS"
field={form.customCSS}
label="Custom CSS"
className="monospace"
rows={8}
>
<a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about custom profile CSS (opens in a new tab)</a>
</TextArea>
}
<Submit onClick={saveProfile} label="Save profile info" errorMsg={errorMsg} statusMsg={statusMsg} />
</div>
<MutationButton label="Save profile info" result={result} />
</form>
);
};
}

View file

@ -18,42 +18,63 @@
"use strict";
const Promise = require("bluebird");
const React = require("react");
const Redux = require("react-redux");
const api = require("../lib/api");
const user = require("../redux/reducers/user").actions;
const submit = require("../lib/submit");
const Languages = require("../components/languages");
const Submit = require("../components/submit");
const query = require("../lib/query");
const {
useTextInput,
useBoolInput
} = require("../lib/form");
const useFormSubmit = require("../lib/form/submit");
const {
Checkbox,
Select,
} = require("../components/form-fields").formFields(user.setSettingsVal, (state) => state.user.settings);
TextInput,
Checkbox
} = require("../components/form/inputs");
const FormWithData = require("../lib/form/form-with-data");
const Languages = require("../components/languages");
const MutationButton = require("../components/form/mutation-button");
module.exports = function UserSettings() {
const dispatch = Redux.useDispatch();
const [errorMsg, setError] = React.useState("");
const [statusMsg, setStatus] = React.useState("");
const updateSettings = submit(
() => dispatch(api.user.updateSettings()),
{setStatus, setError}
return (
<FormWithData
dataQuery={query.useVerifyCredentialsQuery}
DataForm={UserSettingsForm}
/>
);
};
function UserSettingsForm({ data }) {
const { source } = data;
/* form keys
- string source[privacy]
- bool source[sensitive]
- string source[language]
- string source[status_format]
*/
const form = {
defaultPrivacy: useTextInput("source[privacy]", { defaultValue: source.privacy ?? "unlisted" }),
isSensitive: useBoolInput("source[sensitive]", { defaultValue: source.sensitive }),
language: useTextInput("source[language]", { defaultValue: source.language?.toUpperCase() ?? "EN" }),
format: useTextInput("source[status_format]", { defaultValue: source.status_format ?? "plain" }),
};
const [submitForm, result] = useFormSubmit(form, query.useUpdateCredentialsMutation());
return (
<>
<div className="user-settings">
<form className="user-settings" onSubmit={submitForm}>
<h1>Post settings</h1>
<Select id="source.language" name="Default post language" options={
<Languages/>
<Select field={form.language} label="Default post language" options={
<Languages />
}>
</Select>
<Select id="source.privacy" name="Default post privacy" options={
<Select field={form.defaultPrivacy} label="Default post privacy" options={
<>
<option value="private">Private / followers-only</option>
<option value="unlisted">Unlisted</option>
@ -62,7 +83,7 @@ module.exports = function UserSettings() {
}>
<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#privacy-settings" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about post privacy settings (opens in a new tab)</a>
</Select>
<Select id="source.status_format" name="Default post (and bio) format" options={
<Select field={form.format} label="Default post (and bio) format" options={
<>
<option value="plain">Plain (default)</option>
<option value="markdown">Markdown</option>
@ -71,70 +92,50 @@ module.exports = function UserSettings() {
<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#input-types" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about post format settings (opens in a new tab)</a>
</Select>
<Checkbox
id="source.sensitive"
name="Mark my posts as sensitive by default"
field={form.isSensitive}
label="Mark my posts as sensitive by default"
/>
<Submit onClick={updateSettings} label="Save post settings" errorMsg={errorMsg} statusMsg={statusMsg}/>
</div>
<MutationButton label="Save settings" result={result} />
</form>
<div>
<PasswordChange/>
<PasswordChange />
</div>
</>
);
};
}
function PasswordChange() {
const dispatch = Redux.useDispatch();
const form = {
oldPassword: useTextInput("old_password"),
newPassword: useTextInput("old_password", {
validator(val) {
if (val != "" && val == form.oldPassword.value) {
return "New password same as old password";
}
return "";
}
})
};
const [errorMsg, setError] = React.useState("");
const [statusMsg, setStatus] = React.useState("");
const [oldPassword, setOldPassword] = React.useState("");
const [newPassword, setNewPassword] = React.useState("");
const [newPasswordConfirm, setNewPasswordConfirm] = React.useState("");
function changePassword() {
if (newPassword !== newPasswordConfirm) {
setError("New password and confirm new password did not match!");
return;
const verifyNewPassword = useTextInput("verifyNewPassword", {
validator(val) {
if (val != "" && val != form.newPassword.value) {
return "Passwords do not match";
}
return "";
}
});
setStatus("PATCHing");
setError("");
return Promise.try(() => {
let data = {
old_password: oldPassword,
new_password: newPassword
};
return dispatch(api.apiCall("POST", "/api/v1/user/password_change", data, "form"));
}).then(() => {
setStatus("Saved!");
setOldPassword("");
setNewPassword("");
setNewPasswordConfirm("");
}).catch((e) => {
setError(e.message);
setStatus("");
});
}
const [submitForm, result] = useFormSubmit(form, query.usePasswordChangeMutation());
return (
<>
<form className="change-password" onSubmit={submitForm}>
<h1>Change password</h1>
<div className="labelinput">
<label htmlFor="password">Current password</label>
<input name="password" id="password" type="password" autoComplete="current-password" value={oldPassword} onChange={(e) => setOldPassword(e.target.value)} />
</div>
<div className="labelinput">
<label htmlFor="new-password">New password</label>
<input name="new-password" id="new-password" type="password" autoComplete="new-password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} />
</div>
<div className="labelinput">
<label htmlFor="confirm-new-password">Confirm new password</label>
<input name="confirm-new-password" id="confirm-new-password" type="password" autoComplete="new-password" value={newPasswordConfirm} onChange={(e) => setNewPasswordConfirm(e.target.value)} />
</div>
<Submit onClick={changePassword} label="Save new password" errorMsg={errorMsg} statusMsg={statusMsg}/>
</>
<TextInput type="password" field={form.oldPassword} label="Current password" />
<TextInput type="password" field={form.newPassword} label="New password" />
<TextInput type="password" field={verifyNewPassword} label="Confirm new password" />
<MutationButton label="Change password" result={result} />
</form>
);
}