From 9b139b632098e6741b10fa87ff6224dcb5045947 Mon Sep 17 00:00:00 2001 From: f0x52 Date: Wed, 18 Jan 2023 14:45:14 +0100 Subject: [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 --- .../settings/admin/emoji/category-select.jsx | 42 +-- web/source/settings/admin/emoji/local/detail.js | 167 +++++------ web/source/settings/admin/emoji/local/index.js | 16 +- web/source/settings/admin/emoji/local/new-emoji.js | 146 +++------- web/source/settings/admin/emoji/local/overview.js | 51 ++-- .../settings/admin/emoji/local/use-shortcode.js | 61 ++++ web/source/settings/admin/emoji/remote/index.js | 6 +- .../settings/admin/emoji/remote/parse-from-toot.js | 322 +++++++-------------- 8 files changed, 332 insertions(+), 479 deletions(-) create mode 100644 web/source/settings/admin/emoji/local/use-shortcode.js (limited to 'web/source/settings/admin/emoji') diff --git a/web/source/settings/admin/emoji/category-select.jsx b/web/source/settings/admin/emoji/category-select.jsx index d22534ea8..a35b3f2e3 100644 --- a/web/source/settings/admin/emoji/category-select.jsx +++ b/web/source/settings/admin/emoji/category-select.jsx @@ -1,19 +1,19 @@ /* - GoToSocial - Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org - 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 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. + 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 . + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ "use strict"; @@ -36,13 +36,15 @@ function useEmojiByCategory(emoji) { ), [emoji]); } -function CategorySelect({value, categoryState, setIsNew=() => {}, children}) { +function CategorySelect({ field, children }) { + const { value, setIsNew } = field; + const { data: emoji = [], isLoading, isSuccess, error - } = query.useGetAllEmojiQuery({filter: "domain:local"}); + } = query.useGetAllEmojiQuery({ filter: "domain:local" }); const emojiByCategory = useEmojiByCategory(emoji); @@ -52,7 +54,7 @@ function CategorySelect({value, categoryState, setIsNew=() => {}, children}) { const categoryItems = React.useMemo(() => { return syncpipe(emojiByCategory, [ (_) => Object.keys(_), // just emoji category names - (_) => matchSorter(_, value, {threshold: matchSorter.rankings.NO_MATCH}), // sorted by complex algorithm + (_) => matchSorter(_, value, { threshold: matchSorter.rankings.NO_MATCH }), // sorted by complex algorithm (_) => _.map((categoryName) => [ // map to input value, and selectable element with icon categoryName, <> @@ -67,24 +69,24 @@ function CategorySelect({value, categoryState, setIsNew=() => {}, children}) { if (value != undefined && isSuccess && value.trim().length > 0) { setIsNew(!categories.has(value.trim())); } - }, [categories, value, setIsNew, isSuccess]); + }, [categories, value, isSuccess, setIsNew]); if (error) { // fall back to plain text input, but this would almost certainly have caused a bigger error message elsewhere return ( <> - {categoryState.value = e.target.value;}}/>; + { field.value = e.target.value; }} />; ); } else if (isLoading) { - return ; + return ; } return ( ); diff --git a/web/source/settings/admin/emoji/local/detail.js b/web/source/settings/admin/emoji/local/detail.js index cc3ce6a70..cecd36869 100644 --- a/web/source/settings/admin/emoji/local/detail.js +++ b/web/source/settings/admin/emoji/local/detail.js @@ -19,155 +19,128 @@ "use strict"; const React = require("react"); - const { useRoute, Link, Redirect } = require("wouter"); +const query = require("../../../lib/query"); + +const { useComboBoxInput, useFileInput, useValue } = require("../../../lib/form"); const { CategorySelect } = require("../category-select"); -const { useComboBoxInput, useFileInput } = require("../../../components/form"); -const query = require("../../../lib/query"); +const useFormSubmit = require("../../../lib/form/submit"); + const FakeToot = require("../../../components/fake-toot"); +const FormWithData = require("../../../lib/form/form-with-data"); const Loading = require("../../../components/loading"); +const { FileInput } = require("../../../components/form/inputs"); +const MutationButton = require("../../../components/form/mutation-button"); +const { Error } = require("../../../components/error"); const base = "/settings/custom-emoji/local"; module.exports = function EmojiDetailRoute() { let [_match, params] = useRoute(`${base}/:emojiId`); if (params?.emojiId == undefined) { - return ; + return ; } else { return (
< go back - +
); } }; -function EmojiDetailData({emojiId}) { - const {currentData: emoji, isLoading, error} = query.useGetEmojiQuery(emojiId); - - if (error) { - return ( -
- {error.status}: {error.data.error} -
- ); - } else if (isLoading) { - return ( -
- -
- ); - } else { - return ; - } -} - -function EmojiDetail({emoji}) { - const [modifyEmoji, modifyResult] = query.useEditEmojiMutation(); +function EmojiDetailForm({ data: emoji }) { + const form = { + id: useValue("id", emoji.id), + category: useComboBoxInput("category", { defaultValue: emoji.category }), + image: useFileInput("image", { + withPreview: true, + maxSize: 50 * 1024 // TODO: get from instance api + }) + }; - const [isNewCategory, setIsNewCategory] = React.useState(false); + const [modifyEmoji, result] = useFormSubmit(form, query.useEditEmojiMutation()); - const [categoryState, _resetCategory, { category }] = useComboBoxInput("category", {defaultValue: emoji.category}); - - const [onFileChange, _resetFile, { image, imageURL, imageInfo }] = useFileInput("image", { - withPreview: true, - maxSize: 50 * 1024 - }); + // Automatic submitting of category change + React.useEffect(() => { + if ( + form.category.hasChanged() && + !form.category.state.open && + !form.category.isNew) { + modifyEmoji(); + } + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [form.category.hasChanged(), form.category.isNew, form.category.state.open]); - function modifyCategory() { - modifyEmoji({id: emoji.id, category: category.trim()}); - } + const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation(); - function modifyImage() { - modifyEmoji({id: emoji.id, image: image}); + if (deleteResult.isSuccess) { + return ; } - React.useEffect(() => { - if (category != emoji.category && !categoryState.open && !isNewCategory && category.trim().length > 0) { - console.log("updating to", category); - modifyEmoji({id: emoji.id, category: category.trim()}); - } - }, [isNewCategory, category, categoryState.open, emoji.category, emoji.id, modifyEmoji]); - return ( <>
- {emoji.shortcode} + {emoji.shortcode}

{emoji.shortcode}

- + deleteEmoji(emoji.id)} + className="danger" + showError={false} + result={deleteResult} + />
-
-

Modify this emoji {modifyResult.isLoading && "(processing..)"}

- - {modifyResult.error &&
- {modifyResult.error.status}: {modifyResult.error.data.error} -
} +
+

Modify this emoji {result.isLoading && }

- +
- Image -
- - {imageInfo} - -
- - + + + Look at this new custom emoji {emoji.shortcode} isn't it cool? + + {result.error && } + {deleteResult.error && }
-
+ ); -} - -function DeleteButton({id}) { - // TODO: confirmation dialog? - const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation(); - - let text = "Delete"; - if (deleteResult.isLoading) { - text = "Deleting..."; - } - - if (deleteResult.isSuccess) { - return ; - } - - return ( - - ); } \ No newline at end of file diff --git a/web/source/settings/admin/emoji/local/index.js b/web/source/settings/admin/emoji/local/index.js index 4160fe41d..68cbbc47f 100644 --- a/web/source/settings/admin/emoji/local/index.js +++ b/web/source/settings/admin/emoji/local/index.js @@ -19,7 +19,7 @@ "use strict"; const React = require("react"); -const {Switch, Route} = require("wouter"); +const { Switch, Route } = require("wouter"); const EmojiOverview = require("./overview"); const EmojiDetail = require("./detail"); @@ -28,13 +28,11 @@ const base = "/settings/custom-emoji/local"; module.exports = function CustomEmoji() { return ( - <> - - - - - - - + + + + + + ); }; diff --git a/web/source/settings/admin/emoji/local/new-emoji.js b/web/source/settings/admin/emoji/local/new-emoji.js index 6701dbf5a..cf1b5f774 100644 --- a/web/source/settings/admin/emoji/local/new-emoji.js +++ b/web/source/settings/admin/emoji/local/new-emoji.js @@ -18,101 +18,61 @@ "use strict"; -const Promise = require('bluebird'); const React = require("react"); -const FakeToot = require("../../../components/fake-toot"); -const MutateButton = require("../../../components/mutation-button"); +const query = require("../../../lib/query"); const { - useTextInput, useFileInput, useComboBoxInput -} = require("../../../components/form"); +} = require("../../../lib/form"); +const useShortcode = require("./use-shortcode"); -const query = require("../../../lib/query"); -const { CategorySelect } = require('../category-select'); +const useFormSubmit = require("../../../lib/form/submit"); -const shortcodeRegex = /^[a-z0-9_]+$/; +const { + TextInput, FileInput +} = require("../../../components/form/inputs"); -module.exports = function NewEmojiForm({ emoji }) { - const emojiCodes = React.useMemo(() => { - return new Set(emoji.map((e) => e.shortcode)); - }, [emoji]); +const { CategorySelect } = require('../category-select'); +const FakeToot = require("../../../components/fake-toot"); +const MutationButton = require("../../../components/form/mutation-button"); - const [addEmoji, result] = query.useAddEmojiMutation(); +module.exports = function NewEmojiForm() { + const shortcode = useShortcode(); - const [onFileChange, resetFile, { image, imageURL, imageInfo }] = useFileInput("image", { + const image = useFileInput("image", { withPreview: true, - maxSize: 50 * 1024 + maxSize: 50 * 1024 // TODO: get from instance api? }); - const [onShortcodeChange, resetShortcode, { shortcode, setShortcode, shortcodeRef }] = useTextInput("shortcode", { - validator: function validateShortcode(code) { - // technically invalid, but hacky fix to prevent validation error on page load - if (shortcode == "") {return "";} - - if (emojiCodes.has(code)) { - return "Shortcode already in use"; - } - - if (code.length < 2 || code.length > 30) { - return "Shortcode must be between 2 and 30 characters"; - } + const category = useComboBoxInput("category"); - if (code.toLowerCase() != code) { - return "Shortcode must be lowercase"; - } - - if (!shortcodeRegex.test(code)) { - return "Shortcode must only contain lowercase letters, numbers, and underscores"; - } - - return ""; - } - }); - - const [categoryState, resetCategory, { category }] = useComboBoxInput("category"); + const [submitForm, result] = useFormSubmit({ + shortcode, image, category + }, query.useAddEmojiMutation()); React.useEffect(() => { - if (shortcode.length == 0) { - if (image != undefined) { - let [name, _ext] = image.name.split("."); - setShortcode(name); + if (shortcode.value.length == 0) { + if (image.value != undefined) { + let [name, _ext] = image.value.name.split("."); + shortcode.setter(name); } } - // we explicitly don't want to add 'shortcode' as a dependency here - // because we only want this to update to the filename if the field is empty - // at the moment the file is selected, not some time after when the field is emptied - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [image]); - - function uploadEmoji(e) { - if (e) { - e.preventDefault(); - } - Promise.try(() => { - return addEmoji({ - image, - shortcode, - category - }).unwrap(); - }).then(() => { - resetFile(); - resetShortcode(); - resetCategory(); - }).catch((e) => { - console.error("Emoji upload error:", e); - }); - } + /* We explicitly don't want to have 'shortcode' as a dependency here + because we only want to change the shortcode to the filename if the field is empty + at the moment the file is selected, not some time after when the field is emptied + */ + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [image.value]); - let emojiOrShortcode = `:${shortcode}:`; + let emojiOrShortcode = `:${shortcode.value}:`; - if (imageURL != undefined) { + if (image.previewValue != undefined) { emojiOrShortcode = {shortcode}; @@ -126,42 +86,22 @@ module.exports = function NewEmojiForm({ emoji }) { Look at this new custom emoji {emojiOrShortcode} isn't it cool? -
-
- - {imageInfo} - -
- -
- - -
+ + + + - + ); diff --git a/web/source/settings/admin/emoji/local/overview.js b/web/source/settings/admin/emoji/local/overview.js index ebfb89695..524cc928f 100644 --- a/web/source/settings/admin/emoji/local/overview.js +++ b/web/source/settings/admin/emoji/local/overview.js @@ -1,25 +1,25 @@ /* - GoToSocial - Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org - 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 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. + 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 . + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ "use strict"; const React = require("react"); -const {Link} = require("wouter"); +const { Link } = require("wouter"); const NewEmojiForm = require("./new-emoji"); @@ -27,33 +27,31 @@ const query = require("../../../lib/query"); const { useEmojiByCategory } = require("../category-select"); const Loading = require("../../../components/loading"); -const base = "/settings/custom-emoji/local"; - -module.exports = function EmojiOverview() { +module.exports = function EmojiOverview({ baseUrl }) { const { data: emoji = [], isLoading, error - } = query.useGetAllEmojiQuery({filter: "domain:local"}); + } = query.useGetAllEmojiQuery({ filter: "domain:local" }); return ( <>

Custom Emoji (local)

- {error && + {error &&
{error}
} {isLoading - ? + ? : <> - - + + } ); }; -function EmojiList({emoji}) { +function EmojiList({ emoji, baseUrl }) { const emojiByCategory = useEmojiByCategory(emoji); return ( @@ -62,24 +60,23 @@ function EmojiList({emoji}) {
{emoji.length == 0 && "No local emoji yet, add one below"} {Object.entries(emojiByCategory).map(([category, entries]) => { - return ; + return ; })}
); } -function EmojiCategory({category, entries}) { +function EmojiCategory({ category, entries, baseUrl }) { return (
{category}
{entries.map((e) => { return ( - - {/* */} + - {e.shortcode} + {e.shortcode} ); diff --git a/web/source/settings/admin/emoji/local/use-shortcode.js b/web/source/settings/admin/emoji/local/use-shortcode.js new file mode 100644 index 000000000..109914b8b --- /dev/null +++ b/web/source/settings/admin/emoji/local/use-shortcode.js @@ -0,0 +1,61 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + +"use strict"; + +const React = require("react"); + +const query = require("../../../lib/query"); +const { useTextInput } = require("../../../lib/form"); + +const shortcodeRegex = /^[a-z0-9_]+$/; + +module.exports = function useShortcode() { + const { + data: emoji = [] + } = query.useGetAllEmojiQuery({ filter: "domain:local" }); + + const emojiCodes = React.useMemo(() => { + return new Set(emoji.map((e) => e.shortcode)); + }, [emoji]); + + return useTextInput("shortcode", { + validator: function validateShortcode(code) { + // technically invalid, but hacky fix to prevent validation error on page load + if (code == "") { return ""; } + + if (emojiCodes.has(code)) { + return "Shortcode already in use"; + } + + if (code.length < 2 || code.length > 30) { + return "Shortcode must be between 2 and 30 characters"; + } + + if (code.toLowerCase() != code) { + return "Shortcode must be lowercase"; + } + + if (!shortcodeRegex.test(code)) { + return "Shortcode must only contain lowercase letters, numbers, and underscores"; + } + + return ""; + } + }); +}; \ No newline at end of file diff --git a/web/source/settings/admin/emoji/remote/index.js b/web/source/settings/admin/emoji/remote/index.js index fb1e05083..f3bb325e9 100644 --- a/web/source/settings/admin/emoji/remote/index.js +++ b/web/source/settings/admin/emoji/remote/index.js @@ -31,7 +31,7 @@ module.exports = function RemoteEmoji() { data: emoji = [], isLoading, error - } = query.useGetAllEmojiQuery({filter: "domain:local"}); + } = query.useGetAllEmojiQuery({ filter: "domain:local" }); const emojiCodes = React.useMemo(() => { return new Set(emoji.map((e) => e.shortcode)); @@ -40,11 +40,11 @@ module.exports = function RemoteEmoji() { return ( <>

Custom Emoji (remote)

- {error && + {error &&
{error}
} {isLoading - ? + ? : <> diff --git a/web/source/settings/admin/emoji/remote/parse-from-toot.js b/web/source/settings/admin/emoji/remote/parse-from-toot.js index 963ae1a80..309619ea4 100644 --- a/web/source/settings/admin/emoji/remote/parse-from-toot.js +++ b/web/source/settings/admin/emoji/remote/parse-from-toot.js @@ -18,57 +18,35 @@ "use strict"; -const Promise = require("bluebird"); const React = require("react"); -const Redux = require("react-redux"); -const syncpipe = require("syncpipe"); + +const query = require("../../../lib/query"); const { useTextInput, - useComboBoxInput -} = require("../../../components/form"); + useComboBoxInput, + useCheckListInput +} = require("../../../lib/form"); +const useFormSubmit = require("../../../lib/form/submit"); + +const CheckList = require("../../../components/check-list"); const { CategorySelect } = require('../category-select'); -const query = require("../../../lib/query"); -const Loading = require("../../../components/loading"); +const { TextInput } = require("../../../components/form/inputs"); +const MutationButton = require("../../../components/form/mutation-button"); +const { Error } = require("../../../components/error"); module.exports = function ParseFromToot({ emojiCodes }) { - const [searchStatus, { data, isLoading, isSuccess, error }] = query.useSearchStatusForEmojiMutation(); - const instanceDomain = Redux.useSelector((state) => (new URL(state.oauth.instance).host)); + const [searchStatus, result] = query.useSearchStatusForEmojiMutation(); const [onURLChange, _resetURL, { url }] = useTextInput("url"); - const searchResult = React.useMemo(() => { - if (!isSuccess) { - return null; - } - - if (data.type == "none") { - return "No results found"; - } - - if (data.domain == instanceDomain) { - return This is a local user/toot, all referenced emoji are already on your instance; - } - - if (data.list.length == 0) { - return This {data.type == "statuses" ? "toot" : "account"} doesn't use any custom emoji; - } - - return ( - - ); - }, [isSuccess, data, instanceDomain, emojiCodes]); - function submitSearch(e) { e.preventDefault(); - searchStatus(url); + if (url.trim().length != 0) { + searchStatus(url); + } } return ( @@ -87,233 +65,137 @@ module.exports = function ParseFromToot({ emojiCodes }) { onChange={onURLChange} value={url} /> -