diff options
Diffstat (limited to 'web/source/settings/admin')
-rw-r--r-- | web/source/settings/admin/emoji/category-select.jsx | 96 | ||||
-rw-r--r-- | web/source/settings/admin/emoji/detail.js | 148 | ||||
-rw-r--r-- | web/source/settings/admin/emoji/new-emoji.js | 29 | ||||
-rw-r--r-- | web/source/settings/admin/emoji/overview.js | 17 |
4 files changed, 222 insertions, 68 deletions
diff --git a/web/source/settings/admin/emoji/category-select.jsx b/web/source/settings/admin/emoji/category-select.jsx new file mode 100644 index 000000000..3a2ace89b --- /dev/null +++ b/web/source/settings/admin/emoji/category-select.jsx @@ -0,0 +1,96 @@ +/* + 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 splitFilterN = require("split-filter-n"); +const syncpipe = require('syncpipe'); +const { matchSorter } = require("match-sorter"); + +const query = require("../../lib/query"); + +const ComboBox = require("../../components/combo-box"); + +function useEmojiByCategory(emoji) { + // split all emoji over an object keyed by the category names (or Unsorted) + return React.useMemo(() => splitFilterN( + emoji, + [], + (entry) => entry.category ?? "Unsorted" + ), [emoji]); +} + +function CategorySelect({value, categoryState, setIsNew=() => {}, children}) { + const { + data: emoji = [], + isLoading, + isSuccess, + error + } = query.useGetAllEmojiQuery({filter: "domain:local"}); + + const emojiByCategory = useEmojiByCategory(emoji); + + const categories = React.useMemo(() => new Set(Object.keys(emojiByCategory)), [emojiByCategory]); + + // data used by the ComboBox element to select an emoji category + const categoryItems = React.useMemo(() => { + return syncpipe(emojiByCategory, [ + (_) => Object.keys(_), // just emoji category names + (_) => matchSorter(_, value, {threshold: matchSorter.rankings.NO_MATCH}), // sorted by complex algorithm + (_) => _.map((categoryName) => [ // map to input value, and selectable element with icon + categoryName, + <> + <img src={emojiByCategory[categoryName][0].static_url} aria-hidden="true"></img> + {categoryName} + </> + ]) + ]); + }, [emojiByCategory, value]); + + React.useEffect(() => { + if (value != undefined && isSuccess && value.trim().length > 0) { + setIsNew(!categories.has(value.trim())); + } + }, [categories, value, setIsNew, isSuccess]); + + if (error) { // fall back to plain text input, but this would almost certainly have caused a bigger error message elsewhere + return ( + <> + <input type="text" placeholder="e.g., reactions" onChange={(e) => {categoryState.value = e.target.value;}}/>; + </> + ); + } else if (isLoading) { + return <input type="text" value="Loading categories..." disabled={true}/>; + } + + return ( + <ComboBox + state={categoryState} + items={categoryItems} + label="Category" + placeHolder="e.g., reactions" + children={children} + /> + ); +} + +module.exports = { + useEmojiByCategory, + CategorySelect +};
\ No newline at end of file diff --git a/web/source/settings/admin/emoji/detail.js b/web/source/settings/admin/emoji/detail.js index cc0f8e73c..266084718 100644 --- a/web/source/settings/admin/emoji/detail.js +++ b/web/source/settings/admin/emoji/detail.js @@ -22,48 +22,130 @@ const React = require("react"); const { useRoute, Link, Redirect } = require("wouter"); -const BackButton = require("../../components/back-button"); +const { CategorySelect } = require("./category-select"); +const { useComboBoxInput, useFileInput } = require("../../components/form"); const query = require("../../lib/query"); +const FakeToot = require("../../components/fake-toot"); const base = "/settings/admin/custom-emoji"; -/* We wrap the component to generate formFields with a setter depending on the domain - if formFields() is used inside the same component that is re-rendered with their state, - inputs get re-created on every change, causing them to lose focus, and bad performance -*/ -module.exports = function EmojiDetailWrapped() { - let [_match, {emojiId}] = useRoute(`${base}/:emojiId`); +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); - return (<> - {error && <div className="error accent">{error.status}: {error.data.error}</div>} - {isLoading - ? "Loading..." - : <EmojiDetail emoji={emoji}/> - } - </>); -}; + if (error) { + return ( + <div className="error accent"> + {error.status}: {error.data.error} + </div> + ); + } else if (isLoading) { + return "Loading..."; + } else { + return <EmojiDetail emoji={emoji}/>; + } +} function EmojiDetail({emoji}) { - if (emoji == undefined) { - return (<> - <Link to={base}> - <a className="button">go back</a> - </Link> - </>); + 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 && emoji.category != undefined) { + console.log("updating to", category); + modifyEmoji({id: emoji.id, category: category.trim()}); + } + }, [isNewCategory, category, categoryState.open, emoji.category, emoji.id, modifyEmoji]); + return ( - <div> - <h1><BackButton to={base}/> Custom Emoji: {emoji.shortcode}</h1> - <DeleteButton id={emoji.id}/> - <p> - Editing custom emoji isn't implemented yet.<br/> - <a target="_blank" rel="noreferrer" href="https://github.com/superseriousbusiness/gotosocial/issues/797">View implementation progress.</a> - </p> - <img src={emoji.url} alt={emoji.shortcode} title={`:${emoji.shortcode}:`}/> - </div> + <> + <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> + </> ); } @@ -71,9 +153,9 @@ function DeleteButton({id}) { // TODO: confirmation dialog? const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation(); - let text = "Delete this emoji"; + let text = "Delete"; if (deleteResult.isLoading) { - text = "processing..."; + text = "Deleting..."; } if (deleteResult.isSuccess) { @@ -81,6 +163,6 @@ function DeleteButton({id}) { } return ( - <button onClick={() => deleteEmoji(id)} disabled={deleteResult.isLoading}>{text}</button> + <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/new-emoji.js b/web/source/settings/admin/emoji/new-emoji.js index 65dc52132..8cd604c02 100644 --- a/web/source/settings/admin/emoji/new-emoji.js +++ b/web/source/settings/admin/emoji/new-emoji.js @@ -20,11 +20,9 @@ const Promise = require('bluebird'); const React = require("react"); -const { matchSorter } = require("match-sorter"); const FakeToot = require("../../components/fake-toot"); const MutateButton = require("../../components/mutation-button"); -const ComboBox = require("../../components/combo-box"); const { useTextInput, @@ -33,9 +31,9 @@ const { } = require("../../components/form"); const query = require("../../lib/query"); -const syncpipe = require('syncpipe'); +const { CategorySelect } = require('./category-select'); -module.exports = function NewEmojiForm({ emoji, emojiByCategory }) { +module.exports = function NewEmojiForm({ emoji }) { const emojiCodes = React.useMemo(() => { return new Set(emoji.map((e) => e.shortcode)); }, [emoji]); @@ -57,21 +55,6 @@ module.exports = function NewEmojiForm({ emoji, emojiByCategory }) { const [categoryState, resetCategory, { category }] = useComboBoxInput("category"); - // data used by the ComboBox element to select an emoji category - const categoryItems = React.useMemo(() => { - return syncpipe(emojiByCategory, [ - (_) => Object.keys(_), // just emoji category names - (_) => matchSorter(_, category), // sorted by complex algorithm - (_) => _.map((categoryName) => [ // map to input value, and selectable element with icon - categoryName, - <> - <img src={emojiByCategory[categoryName][0].static_url} aria-hidden="true"></img> - {categoryName} - </> - ]) - ]); - }, [emojiByCategory, category]); - React.useEffect(() => { if (shortcode.length == 0) { if (image != undefined) { @@ -152,11 +135,9 @@ module.exports = function NewEmojiForm({ emoji, emojiByCategory }) { /> </div> - <ComboBox - state={categoryState} - items={categoryItems} - label="Category" - placeHolder="e.g., reactions" + <CategorySelect + value={category} + categoryState={categoryState} /> <MutateButton text="Upload emoji" result={result} /> diff --git a/web/source/settings/admin/emoji/overview.js b/web/source/settings/admin/emoji/overview.js index 15891a5ec..b8ac87a0f 100644 --- a/web/source/settings/admin/emoji/overview.js +++ b/web/source/settings/admin/emoji/overview.js @@ -20,11 +20,11 @@ const React = require("react"); const {Link} = require("wouter"); -const splitFilterN = require("split-filter-n"); const NewEmojiForm = require("./new-emoji"); const query = require("../../lib/query"); +const { useEmojiByCategory } = require("./category-select"); const base = "/settings/admin/custom-emoji"; @@ -35,13 +35,6 @@ module.exports = function EmojiOverview() { error } = query.useGetAllEmojiQuery({filter: "domain:local"}); - // split all emoji over an object keyed by the category names (or Unsorted) - const emojiByCategory = React.useMemo(() => splitFilterN( - emoji, - [], - (entry) => entry.category ?? "Unsorted" - ), [emoji]); - return ( <> <h1>Custom Emoji</h1> @@ -51,15 +44,17 @@ module.exports = function EmojiOverview() { {isLoading ? "Loading..." : <> - <EmojiList emoji={emoji} emojiByCategory={emojiByCategory}/> - <NewEmojiForm emoji={emoji} emojiByCategory={emojiByCategory}/> + <EmojiList emoji={emoji}/> + <NewEmojiForm emoji={emoji}/> </> } </> ); }; -function EmojiList({emoji, emojiByCategory}) { +function EmojiList({emoji}) { + const emojiByCategory = useEmojiByCategory(emoji); + return ( <div> <h2>Overview</h2> |