diff options
| author | 2022-11-25 15:49:48 +0100 | |
|---|---|---|
| committer | 2022-11-25 15:49:48 +0100 | |
| commit | 665d902fd72ef4fffb273502b6f010a94a47e15b (patch) | |
| tree | 8fb9eb7aec5cad2ccf96ae1ebe276f18ad0735c0 /web/source/settings/admin | |
| parent | [feature] `PATCH /api/v1/admin/custom_emojis/{id}` endpoint (#1061) (diff) | |
| download | gotosocial-665d902fd72ef4fffb273502b6f010a94a47e15b.tar.xz | |
[feature/frogend] modify local emoji (#1143)
* update danger button red
* emoji category and image modification
* debug bundles in dev
* fix linting error
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> | 
