diff options
Diffstat (limited to 'web/source/settings/admin/emoji/local')
-rw-r--r-- | web/source/settings/admin/emoji/local/detail.js | 173 | ||||
-rw-r--r-- | web/source/settings/admin/emoji/local/index.js | 40 | ||||
-rw-r--r-- | web/source/settings/admin/emoji/local/new-emoji.js | 168 | ||||
-rw-r--r-- | web/source/settings/admin/emoji/local/overview.js | 90 |
4 files changed, 471 insertions, 0 deletions
diff --git a/web/source/settings/admin/emoji/local/detail.js b/web/source/settings/admin/emoji/local/detail.js new file mode 100644 index 000000000..179ee7c7c --- /dev/null +++ b/web/source/settings/admin/emoji/local/detail.js @@ -0,0 +1,173 @@ +/* + 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 { useRoute, Link, Redirect } = require("wouter"); + +const { CategorySelect } = require("../category-select"); +const { useComboBoxInput, useFileInput } = require("../../../components/form"); + +const query = require("../../../lib/query"); +const FakeToot = require("../../../components/fake-toot"); +const Loading = require("../../../components/loading"); + +const base = "/settings/custom-emoji/local"; + +module.exports = function EmojiDetailRoute() { + let [_match, params] = useRoute(`${base}/:emojiId`); + if (params?.emojiId == undefined) { + return <Redirect to={base}/>; + } else { + return ( + <div className="emoji-detail"> + <Link to={base}><a>< go back</a></Link> + <EmojiDetailData emojiId={params.emojiId}/> + </div> + ); + } +}; + +function EmojiDetailData({emojiId}) { + const {currentData: emoji, isLoading, error} = query.useGetEmojiQuery(emojiId); + + if (error) { + return ( + <div className="error accent"> + {error.status}: {error.data.error} + </div> + ); + } else if (isLoading) { + return ( + <div> + <Loading/> + </div> + ); + } else { + return <EmojiDetail emoji={emoji}/>; + } +} + +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}); + } + + 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 ( + <> + <div className="emoji-header"> + <img src={emoji.url} alt={emoji.shortcode} title={emoji.shortcode}/> + <div> + <h2>{emoji.shortcode}</h2> + <DeleteButton id={emoji.id}/> + </div> + </div> + + <div className="left-border"> + <h2>Modify this emoji {modifyResult.isLoading && "(processing..)"}</h2> + + {modifyResult.error && <div className="error"> + {modifyResult.error.status}: {modifyResult.error.data.error} + </div>} + + <div className="update-category"> + <CategorySelect + value={category} + categoryState={categoryState} + setIsNew={setIsNewCategory} + > + <button style={{visibility: (isNewCategory ? "initial" : "hidden")}} onClick={modifyCategory}> + Create + </button> + </CategorySelect> + </div> + + <div className="update-image"> + <b>Image</b> + <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> + + <button onClick={modifyImage} disabled={image == undefined}>Replace image</button> + + <FakeToot> + Look at this new custom emoji <img + className="emoji" + src={imageURL ?? emoji.url} + title={`:${emoji.shortcode}:`} + alt={emoji.shortcode} + /> isn't it cool? + </FakeToot> + </div> + </div> + </> + ); +} + +function DeleteButton({id}) { + // TODO: confirmation dialog? + const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation(); + + let text = "Delete"; + if (deleteResult.isLoading) { + text = "Deleting..."; + } + + if (deleteResult.isSuccess) { + return <Redirect to={base}/>; + } + + return ( + <button className="danger" onClick={() => deleteEmoji(id)} disabled={deleteResult.isLoading}>{text}</button> + ); +}
\ 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 new file mode 100644 index 000000000..1ccdece72 --- /dev/null +++ b/web/source/settings/admin/emoji/local/index.js @@ -0,0 +1,40 @@ +/* + 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 {Switch, Route} = require("wouter"); + +const EmojiOverview = require("./overview"); +const EmojiDetail = require("./detail"); + +const base = "/settings/custom-emoji/local"; + +module.exports = function CustomEmoji() { + return ( + <> + <Switch> + <Route path={`${base}/:emojiId`}> + <EmojiDetail /> + </Route> + <EmojiOverview /> + </Switch> + </> + ); +}; diff --git a/web/source/settings/admin/emoji/local/new-emoji.js b/web/source/settings/admin/emoji/local/new-emoji.js new file mode 100644 index 000000000..985be2d32 --- /dev/null +++ b/web/source/settings/admin/emoji/local/new-emoji.js @@ -0,0 +1,168 @@ +/* + 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, + useComboBoxInput +} = require("../../../components/form"); + +const query = require("../../../lib/query"); +const { CategorySelect } = require('../category-select'); + +const shortcodeRegex = /^[a-z0-9_]+$/; + +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 * 1024 + }); + + 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"; + } + + 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"); + + React.useEffect(() => { + if (shortcode.length == 0) { + if (image != undefined) { + let [name, _ext] = image.name.split("."); + setShortcode(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); + }); + } + + 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'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> + + <CategorySelect + value={category} + categoryState={categoryState} + /> + + <MutateButton text="Upload emoji" result={result} /> + </form> + </div> + ); +};
\ No newline at end of file diff --git a/web/source/settings/admin/emoji/local/overview.js b/web/source/settings/admin/emoji/local/overview.js new file mode 100644 index 000000000..7a5cfaad6 --- /dev/null +++ b/web/source/settings/admin/emoji/local/overview.js @@ -0,0 +1,90 @@ +/* + 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 {Link} = require("wouter"); + +const NewEmojiForm = require("./new-emoji"); + +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() { + const { + data: emoji = [], + isLoading, + error + } = query.useGetAllEmojiQuery({filter: "domain:local"}); + + return ( + <> + <h1>Custom Emoji (local)</h1> + {error && + <div className="error accent">{error}</div> + } + {isLoading + ? <Loading/> + : <> + <EmojiList emoji={emoji}/> + <NewEmojiForm emoji={emoji}/> + </> + } + </> + ); +}; + +function EmojiList({emoji}) { + const emojiByCategory = useEmojiByCategory(emoji); + + return ( + <div> + <h2>Overview</h2> + <div className="list emoji-list"> + {emoji.length == 0 && "No local emoji yet, add one below"} + {Object.entries(emojiByCategory).map(([category, entries]) => { + return <EmojiCategory key={category} category={category} entries={entries}/>; + })} + </div> + </div> + ); +} + +function EmojiCategory({category, entries}) { + return ( + <div className="entry"> + <b>{category}</b> + <div className="emoji-group"> + {entries.map((e) => { + return ( + <Link key={e.id} to={`${base}/${e.id}`}> + {/* <Link key={e.static_url} to={`${base}`}> */} + <a> + <img src={e.url} alt={e.shortcode} title={`:${e.shortcode}:`}/> + </a> + </Link> + ); + })} + </div> + </div> + ); +}
\ No newline at end of file |