diff options
Diffstat (limited to 'web/source')
| -rw-r--r-- | web/source/css/_colors.css | 7 | ||||
| -rw-r--r-- | web/source/css/base.css | 10 | ||||
| -rw-r--r-- | web/source/index.js | 1 | ||||
| -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 | ||||
| -rw-r--r-- | web/source/settings/components/combo-box.jsx | 15 | ||||
| -rw-r--r-- | web/source/settings/components/form/combobox.jsx | 10 | ||||
| -rw-r--r-- | web/source/settings/lib/query/custom-emoji.js | 17 | ||||
| -rw-r--r-- | web/source/settings/style.css | 52 | 
11 files changed, 321 insertions, 81 deletions
diff --git a/web/source/css/_colors.css b/web/source/css/_colors.css index 82028dce6..70c12486e 100644 --- a/web/source/css/_colors.css +++ b/web/source/css/_colors.css @@ -46,6 +46,7 @@ $blue3: #89caff; /* hover/selected accent to $blue2, can be used with $gray1 (7.  $error1: #860000; /* Error border/foreground text, can be used with $error2 (5.0), $white1 (10), $white2 (5.1) */  $error2: #ff9796; /* Error background text, can be used with $error1 (5.0), $gray1 (6.6), $gray2 (5.3), $gray3 (4.8) */ +$error3: #dd2c2c; /* Error button background text, can be used with $white1 (4.51) */  $error-link: #185F8C; /* Error link text, can be used with $error2 (5.54) */  $fg: $white1; @@ -69,9 +70,9 @@ $button-bg: $blue2;  $button-fg: $gray1;  $button-hover-bg: $blue3; -$button-danger-bg: $orange1; -$button-danger-fg: $gray1; -$button-danger-hover-bg: $orange2; +$button-danger-bg: $error3; +$button-danger-fg: $white1; +$button-danger-hover-bg: $error2;  $toot-focus-bg: $gray5;  $toot-unfocus-bg: $gray2; diff --git a/web/source/css/base.css b/web/source/css/base.css index 760189be3..73014de8d 100644 --- a/web/source/css/base.css +++ b/web/source/css/base.css @@ -172,6 +172,16 @@ main {  		}  	} +	&:disabled { +		color: $white2; +		background: $gray2; +		cursor: auto; + +		&:hover { +			background: $gray3; +		} +	} +  	&:hover {  		background: $button-hover-bg;  	} diff --git a/web/source/index.js b/web/source/index.js index 90ee5a4ea..a96e663cd 100644 --- a/web/source/index.js +++ b/web/source/index.js @@ -66,7 +66,6 @@ skulk({  			],  		},  		settings: { -			debug: false,  			entryFile: "settings",  			outputFile: "settings.js",  			prodCfg: prodCfg, 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> diff --git a/web/source/settings/components/combo-box.jsx b/web/source/settings/components/combo-box.jsx index 1e6293890..d69df55b1 100644 --- a/web/source/settings/components/combo-box.jsx +++ b/web/source/settings/components/combo-box.jsx @@ -26,16 +26,19 @@ const {  	ComboboxPopover,  } = require("ariakit/combobox"); -module.exports = function ComboBox({state, items, label, placeHolder}) { +module.exports = function ComboBox({state, items, label, placeHolder, children}) {  	return (  		<div className="form-field combobox-wrapper">  			<label>  				{label} -				<Combobox -					state={state} -					placeholder={placeHolder} -					className="combobox input" -				/> +				<div className="row"> +					<Combobox +						state={state} +						placeholder={placeHolder} +						className="combobox input" +					/> +					{children} +				</div>  			</label>  			<ComboboxPopover state={state} className="popover">  				{items.map(([key, value]) => ( diff --git a/web/source/settings/components/form/combobox.jsx b/web/source/settings/components/form/combobox.jsx index 6ab235ed3..d21a8c3f3 100644 --- a/web/source/settings/components/form/combobox.jsx +++ b/web/source/settings/components/form/combobox.jsx @@ -20,11 +20,15 @@  const { useComboboxState } = require("ariakit/combobox"); -module.exports = function useComboBoxInput({name, Name}, {validator} = {}) { -	const state = useComboboxState({ gutter: 0, sameWidth: true }); +module.exports = function useComboBoxInput({name, Name}, {validator, defaultValue} = {}) { +	const state = useComboboxState({ +		defaultValue, +		gutter: 0, +		sameWidth: true +	});  	function reset() { -		state.value = ""; +		state.setValue("");  	}  	return [ diff --git a/web/source/settings/lib/query/custom-emoji.js b/web/source/settings/lib/query/custom-emoji.js index a26da75ca..fa2a08db6 100644 --- a/web/source/settings/lib/query/custom-emoji.js +++ b/web/source/settings/lib/query/custom-emoji.js @@ -54,6 +54,23 @@ const endpoints = (build) => ({  				? [{type: "Emojis", id: "LIST"}, {type: "Emojis", id: res.id}]  				: [{type: "Emojis", id: "LIST"}]  	}), +	editEmoji: build.mutation({ +		query: ({id, ...patch}) => { +			return { +				method: "PATCH", +				url: `/api/v1/admin/custom_emojis/${id}`, +				asForm: true, +				body: { +					type: "modify", +					...patch +				} +			}; +		}, +		invalidatesTags: (res) =>  +			res +				? [{type: "Emojis", id: "LIST"}, {type: "Emojis", id: res.id}] +				: [{type: "Emojis", id: "LIST"}] +	}),  	deleteEmoji: build.mutation({  		query: (id) => ({  			method: "DELETE", diff --git a/web/source/settings/style.css b/web/source/settings/style.css index 2a32fc0ad..7922a4053 100644 --- a/web/source/settings/style.css +++ b/web/source/settings/style.css @@ -544,4 +544,56 @@ span.form-info {  .combobox-item[data-active-item] {    background: $button-hover-bg;    color: hsl(204 20% 100%); +} + +.row { +	display: flex; +} + +.emoji-detail { +	display: flex; +	flex-direction: column; +	gap: 1rem !important; + +	& > a { +		align-self: flex-start; +	} + +	.emoji-header { +		display: flex; +		align-items: center; +		gap: 0.5rem; + +		img { +			height: 8.5rem; +			width: 8.5rem; +			border: 0.2rem solid $border-accent; +			object-fit: contain; +			padding: 0.5rem; +		} +	} + +	.update-category { +		margin-bottom: 1rem; +		.combobox-wrapper button { +			font-size: 1rem; +			margin: 0.15rem 0; +		} +	 +		.row { +			margin-top: 0.4rem; +			gap: 0.5rem; +		} +	} + +	.update-image { +		display: flex; +		flex-direction: column; +		gap: 0.5rem; +	} +} + +.left-border { +	border-left: 0.2rem solid $border-accent; +	padding-left: 0.4rem;  }
\ No newline at end of file  | 
