From 9b139b632098e6741b10fa87ff6224dcb5045947 Mon Sep 17 00:00:00 2001 From: f0x52 Date: Wed, 18 Jan 2023 14:45:14 +0100 Subject: [PATCH] [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 --- web/source/.eslintrc.js | 24 +- .../temporary.js => .license-header.js} | 15 - web/source/css/_colors.css | 12 +- web/source/css/base.css | 8 +- web/source/index.js | 3 +- web/source/package.json | 4 +- web/source/settings/admin/actions.js | 41 +- .../settings/admin/emoji/category-select.jsx | 42 +- .../settings/admin/emoji/local/detail.js | 169 ++++---- .../settings/admin/emoji/local/index.js | 16 +- .../settings/admin/emoji/local/new-emoji.js | 152 ++----- .../settings/admin/emoji/local/overview.js | 51 ++- .../admin/emoji/local/use-shortcode.js | 61 +++ .../settings/admin/emoji/remote/index.js | 6 +- .../admin/emoji/remote/parse-from-toot.js | 328 +++++---------- web/source/settings/admin/federation.js | 394 ------------------ .../settings/admin/federation/detail.js | 146 +++++++ .../admin/federation/import-export.js | 307 ++++++++++++++ .../submit.jsx => admin/federation/index.js} | 31 +- .../settings/admin/federation/overview.js | 100 +++++ web/source/settings/admin/settings.js | 107 +++-- .../components/authorization/index.jsx | 76 ++++ .../components/authorization/login.jsx | 67 +++ .../settings/components/back-button.jsx | 2 +- web/source/settings/components/check-list.jsx | 58 +++ web/source/settings/components/combo-box.jsx | 8 +- web/source/settings/components/error.jsx | 45 +- .../settings/components/fake-profile.jsx | 17 +- web/source/settings/components/fake-toot.jsx | 13 +- .../settings/components/form-fields.jsx | 167 -------- web/source/settings/components/form/index.js | 37 -- .../settings/components/form/inputs.jsx | 141 +++++++ .../components/form/mutation-button.jsx | 49 +++ web/source/settings/components/loading.jsx | 2 +- web/source/settings/components/login.jsx | 102 ----- web/source/settings/components/nav-button.jsx | 2 +- web/source/settings/index.js | 146 ++----- web/source/settings/lib/api/admin.js | 168 -------- web/source/settings/lib/api/index.js | 193 --------- web/source/settings/lib/api/oauth.js | 127 ------ web/source/settings/lib/api/user.js | 67 --- web/source/settings/lib/errors.js | 27 -- web/source/settings/lib/form/bool.jsx | 50 +++ web/source/settings/lib/form/check-list.jsx | 147 +++++++ .../combobox.jsx => lib/form/combo-box.jsx} | 21 +- .../{components => lib}/form/file.jsx | 35 +- .../form/form-with-data.jsx} | 31 +- .../instances.js => lib/form/index.js} | 44 +- web/source/settings/lib/form/radio.jsx | 51 +++ web/source/settings/lib/form/submit.js | 83 ++++ .../{components => lib}/form/text.jsx | 25 +- web/source/settings/lib/get-views.js | 4 +- .../settings/lib/query/admin/custom-emoji.js | 195 +++++++++ .../settings/lib/query/admin/import-export.js | 212 ++++++++++ web/source/settings/lib/query/admin/index.js | 84 ++++ web/source/settings/lib/query/base.js | 60 ++- web/source/settings/lib/query/custom-emoji.js | 180 -------- web/source/settings/lib/query/index.js | 28 +- web/source/settings/lib/query/lib.js | 75 ++++ web/source/settings/lib/query/oauth.js | 158 +++++++ .../settings/lib/{submit.js => query/user.js} | 50 +-- web/source/settings/redux/index.js | 34 +- .../settings/redux/{reducers => }/oauth.js | 28 +- web/source/settings/redux/reducers/admin.js | 99 ----- web/source/settings/redux/reducers/user.js | 50 --- web/source/settings/style.css | 223 +++++++--- web/source/settings/user/profile.js | 128 ++++-- web/source/settings/user/settings.js | 149 +++---- web/source/yarn.lock | 17 +- 69 files changed, 3129 insertions(+), 2663 deletions(-) rename web/source/{settings/redux/reducers/temporary.js => .license-header.js} (75%) create mode 100644 web/source/settings/admin/emoji/local/use-shortcode.js delete mode 100644 web/source/settings/admin/federation.js create mode 100644 web/source/settings/admin/federation/detail.js create mode 100644 web/source/settings/admin/federation/import-export.js rename web/source/settings/{components/submit.jsx => admin/federation/index.js} (59%) create mode 100644 web/source/settings/admin/federation/overview.js create mode 100644 web/source/settings/components/authorization/index.jsx create mode 100644 web/source/settings/components/authorization/login.jsx create mode 100644 web/source/settings/components/check-list.jsx delete mode 100644 web/source/settings/components/form-fields.jsx delete mode 100644 web/source/settings/components/form/index.js create mode 100644 web/source/settings/components/form/inputs.jsx create mode 100644 web/source/settings/components/form/mutation-button.jsx delete mode 100644 web/source/settings/components/login.jsx delete mode 100644 web/source/settings/lib/api/admin.js delete mode 100644 web/source/settings/lib/api/index.js delete mode 100644 web/source/settings/lib/api/oauth.js delete mode 100644 web/source/settings/lib/api/user.js delete mode 100644 web/source/settings/lib/errors.js create mode 100644 web/source/settings/lib/form/bool.jsx create mode 100644 web/source/settings/lib/form/check-list.jsx rename web/source/settings/{components/form/combobox.jsx => lib/form/combo-box.jsx} (71%) rename web/source/settings/{components => lib}/form/file.jsx (78%) rename web/source/settings/{components/mutation-button.jsx => lib/form/form-with-data.jsx} (61%) rename web/source/settings/{redux/reducers/instances.js => lib/form/index.js} (54%) create mode 100644 web/source/settings/lib/form/radio.jsx create mode 100644 web/source/settings/lib/form/submit.js rename web/source/settings/{components => lib}/form/text.jsx (73%) create mode 100644 web/source/settings/lib/query/admin/custom-emoji.js create mode 100644 web/source/settings/lib/query/admin/import-export.js create mode 100644 web/source/settings/lib/query/admin/index.js delete mode 100644 web/source/settings/lib/query/custom-emoji.js create mode 100644 web/source/settings/lib/query/lib.js create mode 100644 web/source/settings/lib/query/oauth.js rename web/source/settings/lib/{submit.js => query/user.js} (57%) rename web/source/settings/redux/{reducers => }/oauth.js (67%) delete mode 100644 web/source/settings/redux/reducers/admin.js delete mode 100644 web/source/settings/redux/reducers/user.js diff --git a/web/source/.eslintrc.js b/web/source/.eslintrc.js index 7615c76d..09160993 100644 --- a/web/source/.eslintrc.js +++ b/web/source/.eslintrc.js @@ -1,5 +1,27 @@ +/* + 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"; module.exports = { - "extends": ["@joepie91/eslint-config/react"] + "extends": ["@joepie91/eslint-config/react"], + "plugins": ["license-header"], + "rules": { + "license-header/header": ["error", ".license-header.js"] + } }; \ No newline at end of file diff --git a/web/source/settings/redux/reducers/temporary.js b/web/source/.license-header.js similarity index 75% rename from web/source/settings/redux/reducers/temporary.js rename to web/source/.license-header.js index 0155f097..41b9ad67 100644 --- a/web/source/settings/redux/reducers/temporary.js +++ b/web/source/.license-header.js @@ -15,18 +15,3 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ - -"use strict"; - -const {createSlice} = require("@reduxjs/toolkit"); - -module.exports = createSlice({ - name: "temporary", - initialState: { - }, - reducers: { - setStatus: function(state, {payload}) { - state.status = payload; - } - } -}); \ No newline at end of file diff --git a/web/source/css/_colors.css b/web/source/css/_colors.css index ca17f579..13a75c9f 100644 --- a/web/source/css/_colors.css +++ b/web/source/css/_colors.css @@ -47,7 +47,13 @@ $blue3: #89caff; /* hover/selected accent to $blue2, can be used with $gray1 (7. $error1: #860000; /* Error border/foreground text, can be used with $error2 (5.0), $white1 (10), $white2 (5.1) */ $error2: #ff9796; /* Error background text, can be used with $error1 (5.0), $gray1 (6.6), $gray2 (5.3), $gray3 (4.8) */ $error3: #dd2c2c; /* Error button background text, can be used with $white1 (4.51) */ -$error-link: #185F8C; /* Error link text, can be used with $error2 (5.54) */ +$error-link: #01318C; /* Error link text, can be used with $error2 (5.56) */ + +$green1: #94E749; /* Green for positive/confirmation, similar contrast (luminance) as $blue2 */ + +$info-fg: $gray1; +$info-bg: #b3ddff; +$info-link: $error-link; $fg: $white1; $bg: $gray1; @@ -92,6 +98,7 @@ $avatar-border: $orange2; $input-bg: $gray4; $input-disabled-bg: $gray2; $input-border: $blue1; +$input-error-border: $error3; $input-focus-border: $blue3; $settings-nav-bg: $bg-accent; @@ -107,5 +114,6 @@ $settings-nav-bg-active: $gray2; $error-fg: $error1; $error-bg: $error2; -$settings-entry-bg: $gray3; +$settings-entry-bg: $gray2; +$settings-entry-alternate-bg: $gray3; $settings-entry-hover-bg: $gray4; \ No newline at end of file diff --git a/web/source/css/base.css b/web/source/css/base.css index 340052b5..e0350577 100644 --- a/web/source/css/base.css +++ b/web/source/css/base.css @@ -311,12 +311,16 @@ input, select, textarea, .input { font-size: 1rem; padding: 0.3rem; - &:focus { + &:focus, &:active { border-color: $input-focus-border; } + &:invalid { + border-color: $input-error-border; + } + &:disabled { - background: $input-disabled-bg; + background: transparent; } } diff --git a/web/source/index.js b/web/source/index.js index 7ad57104..e4b2086d 100644 --- a/web/source/index.js +++ b/web/source/index.js @@ -32,7 +32,7 @@ const prodCfg = { global: true, exts: ".js" }], - ["@browserify/envify", {global: true}] + ["@browserify/envify", { global: true }] ] }; @@ -66,6 +66,7 @@ skulk({ ], }, settings: { + debug: false, entryFile: "settings", outputFile: "settings.js", prodCfg: prodCfg, diff --git a/web/source/package.json b/web/source/package.json index 7685df23..f9520bab 100644 --- a/web/source/package.json +++ b/web/source/package.json @@ -6,7 +6,7 @@ "author": "f0x", "license": "AGPL-3.0", "scripts": { - "lint": "eslint .", + "lint": "eslint . --ext .js,.jsx", "build": "node index.js", "dev": "NODE_ENV=development node index.js" }, @@ -14,7 +14,6 @@ "@reduxjs/toolkit": "^1.8.6", "ariakit": "^2.0.0-next.41", "bluebird": "^3.7.2", - "dotty": "^0.1.2", "is-valid-domain": "^0.1.6", "js-file-download": "^0.4.12", "langs": "^2.0.0", @@ -44,6 +43,7 @@ "babelify": "^10.0.0", "css-extract": "^2.0.0", "eslint": "^8.26.0", + "eslint-plugin-license-header": "^0.6.0", "eslint-plugin-react": "^7.31.10", "eslint-plugin-react-hooks": "^4.6.0", "factor-bundle": "^2.5.0", diff --git a/web/source/settings/admin/actions.js b/web/source/settings/admin/actions.js index 66caa179..b91d81e1 100644 --- a/web/source/settings/admin/actions.js +++ b/web/source/settings/admin/actions.js @@ -19,42 +19,43 @@ "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 submit = require("../lib/submit"); +const { useTextInput } = require("../lib/form"); +const { TextInput } = require("../components/form/inputs"); + +const MutationButton = require("../components/form/mutation-button"); module.exports = function AdminActionPanel() { - const dispatch = Redux.useDispatch(); + const daysField = useTextInput("days", { defaultValue: 30 }); - const [days, setDays] = React.useState(30); + const [mediaCleanup, mediaCleanupResult] = query.useMediaCleanupMutation(); - const [errorMsg, setError] = React.useState(""); - const [statusMsg, setStatus] = React.useState(""); - - const removeMedia = submit( - () => dispatch(api.admin.mediaCleanup(days)), - {setStatus, setError} - ); + function submitMediaCleanup(e) { + e.preventDefault(); + mediaCleanup(daysField.value); + } return ( <>

Admin Actions

-
+

Media cleanup

Clean up remote media older than the specified number of days. If the remote instance is still online they will be refetched when needed. Also cleans up unused headers and avatars from the media cache.

-
- - setDays(e.target.value)}/> -
- -
+ + + ); }; \ No newline at end of file diff --git a/web/source/settings/admin/emoji/category-select.jsx b/web/source/settings/admin/emoji/category-select.jsx index d22534ea..a35b3f2e 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 cc3ce6a7..cecd3686 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 { CategorySelect } = require("../category-select"); -const { useComboBoxInput, useFileInput } = require("../../../components/form"); - const query = require("../../../lib/query"); + +const { useComboBoxInput, useFileInput, useValue } = require("../../../lib/form"); +const { CategorySelect } = require("../category-select"); + +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); +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 + }) + }; - if (error) { - return ( -
- {error.status}: {error.data.error} -
- ); - } else if (isLoading) { - return ( -
- -
- ); - } else { - return ; - } -} - -function EmojiDetail({emoji}) { - const [modifyEmoji, modifyResult] = query.useEditEmojiMutation(); - - const [isNewCategory, setIsNewCategory] = React.useState(false); - - const [categoryState, _resetCategory, { category }] = useComboBoxInput("category", {defaultValue: emoji.category}); - - const [onFileChange, _resetFile, { image, imageURL, imageInfo }] = useFileInput("image", { - withPreview: true, - maxSize: 50 * 1024 - }); - - function modifyCategory() { - modifyEmoji({id: emoji.id, category: category.trim()}); - } - - function modifyImage() { - modifyEmoji({id: emoji.id, image: image}); - } + const [modifyEmoji, result] = useFormSubmit(form, query.useEditEmojiMutation()); + // Automatic submitting of category change 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()}); + if ( + form.category.hasChanged() && + !form.category.state.open && + !form.category.isNew) { + modifyEmoji(); } - }, [isNewCategory, category, categoryState.open, emoji.category, emoji.id, modifyEmoji]); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [form.category.hasChanged(), form.category.isNew, form.category.state.open]); + + const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation(); + + if (deleteResult.isSuccess) { + return ; + } 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 4160fe41..68cbbc47 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 6701dbf5..cf1b5f77 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 useFormSubmit = require("../../../lib/form/submit"); + +const { + TextInput, FileInput +} = require("../../../components/form/inputs"); -const query = require("../../../lib/query"); const { CategorySelect } = require('../category-select'); +const FakeToot = require("../../../components/fake-toot"); +const MutationButton = require("../../../components/form/mutation-button"); -const shortcodeRegex = /^[a-z0-9_]+$/; +module.exports = function NewEmojiForm() { + const shortcode = useShortcode(); -module.exports = function NewEmojiForm({ emoji }) { - const emojiCodes = React.useMemo(() => { - return new Set(emoji.map((e) => e.shortcode)); - }, [emoji]); - - const [addEmoji, result] = query.useAddEmojiMutation(); - - 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 "";} + const category = useComboBoxInput("category"); - 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 ""; - } - }); - - 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(); - } + /* 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]); - Promise.try(() => { - return addEmoji({ - image, - shortcode, - category - }).unwrap(); - }).then(() => { - resetFile(); - resetShortcode(); - resetCategory(); - }).catch((e) => { - console.error("Emoji upload error:", e); - }); - } + let emojiOrShortcode = `:${shortcode.value}:`; - let emojiOrShortcode = `:${shortcode}:`; - - 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 ebfb8969..524cc928 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 00000000..109914b8 --- /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 fb1e0508..f3bb325e 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 963ae1a8..309619ea 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} /> -