From 1e94c50cb9196cc812a40c2ab8568833a645df1c Mon Sep 17 00:00:00 2001 From: f0x Date: Thu, 3 Nov 2022 17:54:25 +0000 Subject: [PATCH] refactor emoji upload to rtk query --- web/source/index.js | 1 + web/source/settings/admin/emoji/overview.js | 186 +++++++++++++----- .../settings/components/mutation-button.jsx | 42 ++++ web/source/settings/lib/query/custom-emoji.js | 26 ++- web/source/settings/style.css | 6 + 5 files changed, 196 insertions(+), 65 deletions(-) create mode 100644 web/source/settings/components/mutation-button.jsx diff --git a/web/source/index.js b/web/source/index.js index a96e663cd..90ee5a4ea 100644 --- a/web/source/index.js +++ b/web/source/index.js @@ -66,6 +66,7 @@ skulk({ ], }, settings: { + debug: false, entryFile: "settings", outputFile: "settings.js", prodCfg: prodCfg, diff --git a/web/source/settings/admin/emoji/overview.js b/web/source/settings/admin/emoji/overview.js index bccb00ade..e637ba3da 100644 --- a/web/source/settings/admin/emoji/overview.js +++ b/web/source/settings/admin/emoji/overview.js @@ -23,14 +23,10 @@ const React = require("react"); const Redux = require("react-redux"); const {Link} = require("wouter"); const defaultValue = require('default-value'); +const prettierBytes = require("prettier-bytes"); -const Submit = require("../../components/submit"); const FakeToot = require("../../components/fake-toot"); -const { formFields } = require("../../components/form-fields"); - -const api = require("../../lib/api"); -const adminActions = require("../../redux/reducers/admin").actions; -const submit = require("../../lib/submit"); +const MutateButton = require("../../components/mutation-button"); const query = require("../../lib/query"); @@ -53,7 +49,7 @@ module.exports = function EmojiOverview() { ? "Loading..." : <> - + } @@ -106,46 +102,117 @@ function EmojiCategory({category, entries}) { ); } -const NewEmojiForm = formFields(adminActions.updateNewEmojiVal, (state) => state.admin.newEmoji); -function NewEmoji() { - const dispatch = Redux.useDispatch(); - const newEmojiForm = Redux.useSelector((state) => state.admin.newEmoji); +function useFileInput({withPreview, maxSize}) { + const [file, setFile] = React.useState(); + const [imageURL, setImageURL] = React.useState(); + const [info, setInfo] = React.useState("no file selected"); - const [errorMsg, setError] = React.useState(""); - const [statusMsg, setStatus] = React.useState(""); + function onChange(e) { + let file = e.target.files[0]; + setFile(file); - const uploadEmoji = submit( - () => dispatch(api.admin.newEmoji()), + URL.revokeObjectURL(imageURL); + + if (file != undefined) { + if (withPreview) { + setImageURL(URL.createObjectURL(file)); + } + + let size = prettierBytes(file.size); + if (maxSize && file.size > maxSize) { + size = {size}; + } + + setInfo(<> + {file.name} ({size}) + ); + } else { + setInfo("no file selected"); + } + } + + function reset() { + setFile(); + URL.revokeObjectURL(imageURL); + setInfo("no file selected"); + } + + return [ + onChange, + reset, { - setStatus, setError, - onSuccess: function() { - URL.revokeObjectURL(newEmojiForm.image); - return Promise.all([ - dispatch(adminActions.updateNewEmojiVal(["image", undefined])), - dispatch(adminActions.updateNewEmojiVal(["imageFile", undefined])), - dispatch(adminActions.updateNewEmojiVal(["shortcode", ""])), - ]); - } + file, + imageURL, + info: {info}, } - ); + ]; +} - React.useEffect(() => { - if (newEmojiForm.shortcode.length == 0) { - if (newEmojiForm.imageFile != undefined) { - let [name, ext] = newEmojiForm.imageFile.name.split("."); - dispatch(adminActions.updateNewEmojiVal(["shortcode", name])); - } - } +// TODO: change form field code, maybe look into redux-final-form or similar +// or evaluate if we even need to put most of this in the store +function NewEmoji({emoji}) { + const emojiCodes = React.useMemo(() => { + return new Set(emoji.map((e) => e.shortcode)); + }, [emoji]); + const [addEmoji, result] = query.useAddEmojiMutation(); + const [onFileChange, resetFile, {file, imageURL, info}] = useFileInput({ + withPreview: true, + maxSize: 50 * 1000 }); - let emojiOrShortcode = `:${newEmojiForm.shortcode}:`; + const [shortcode, setShortcode] = React.useState(""); + const shortcodeRef = React.useRef(null); - if (newEmojiForm.image != undefined) { + function onShortChange(e) { + let input = e.target.value; + setShortcode(input); + validateShortcode(input); + } + + function validateShortcode(code) { + console.log("code: (%s)", code); + if (emojiCodes.has(code)) { + shortcodeRef.current.setCustomValidity("Shortcode already in use"); + } else { + shortcodeRef.current.setCustomValidity(""); + } + shortcodeRef.current.reportValidity(); + } + + React.useEffect(() => { + if (shortcode.length == 0) { + if (file != undefined) { + let [name, _ext] = file.name.split("."); + setShortcode(name); + validateShortcode(name); + } + } + }, [file, shortcode]); + + function uploadEmoji(e) { + if (e) { + e.preventDefault(); + } + + Promise.try(() => { + return addEmoji({ + image: file, + shortcode + }); + }).then(() => { + resetFile(); + setShortcode(""); + }); + } + + let emojiOrShortcode = `:${shortcode}:`; + + if (imageURL != undefined) { emojiOrShortcode = {newEmojiForm.shortcode}; } @@ -157,21 +224,38 @@ function NewEmoji() { Look at this new custom emoji {emojiOrShortcode} isn't it cool? - +
+
+ + {info} + +
- - - +
+ + +
+ + + ); } \ No newline at end of file diff --git a/web/source/settings/components/mutation-button.jsx b/web/source/settings/components/mutation-button.jsx new file mode 100644 index 000000000..f79bab2d2 --- /dev/null +++ b/web/source/settings/components/mutation-button.jsx @@ -0,0 +1,42 @@ +/* + GoToSocial + Copyright (C) 2021-2022 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"); + +module.exports = function MutateButton({text, result}) { + let buttonText = text; + + if (result.isLoading) { + buttonText = "Processing..."; + } + + return (
+ {result.error && +
{result.error.status}: {result.error.error}
+ } + +
+ ); +}; \ No newline at end of file diff --git a/web/source/settings/lib/query/custom-emoji.js b/web/source/settings/lib/query/custom-emoji.js index c448c2ec7..a13fa60fd 100644 --- a/web/source/settings/lib/query/custom-emoji.js +++ b/web/source/settings/lib/query/custom-emoji.js @@ -21,7 +21,6 @@ const Promise = require("bluebird"); const base = require("./base"); -const { getChanges } = require("../api"); const endpoints = (build) => ({ getAllEmoji: build.query({ @@ -30,12 +29,12 @@ const endpoints = (build) => ({ params: { limit: 0, ...params - }, - providesTags: (res) => - res - ? [...res.map((id) => ({type: "Emojis", id})), {type: "Emojis", id: "LIST"}] - : [{type: "Emojis", id: "LIST"}] - }) + } + }), + providesTags: (res) => + res + ? [...res.map((emoji) => ({type: "Emojis", id: emoji.id})), {type: "Emojis", id: "LIST"}] + : [{type: "Emojis", id: "LIST"}] }), getEmoji: build.query({ query: (id) => ({ @@ -43,20 +42,19 @@ const endpoints = (build) => ({ }), providesTags: (res, error, id) => [{type: "Emojis", id}] }), - addEmoji: build.query({ + addEmoji: build.mutation({ query: (form) => { - const body = getChanges(form, { - formKeys: ["shortcode"], - fileKeys: ["image"] - }); return { method: "POST", url: `/api/v1/admin/custom_emojis`, asForm: true, - body + body: form }; }, - providesTags: (res, error, id) => [{type: "Emojis", id}] + invalidatesTags: (res) => + res + ? [{type: "Emojis", id: "LIST"}, {type: "Emojis", id: res.id}] + : [{type: "Emojis", id: "LIST"}] }), deleteEmoji: build.mutation({ query: (id) => ({ diff --git a/web/source/settings/style.css b/web/source/settings/style.css index 35d11fa08..bc86aebe2 100644 --- a/web/source/settings/style.css +++ b/web/source/settings/style.css @@ -283,6 +283,12 @@ section.with-sidebar > div { } } +.form-flex { + display: flex; + flex-direction: column; + gap: 1rem; +} + .file-upload > div { display: flex; gap: 1rem;