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 .
+*/
+
+"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,
+ <>
+
+ {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 (
+ <>
+ {categoryState.value = e.target.value;}}/>;
+ >
+ );
+ } else if (isLoading) {
+ return ;
+ }
+
+ return (
+
+ );
+}
+
+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`);
- const {currentData: emoji, isLoading, error} = query.useGetEmojiQuery(emojiId);
-
- return (<>
- {error &&