refactor emoji upload to rtk query

This commit is contained in:
f0x 2022-11-03 17:54:25 +00:00
parent bcaf1e95f1
commit 1e94c50cb9
5 changed files with 196 additions and 65 deletions

View file

@ -66,6 +66,7 @@ skulk({
], ],
}, },
settings: { settings: {
debug: false,
entryFile: "settings", entryFile: "settings",
outputFile: "settings.js", outputFile: "settings.js",
prodCfg: prodCfg, prodCfg: prodCfg,

View file

@ -23,14 +23,10 @@ const React = require("react");
const Redux = require("react-redux"); const Redux = require("react-redux");
const {Link} = require("wouter"); const {Link} = require("wouter");
const defaultValue = require('default-value'); const defaultValue = require('default-value');
const prettierBytes = require("prettier-bytes");
const Submit = require("../../components/submit");
const FakeToot = require("../../components/fake-toot"); const FakeToot = require("../../components/fake-toot");
const { formFields } = require("../../components/form-fields"); const MutateButton = require("../../components/mutation-button");
const api = require("../../lib/api");
const adminActions = require("../../redux/reducers/admin").actions;
const submit = require("../../lib/submit");
const query = require("../../lib/query"); const query = require("../../lib/query");
@ -53,7 +49,7 @@ module.exports = function EmojiOverview() {
? "Loading..." ? "Loading..."
: <> : <>
<EmojiList emoji={emoji}/> <EmojiList emoji={emoji}/>
<NewEmoji/> <NewEmoji emoji={emoji}/>
</> </>
} }
</> </>
@ -106,46 +102,117 @@ function EmojiCategory({category, entries}) {
); );
} }
const NewEmojiForm = formFields(adminActions.updateNewEmojiVal, (state) => state.admin.newEmoji); function useFileInput({withPreview, maxSize}) {
function NewEmoji() { const [file, setFile] = React.useState();
const dispatch = Redux.useDispatch(); const [imageURL, setImageURL] = React.useState();
const newEmojiForm = Redux.useSelector((state) => state.admin.newEmoji); const [info, setInfo] = React.useState("no file selected");
const [errorMsg, setError] = React.useState(""); function onChange(e) {
const [statusMsg, setStatus] = React.useState(""); let file = e.target.files[0];
setFile(file);
const uploadEmoji = submit( URL.revokeObjectURL(imageURL);
() => dispatch(api.admin.newEmoji()),
if (file != undefined) {
if (withPreview) {
setImageURL(URL.createObjectURL(file));
}
let size = prettierBytes(file.size);
if (maxSize && file.size > maxSize) {
size = <span className="error-text">{size}</span>;
}
setInfo(<>
{file.name} ({size})
</>);
} else {
setInfo("no file selected");
}
}
function reset() {
setFile();
URL.revokeObjectURL(imageURL);
setInfo("no file selected");
}
return [
onChange,
reset,
{ {
setStatus, setError, file,
onSuccess: function() { imageURL,
URL.revokeObjectURL(newEmojiForm.image); info: <span>{info}</span>,
return Promise.all([
dispatch(adminActions.updateNewEmojiVal(["image", undefined])),
dispatch(adminActions.updateNewEmojiVal(["imageFile", undefined])),
dispatch(adminActions.updateNewEmojiVal(["shortcode", ""])),
]);
}
} }
); ];
}
React.useEffect(() => { // TODO: change form field code, maybe look into redux-final-form or similar
if (newEmojiForm.shortcode.length == 0) { // or evaluate if we even need to put most of this in the store
if (newEmojiForm.imageFile != undefined) { function NewEmoji({emoji}) {
let [name, ext] = newEmojiForm.imageFile.name.split("."); const emojiCodes = React.useMemo(() => {
dispatch(adminActions.updateNewEmojiVal(["shortcode", name])); 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 = <img emojiOrShortcode = <img
className="emoji" className="emoji"
src={newEmojiForm.image} src={imageURL}
title={`:${newEmojiForm.shortcode}:`} title={`:${shortcode}:`}
alt={newEmojiForm.shortcode} alt={shortcode}
/>; />;
} }
@ -157,21 +224,38 @@ function NewEmoji() {
Look at this new custom emoji {emojiOrShortcode} isn&apos;t it cool? Look at this new custom emoji {emojiOrShortcode} isn&apos;t it cool?
</FakeToot> </FakeToot>
<NewEmojiForm.File <form onSubmit={uploadEmoji} className="form-flex">
id="image" <div className="form-field file">
name="Image" <label className="file-input button" htmlFor="image">
fileType="image/png,image/gif" Browse
showSize={true} </label>
maxSize={50 * 1000} {info}
/> <input
className="hidden"
type="file"
id="image"
name="Image"
accept="image/png,image/gif"
onChange={onFileChange}
/>
</div>
<NewEmojiForm.TextInput <div className="form-field text">
id="shortcode" <label htmlFor="shortcode">
name="Shortcode (without : :), must be unique on the instance" Shortcode, must be unique among the instance's local emoji
placeHolder="blobcat" </label>
/> <input
type="text"
id="shortcode"
name="Shortcode"
ref={shortcodeRef}
onChange={onShortChange}
value={shortcode}
/>
</div>
<Submit onClick={uploadEmoji} label="Upload" errorMsg={errorMsg} statusMsg={statusMsg} /> <MutateButton text="Upload emoji" result={result}/>
</form>
</div> </div>
); );
} }

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
"use strict";
const React = require("react");
module.exports = function MutateButton({text, result}) {
let buttonText = text;
if (result.isLoading) {
buttonText = "Processing...";
}
return (<div>
{result.error &&
<div className="error">{result.error.status}: {result.error.error}</div>
}
<input
className="button"
type="submit"
disabled={result.isLoading}
value={buttonText}
/>
</div>
);
};

View file

@ -21,7 +21,6 @@
const Promise = require("bluebird"); const Promise = require("bluebird");
const base = require("./base"); const base = require("./base");
const { getChanges } = require("../api");
const endpoints = (build) => ({ const endpoints = (build) => ({
getAllEmoji: build.query({ getAllEmoji: build.query({
@ -30,12 +29,12 @@ const endpoints = (build) => ({
params: { params: {
limit: 0, limit: 0,
...params ...params
}, }
providesTags: (res) => }),
res providesTags: (res) =>
? [...res.map((id) => ({type: "Emojis", id})), {type: "Emojis", id: "LIST"}] res
: [{type: "Emojis", id: "LIST"}] ? [...res.map((emoji) => ({type: "Emojis", id: emoji.id})), {type: "Emojis", id: "LIST"}]
}) : [{type: "Emojis", id: "LIST"}]
}), }),
getEmoji: build.query({ getEmoji: build.query({
query: (id) => ({ query: (id) => ({
@ -43,20 +42,19 @@ const endpoints = (build) => ({
}), }),
providesTags: (res, error, id) => [{type: "Emojis", id}] providesTags: (res, error, id) => [{type: "Emojis", id}]
}), }),
addEmoji: build.query({ addEmoji: build.mutation({
query: (form) => { query: (form) => {
const body = getChanges(form, {
formKeys: ["shortcode"],
fileKeys: ["image"]
});
return { return {
method: "POST", method: "POST",
url: `/api/v1/admin/custom_emojis`, url: `/api/v1/admin/custom_emojis`,
asForm: true, 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({ deleteEmoji: build.mutation({
query: (id) => ({ query: (id) => ({

View file

@ -283,6 +283,12 @@ section.with-sidebar > div {
} }
} }
.form-flex {
display: flex;
flex-direction: column;
gap: 1rem;
}
.file-upload > div { .file-upload > div {
display: flex; display: flex;
gap: 1rem; gap: 1rem;