refactor custom emoji form fields

This commit is contained in:
f0x 2022-11-07 19:46:44 +00:00
parent 41f25653d1
commit 90fa9a3ce6
9 changed files with 325 additions and 176 deletions

View file

@ -261,12 +261,16 @@ section.login {
} }
section.error { section.error {
word-break: break-word;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
margin-bottom: 0.5rem;
span { span {
font-size: 2em; font-size: 2em;
} }
pre { pre {
border: 1px solid #ff000080; border: 1px solid #ff000080;
margin-left: 1em; margin-left: 1em;

View file

@ -0,0 +1,133 @@
/*
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 Promise = require('bluebird');
const React = require("react");
const FakeToot = require("../../components/fake-toot");
const MutateButton = require("../../components/mutation-button");
const {
useTextInput,
useFileInput
} = require("../../components/form");
const query = require("../../lib/query");
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", {
withPreview: true,
maxSize: 50 * 1000
});
const [onShortcodeChange, resetShortcode, {shortcode, setShortcode, shortcodeRef}] = useTextInput("shortcode", {
validator: function validateShortcode(code) {
return emojiCodes.has(code)
? "Shortcode already in use"
: "";
}
});
React.useEffect(() => {
if (shortcode.length == 0) {
if (image != undefined) {
let [name, _ext] = image.name.split(".");
setShortcode(name);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [image]);
function uploadEmoji(e) {
if (e) {
e.preventDefault();
}
Promise.try(() => {
return addEmoji({
image,
shortcode
});
}).then(() => {
resetFile();
resetShortcode();
});
}
let emojiOrShortcode = `:${shortcode}:`;
if (imageURL != undefined) {
emojiOrShortcode = <img
className="emoji"
src={imageURL}
title={`:${shortcode}:`}
alt={shortcode}
/>;
}
return (
<div>
<h2>Add new custom emoji</h2>
<FakeToot>
Look at this new custom emoji {emojiOrShortcode} isn&apos;t it cool?
</FakeToot>
<form onSubmit={uploadEmoji} className="form-flex">
<div className="form-field file">
<label className="file-input button" htmlFor="image">
Browse
</label>
{imageInfo}
<input
className="hidden"
type="file"
id="image"
name="Image"
accept="image/png,image/gif"
onChange={onFileChange}
/>
</div>
<div className="form-field text">
<label htmlFor="shortcode">
Shortcode, must be unique among the instance's local emoji
</label>
<input
type="text"
id="shortcode"
name="Shortcode"
ref={shortcodeRef}
onChange={onShortcodeChange}
value={shortcode}
/>
</div>
<MutateButton text="Upload emoji" result={result}/>
</form>
</div>
);
};

View file

@ -18,14 +18,11 @@
"use strict"; "use strict";
const Promise = require("bluebird");
const React = require("react"); const React = require("react");
const {Link} = require("wouter"); const {Link} = require("wouter");
const defaultValue = require('default-value'); const defaultValue = require('default-value');
const prettierBytes = require("prettier-bytes");
const FakeToot = require("../../components/fake-toot"); const NewEmojiForm = require("./new-emoji");
const MutateButton = require("../../components/mutation-button");
const query = require("../../lib/query"); const query = require("../../lib/query");
@ -48,7 +45,7 @@ module.exports = function EmojiOverview() {
? "Loading..." ? "Loading..."
: <> : <>
<EmojiList emoji={emoji}/> <EmojiList emoji={emoji}/>
<NewEmoji emoji={emoji}/> <NewEmojiForm emoji={emoji}/>
</> </>
} }
</> </>
@ -100,159 +97,3 @@ function EmojiCategory({category, entries}) {
</div> </div>
); );
} }
function useFileInput({withPreview, maxSize}) {
const [file, setFile] = React.useState();
const [imageURL, setImageURL] = React.useState();
const [info, setInfo] = React.useState("no file selected");
function onChange(e) {
let file = e.target.files[0];
setFile(file);
URL.revokeObjectURL(imageURL);
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,
{
file,
imageURL,
info: <span>{info}</span>,
}
];
}
// 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
});
const [shortcode, setShortcode] = React.useState("");
const shortcodeRef = React.useRef(null);
function onShortChange(e) {
let input = e.target.value;
setShortcode(input);
}
React.useEffect(() => {
if (emojiCodes.has(shortcode)) {
shortcodeRef.current.setCustomValidity("Shortcode already in use");
} else {
shortcodeRef.current.setCustomValidity("");
}
shortcodeRef.current.reportValidity();
}, [shortcode, shortcodeRef, emojiCodes]);
React.useEffect(() => {
if (shortcode.length == 0) {
if (file != undefined) {
let [name, _ext] = file.name.split(".");
setShortcode(name);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [file]);
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
className="emoji"
src={imageURL}
title={`:${shortcode}:`}
alt={shortcode}
/>;
}
return (
<div>
<h2>Add new custom emoji</h2>
<FakeToot>
Look at this new custom emoji {emojiOrShortcode} isn&apos;t it cool?
</FakeToot>
<form onSubmit={uploadEmoji} className="form-flex">
<div className="form-field file">
<label className="file-input button" htmlFor="image">
Browse
</label>
{info}
<input
className="hidden"
type="file"
id="image"
name="Image"
accept="image/png,image/gif"
onChange={onFileChange}
/>
</div>
<div className="form-field text">
<label htmlFor="shortcode">
Shortcode, must be unique among the instance's local emoji
</label>
<input
type="text"
id="shortcode"
name="Shortcode"
ref={shortcodeRef}
onChange={onShortChange}
value={shortcode}
/>
</div>
<MutateButton text="Upload emoji" result={result}/>
</form>
</div>
);
}

View file

@ -119,7 +119,7 @@ module.exports = {
field = ( field = (
<> <>
<label htmlFor={id} className="file-input button">Browse</label> <label htmlFor={id} className="file-input button">Browse</label>
<span> <span className="form-info">
{file ? file.name : "no file selected"} {size} {file ? file.name : "no file selected"} {size}
</span> </span>
{/* <a onClick={removeFile("header")}>remove</a> */} {/* <a onClick={removeFile("header")}>remove</a> */}

View file

@ -0,0 +1,78 @@
/*
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");
const prettierBytes = require("prettier-bytes");
module.exports = function useFileInput({name, Name}, {
withPreview,
maxSize,
initialInfo = "no file selected"
}) {
const [file, setFile] = React.useState();
const [imageURL, setImageURL] = React.useState();
const [info, setInfo] = React.useState();
function onChange(e) {
let file = e.target.files[0];
setFile(file);
URL.revokeObjectURL(imageURL);
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();
}
}
function reset() {
URL.revokeObjectURL(imageURL);
setImageURL();
setFile();
setInfo();
}
return [
onChange,
reset,
{
[name]: file,
[`${name}URL`]: imageURL,
[`${name}Info`]: <span className="form-info">
{info
? info
: initialInfo
}
</span>
}
];
};

View file

@ -0,0 +1,36 @@
/*
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";
function capitalizeFirst(str) {
return str.slice(0,1).toUpperCase()+str.slice(1);
}
function makeHook(func) {
return (name, ...args) => func({
name,
Name: capitalizeFirst(name)
},
...args);
}
module.exports = {
useTextInput: makeHook(require("./text")),
useFileInput: makeHook(require("./file"))
};

View file

@ -0,0 +1,56 @@
/*
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 useTextInput({name, Name}, {validator} = {}) {
const [text, setText] = React.useState("");
const textRef = React.useRef(null);
function onChange(e) {
let input = e.target.value;
setText(input);
if (validator) {
validator(input);
}
}
function reset() {
setText("");
}
React.useEffect(() => {
if (validator) {
textRef.current.setCustomValidity(validator(text));
textRef.current.reportValidity();
}
}, [text, textRef, validator]);
return [
onChange,
reset,
{
[name]: text,
[`${name}Ref`]: textRef,
[`set${Name}`]: setText
}
];
};

View file

@ -29,7 +29,7 @@ module.exports = function MutateButton({text, result}) {
return (<div> return (<div>
{result.error && {result.error &&
<div className="error">{result.error.status}: {result.error.error}</div> <section className="error">{result.error.status}: {result.error.data.error}</section>
} }
<input <input
className="button" className="button"

View file

@ -336,19 +336,6 @@ section.with-sidebar > div {
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
div.form-field {
width: 100%;
display: flex;
span {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0.3rem 0;
}
}
h3 { h3 {
margin-top: 0; margin-top: 0;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
@ -369,6 +356,20 @@ section.with-sidebar > div {
font-weight: bold; font-weight: bold;
} }
.form-field.file {
width: 100%;
display: flex;
}
span.form-info {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0.3rem 0;
}
.list { .list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;