diff options
Diffstat (limited to 'web/source/settings')
| -rw-r--r-- | web/source/settings/admin/emoji/new-emoji.js | 49 | ||||
| -rw-r--r-- | web/source/settings/admin/emoji/overview.js | 33 | ||||
| -rw-r--r-- | web/source/settings/components/combo-box.jsx | 49 | ||||
| -rw-r--r-- | web/source/settings/components/form/combobox.jsx | 37 | ||||
| -rw-r--r-- | web/source/settings/components/form/index.js | 3 | ||||
| -rw-r--r-- | web/source/settings/redux/reducers/user.js | 9 | ||||
| -rw-r--r-- | web/source/settings/style.css | 58 | 
7 files changed, 205 insertions, 33 deletions
| diff --git a/web/source/settings/admin/emoji/new-emoji.js b/web/source/settings/admin/emoji/new-emoji.js index e5bc8893d..65dc52132 100644 --- a/web/source/settings/admin/emoji/new-emoji.js +++ b/web/source/settings/admin/emoji/new-emoji.js @@ -20,30 +20,34 @@  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 {  +const {  	useTextInput, -	useFileInput +	useFileInput, +	useComboBoxInput  } = require("../../components/form");  const query = require("../../lib/query"); +const syncpipe = require('syncpipe'); -module.exports = function NewEmojiForm({emoji}) { +module.exports = function NewEmojiForm({ emoji, emojiByCategory }) {  	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", { +	const [onFileChange, resetFile, { image, imageURL, imageInfo }] = useFileInput("image", {  		withPreview: true,  		maxSize: 50 * 1024  	}); -	const [onShortcodeChange, resetShortcode, {shortcode, setShortcode, shortcodeRef}] = useTextInput("shortcode", { +	const [onShortcodeChange, resetShortcode, { shortcode, setShortcode, shortcodeRef }] = useTextInput("shortcode", {  		validator: function validateShortcode(code) {  			return emojiCodes.has(code)  				? "Shortcode already in use" @@ -51,6 +55,23 @@ module.exports = function NewEmojiForm({emoji}) {  		}  	}); +	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) { @@ -58,6 +79,9 @@ module.exports = function NewEmojiForm({emoji}) {  				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]); @@ -69,11 +93,13 @@ module.exports = function NewEmojiForm({emoji}) {  		Promise.try(() => {  			return addEmoji({  				image, -				shortcode +				shortcode, +				category  			});  		}).then(() => {  			resetFile();  			resetShortcode(); +			resetCategory();  		});  	} @@ -125,8 +151,15 @@ module.exports = function NewEmojiForm({emoji}) {  						value={shortcode}  					/>  				</div> -				 -				<MutateButton text="Upload emoji" result={result}/> + +				<ComboBox +					state={categoryState} +					items={categoryItems} +					label="Category" +					placeHolder="e.g., reactions" +				/> + +				<MutateButton text="Upload emoji" result={result} />  			</form>  		</div>  	); diff --git a/web/source/settings/admin/emoji/overview.js b/web/source/settings/admin/emoji/overview.js index 028276da2..15891a5ec 100644 --- a/web/source/settings/admin/emoji/overview.js +++ b/web/source/settings/admin/emoji/overview.js @@ -20,7 +20,7 @@  const React = require("react");  const {Link} = require("wouter"); -const defaultValue = require('default-value'); +const splitFilterN = require("split-filter-n");  const NewEmojiForm = require("./new-emoji"); @@ -30,11 +30,18 @@ const base = "/settings/admin/custom-emoji";  module.exports = function EmojiOverview() {  	const { -		data: emoji, +		data: emoji = [],  		isLoading,  		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> @@ -44,33 +51,21 @@ module.exports = function EmojiOverview() {  			{isLoading  				? "Loading..."  				: <> -					<EmojiList emoji={emoji}/> -					<NewEmojiForm emoji={emoji}/> +					<EmojiList emoji={emoji} emojiByCategory={emojiByCategory}/> +					<NewEmojiForm emoji={emoji} emojiByCategory={emojiByCategory}/>  				</>  			}  		</>  	);  }; -function EmojiList({emoji}) { -	const byCategory = React.useMemo(() => { -		const categories = {}; - -		emoji.forEach((emoji) => { -			let cat = defaultValue(emoji.category, "Unsorted"); -			categories[cat] = defaultValue(categories[cat], []); -			categories[cat].push(emoji); -		}); - -		return categories; -	}, [emoji]); -	 +function EmojiList({emoji, emojiByCategory}) {  	return (  		<div>  			<h2>Overview</h2>  			<div className="list emoji-list"> -				{emoji.length == 0 && "No local emoji yet"} -				{Object.entries(byCategory).map(([category, entries]) => { +				{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> diff --git a/web/source/settings/components/combo-box.jsx b/web/source/settings/components/combo-box.jsx new file mode 100644 index 000000000..1e6293890 --- /dev/null +++ b/web/source/settings/components/combo-box.jsx @@ -0,0 +1,49 @@ +/* +	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 { +	Combobox, +	ComboboxItem, +	ComboboxPopover, +} = require("ariakit/combobox"); + +module.exports = function ComboBox({state, items, label, placeHolder}) { +	return ( +		<div className="form-field combobox-wrapper"> +			<label> +				{label} +				<Combobox +					state={state} +					placeholder={placeHolder} +					className="combobox input" +				/> +			</label> +			<ComboboxPopover state={state} className="popover"> +				{items.map(([key, value]) => ( +					<ComboboxItem className="combobox-item" key={key} value={key}> +						{value} +					</ComboboxItem> +				))} +			</ComboboxPopover> +		</div> +	); +};
\ No newline at end of file diff --git a/web/source/settings/components/form/combobox.jsx b/web/source/settings/components/form/combobox.jsx new file mode 100644 index 000000000..6ab235ed3 --- /dev/null +++ b/web/source/settings/components/form/combobox.jsx @@ -0,0 +1,37 @@ +/* +	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 { useComboboxState } = require("ariakit/combobox"); + +module.exports = function useComboBoxInput({name, Name}, {validator} = {}) { +	const state = useComboboxState({ gutter: 0, sameWidth: true }); + +	function reset() { +		state.value = ""; +	} + +	return [ +		state, +		reset, +		{ +			[name]: state.value, +		} +	]; +};
\ No newline at end of file diff --git a/web/source/settings/components/form/index.js b/web/source/settings/components/form/index.js index 5edc52364..e226a4b04 100644 --- a/web/source/settings/components/form/index.js +++ b/web/source/settings/components/form/index.js @@ -32,5 +32,6 @@ function makeHook(func) {  module.exports = {  	useTextInput: makeHook(require("./text")), -	useFileInput: makeHook(require("./file")) +	useFileInput: makeHook(require("./file")), +	useComboBoxInput: makeHook(require("./combobox"))  };
\ No newline at end of file diff --git a/web/source/settings/redux/reducers/user.js b/web/source/settings/redux/reducers/user.js index b4463c9f9..861f519d1 100644 --- a/web/source/settings/redux/reducers/user.js +++ b/web/source/settings/redux/reducers/user.js @@ -20,7 +20,6 @@  const { createSlice } = require("@reduxjs/toolkit");  const d = require("dotty"); -const defaultValue = require("default-value");  module.exports = createSlice({  	name: "user", @@ -30,10 +29,10 @@ module.exports = createSlice({  	},  	reducers: {  		setAccount: (state, { payload }) => { -			payload.source = defaultValue(payload.source, {}); -			payload.source.language = defaultValue(payload.source.language.toUpperCase(), "EN"); -			payload.source.status_format = defaultValue(payload.source.status_format, "plain"); -			payload.source.sensitive = defaultValue(payload.source.sensitive, false); +			payload.source = payload.source ?? {}; +			payload.source.language = payload.source.language.toUpperCase() ?? "EN"; +			payload.source.status_format = payload.source.status_format ?? "plain"; +			payload.source.sensitive = payload.source.sensitive ?? false;  			state.profile = payload;  			// /user/settings only needs a copy of the 'source' obj diff --git a/web/source/settings/style.css b/web/source/settings/style.css index 93e52f680..3af52337a 100644 --- a/web/source/settings/style.css +++ b/web/source/settings/style.css @@ -502,4 +502,62 @@ span.form-info {  	.instance-list .filter {  		flex-direction: column;  	} +} + +.combobox-wrapper { +  display: flex; +  flex-direction: column; + +	input[aria-expanded="true"] { +		border-bottom: none; +	} +} + +.combobox { +  height: 2.5rem; +  font-size: 1rem; +  line-height: 1.5rem; +} + +.popover { +  position: relative; +  z-index: 50; +  display: flex; +  max-height: min(var(--popover-available-height,300px),300px); +  flex-direction: column; +  overflow: auto; +  overscroll-behavior: contain; +	border: 0.15rem solid $orange2; +	background: $bg-accent; +} + +.combobox-item { +  display: flex; +  cursor: pointer; +  scroll-margin: 0.5rem; +  align-items: center; +  gap: 0.5rem; +  padding: 0.5rem; +	line-height: 1.5rem; +	border-bottom: 0.15rem solid $gray3; + +	&:last-child { +		border: none; +	} + +	img { +		height: 1.5rem; +		width: 1.5rem; +		object-fit: contain; +	} +} + +.combobox-item:hover { +	background: $button-hover-bg; +	color: $button-fg; +} + +.combobox-item[data-active-item] { +  background: $button-hover-bg; +  color: hsl(204 20% 100%);  }
\ No newline at end of file | 
