summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--web/source/css/base.css10
-rw-r--r--web/source/settings/admin/emoji/local/detail.js (renamed from web/source/settings/admin/emoji/detail.js)17
-rw-r--r--web/source/settings/admin/emoji/local/index.js (renamed from web/source/settings/admin/emoji/index.js)2
-rw-r--r--web/source/settings/admin/emoji/local/new-emoji.js (renamed from web/source/settings/admin/emoji/new-emoji.js)39
-rw-r--r--web/source/settings/admin/emoji/local/overview.js (renamed from web/source/settings/admin/emoji/overview.js)11
-rw-r--r--web/source/settings/admin/emoji/remote/index.js54
-rw-r--r--web/source/settings/admin/emoji/remote/parse-from-toot.js319
-rw-r--r--web/source/settings/admin/federation.js7
-rw-r--r--web/source/settings/components/form/text.jsx16
-rw-r--r--web/source/settings/components/loading.jsx27
-rw-r--r--web/source/settings/index.js9
-rw-r--r--web/source/settings/lib/query/custom-emoji.js97
-rw-r--r--web/source/settings/style.css48
13 files changed, 623 insertions, 33 deletions
diff --git a/web/source/css/base.css b/web/source/css/base.css
index d2fa95a3b..73b533733 100644
--- a/web/source/css/base.css
+++ b/web/source/css/base.css
@@ -394,3 +394,13 @@ footer {
color: $gray1;
}
}
+
+label {
+ cursor: pointer;
+}
+
+@media (prefers-reduced-motion) {
+ .fa-spin {
+ animation: none;
+ }
+} \ No newline at end of file
diff --git a/web/source/settings/admin/emoji/detail.js b/web/source/settings/admin/emoji/local/detail.js
index 51e291448..179ee7c7c 100644
--- a/web/source/settings/admin/emoji/detail.js
+++ b/web/source/settings/admin/emoji/local/detail.js
@@ -22,13 +22,14 @@ const React = require("react");
const { useRoute, Link, Redirect } = require("wouter");
-const { CategorySelect } = require("./category-select");
-const { useComboBoxInput, useFileInput } = require("../../components/form");
+const { CategorySelect } = require("../category-select");
+const { useComboBoxInput, useFileInput } = require("../../../components/form");
-const query = require("../../lib/query");
-const FakeToot = require("../../components/fake-toot");
+const query = require("../../../lib/query");
+const FakeToot = require("../../../components/fake-toot");
+const Loading = require("../../../components/loading");
-const base = "/settings/admin/custom-emoji";
+const base = "/settings/custom-emoji/local";
module.exports = function EmojiDetailRoute() {
let [_match, params] = useRoute(`${base}/:emojiId`);
@@ -54,7 +55,11 @@ function EmojiDetailData({emojiId}) {
</div>
);
} else if (isLoading) {
- return "Loading...";
+ return (
+ <div>
+ <Loading/>
+ </div>
+ );
} else {
return <EmojiDetail emoji={emoji}/>;
}
diff --git a/web/source/settings/admin/emoji/index.js b/web/source/settings/admin/emoji/local/index.js
index 0fcda8264..1ccdece72 100644
--- a/web/source/settings/admin/emoji/index.js
+++ b/web/source/settings/admin/emoji/local/index.js
@@ -24,7 +24,7 @@ const {Switch, Route} = require("wouter");
const EmojiOverview = require("./overview");
const EmojiDetail = require("./detail");
-const base = "/settings/admin/custom-emoji";
+const base = "/settings/custom-emoji/local";
module.exports = function CustomEmoji() {
return (
diff --git a/web/source/settings/admin/emoji/new-emoji.js b/web/source/settings/admin/emoji/local/new-emoji.js
index 8cd604c02..985be2d32 100644
--- a/web/source/settings/admin/emoji/new-emoji.js
+++ b/web/source/settings/admin/emoji/local/new-emoji.js
@@ -21,17 +21,19 @@
const Promise = require('bluebird');
const React = require("react");
-const FakeToot = require("../../components/fake-toot");
-const MutateButton = require("../../components/mutation-button");
+const FakeToot = require("../../../components/fake-toot");
+const MutateButton = require("../../../components/mutation-button");
const {
useTextInput,
useFileInput,
useComboBoxInput
-} = require("../../components/form");
+} = require("../../../components/form");
-const query = require("../../lib/query");
-const { CategorySelect } = require('./category-select');
+const query = require("../../../lib/query");
+const { CategorySelect } = require('../category-select');
+
+const shortcodeRegex = /^[a-z0-9_]+$/;
module.exports = function NewEmojiForm({ emoji }) {
const emojiCodes = React.useMemo(() => {
@@ -47,9 +49,26 @@ module.exports = function NewEmojiForm({ emoji }) {
const [onShortcodeChange, resetShortcode, { shortcode, setShortcode, shortcodeRef }] = useTextInput("shortcode", {
validator: function validateShortcode(code) {
- return emojiCodes.has(code)
- ? "Shortcode already in use"
- : "";
+ // technically invalid, but hacky fix to prevent validation error on page load
+ if (shortcode == "") {return "";}
+
+ if (emojiCodes.has(code)) {
+ return "Shortcode already in use";
+ }
+
+ if (code.length < 2 || code.length > 30) {
+ return "Shortcode must be between 2 and 30 characters";
+ }
+
+ if (code.toLowerCase() != code) {
+ return "Shortcode must be lowercase";
+ }
+
+ if (!shortcodeRegex.test(code)) {
+ return "Shortcode must only contain lowercase letters, numbers, and underscores";
+ }
+
+ return "";
}
});
@@ -78,11 +97,13 @@ module.exports = function NewEmojiForm({ emoji }) {
image,
shortcode,
category
- });
+ }).unwrap();
}).then(() => {
resetFile();
resetShortcode();
resetCategory();
+ }).catch((e) => {
+ console.error("Emoji upload error:", e);
});
}
diff --git a/web/source/settings/admin/emoji/overview.js b/web/source/settings/admin/emoji/local/overview.js
index b8ac87a0f..7a5cfaad6 100644
--- a/web/source/settings/admin/emoji/overview.js
+++ b/web/source/settings/admin/emoji/local/overview.js
@@ -23,10 +23,11 @@ const {Link} = require("wouter");
const NewEmojiForm = require("./new-emoji");
-const query = require("../../lib/query");
-const { useEmojiByCategory } = require("./category-select");
+const query = require("../../../lib/query");
+const { useEmojiByCategory } = require("../category-select");
+const Loading = require("../../../components/loading");
-const base = "/settings/admin/custom-emoji";
+const base = "/settings/custom-emoji/local";
module.exports = function EmojiOverview() {
const {
@@ -37,12 +38,12 @@ module.exports = function EmojiOverview() {
return (
<>
- <h1>Custom Emoji</h1>
+ <h1>Custom Emoji (local)</h1>
{error &&
<div className="error accent">{error}</div>
}
{isLoading
- ? "Loading..."
+ ? <Loading/>
: <>
<EmojiList emoji={emoji}/>
<NewEmojiForm emoji={emoji}/>
diff --git a/web/source/settings/admin/emoji/remote/index.js b/web/source/settings/admin/emoji/remote/index.js
new file mode 100644
index 000000000..ae59673a5
--- /dev/null
+++ b/web/source/settings/admin/emoji/remote/index.js
@@ -0,0 +1,54 @@
+/*
+ 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 ParseFromToot = require("./parse-from-toot");
+
+const query = require("../../../lib/query");
+const Loading = require("../../../components/loading");
+
+module.exports = function RemoteEmoji() {
+ // local emoji are queried for shortcode collision detection
+ const {
+ data: emoji = [],
+ isLoading,
+ error
+ } = query.useGetAllEmojiQuery({filter: "domain:local"});
+
+ const emojiCodes = React.useMemo(() => {
+ return new Set(emoji.map((e) => e.shortcode));
+ }, [emoji]);
+
+ return (
+ <>
+ <h1>Custom Emoji (remote)</h1>
+ {error &&
+ <div className="error accent">{error}</div>
+ }
+ {isLoading
+ ? <Loading/>
+ : <>
+ <ParseFromToot emoji={emoji} emojiCodes={emojiCodes} />
+ </>
+ }
+ </>
+ );
+}; \ No newline at end of file
diff --git a/web/source/settings/admin/emoji/remote/parse-from-toot.js b/web/source/settings/admin/emoji/remote/parse-from-toot.js
new file mode 100644
index 000000000..75ff8bf7a
--- /dev/null
+++ b/web/source/settings/admin/emoji/remote/parse-from-toot.js
@@ -0,0 +1,319 @@
+/*
+ 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 Promise = require("bluebird");
+const React = require("react");
+const Redux = require("react-redux");
+const syncpipe = require("syncpipe");
+
+const {
+ useTextInput,
+ useComboBoxInput
+} = require("../../../components/form");
+
+const { CategorySelect } = require('../category-select');
+
+const query = require("../../../lib/query");
+const Loading = require("../../../components/loading");
+
+module.exports = function ParseFromToot({ emojiCodes }) {
+ const [searchStatus, { data, isLoading, isSuccess, error }] = query.useSearchStatusForEmojiMutation();
+ const instanceDomain = Redux.useSelector((state) => (new URL(state.oauth.instance).host));
+
+ const [onURLChange, _resetURL, { url }] = useTextInput("url");
+
+ const searchResult = React.useMemo(() => {
+ if (!isSuccess) {
+ return null;
+ }
+
+ if (data.type == "none") {
+ return "No results found";
+ }
+
+ if (data.domain == instanceDomain) {
+ return <b>This is a local user/toot, all referenced emoji are already on your instance</b>;
+ }
+
+ if (data.list.length == 0) {
+ return <b>This {data.type == "statuses" ? "toot" : "account"} doesn't use any custom emoji</b>;
+ }
+
+ return (
+ <CopyEmojiForm
+ localEmojiCodes={emojiCodes}
+ type={data.type}
+ domain={data.domain}
+ emojiList={data.list}
+ />
+ );
+ }, [isSuccess, data, instanceDomain, emojiCodes]);
+
+ function submitSearch(e) {
+ e.preventDefault();
+ searchStatus(url);
+ }
+
+ return (
+ <div className="parse-emoji">
+ <h2>Steal this look</h2>
+ <form onSubmit={submitSearch}>
+ <div className="form-field text">
+ <label htmlFor="url">
+ Link to a toot:
+ </label>
+ <div className="row">
+ <input
+ type="text"
+ id="url"
+ name="url"
+ onChange={onURLChange}
+ value={url}
+ />
+ <button disabled={isLoading}>
+ <i className={[
+ "fa",
+ (isLoading
+ ? "fa-refresh fa-spin"
+ : "fa-search")
+ ].join(" ")} aria-hidden="true" title="Search"/>
+ <span className="sr-only">Search</span>
+ </button>
+ </div>
+ {isLoading && <Loading/>}
+ {error && <div className="error">{error.data.error}</div>}
+ </div>
+ </form>
+ {searchResult}
+ </div>
+ );
+};
+
+function makeEmojiState(emojiList, checked) {
+ /* Return a new object, with a key for every emoji's shortcode,
+ And a value for it's checkbox `checked` state.
+ */
+ return syncpipe(emojiList, [
+ (_) => _.map((emoji) => [emoji.shortcode, {
+ checked,
+ valid: true
+ }]),
+ (_) => Object.fromEntries(_)
+ ]);
+}
+
+function updateEmojiState(emojiState, checked) {
+ /* Create a new object with all emoji entries' checked state updated */
+ return syncpipe(emojiState, [
+ (_) => Object.entries(emojiState),
+ (_) => _.map(([key, val]) => [key, {
+ ...val,
+ checked
+ }]),
+ (_) => Object.fromEntries(_)
+ ]);
+}
+
+function CopyEmojiForm({ localEmojiCodes, type, domain, emojiList }) {
+ const [patchRemoteEmojis, patchResult] = query.usePatchRemoteEmojisMutation();
+ const [err, setError] = React.useState();
+
+ const toggleAllRef = React.useRef(null);
+ const [toggleAllState, setToggleAllState] = React.useState(0);
+ const [emojiState, setEmojiState] = React.useState(makeEmojiState(emojiList, false));
+ const [someSelected, setSomeSelected] = React.useState(false);
+
+ const [categoryState, resetCategory, { category }] = useComboBoxInput("category");
+
+ React.useEffect(() => {
+ if (emojiList != undefined) {
+ setEmojiState(makeEmojiState(emojiList, false));
+ }
+ }, [emojiList]);
+
+ React.useEffect(() => {
+ /* Updates (un)check all checkbox, based on shortcode checkboxes
+ Can be 0 (not checked), 1 (checked) or 2 (indeterminate)
+ */
+ if (toggleAllRef.current == null) {
+ return;
+ }
+
+ let values = Object.values(emojiState);
+ /* one or more boxes are checked */
+ let some = values.some((v) => v.checked);
+
+ let all = false;
+ if (some) {
+ /* there's not at least one unchecked box */
+ all = !values.some((v) => v.checked == false);
+ }
+
+ setSomeSelected(some);
+
+ if (some && !all) {
+ setToggleAllState(2);
+ toggleAllRef.current.indeterminate = true;
+ } else {
+ setToggleAllState(all ? 1 : 0);
+ toggleAllRef.current.indeterminate = false;
+ }
+ }, [emojiState, toggleAllRef]);
+
+ function updateEmoji(shortcode, value) {
+ setEmojiState({
+ ...emojiState,
+ [shortcode]: {
+ ...emojiState[shortcode],
+ ...value
+ }
+ });
+ }
+
+ function toggleAll(e) {
+ let selectAll = e.target.checked;
+
+ if (toggleAllState == 2) { // indeterminate
+ selectAll = false;
+ }
+
+ setEmojiState(updateEmojiState(emojiState, selectAll));
+ setToggleAllState(selectAll);
+ }
+
+ function submit(action) {
+ Promise.try(() => {
+ setError(null);
+ const selectedShortcodes = syncpipe(emojiState, [
+ (_) => Object.entries(_),
+ (_) => _.filter(([_shortcode, entry]) => entry.checked),
+ (_) => _.map(([shortcode, entry]) => {
+ if (action == "copy" && !entry.valid) {
+ throw `One or more selected emoji have non-unique shortcodes (${shortcode}), unselect them or pick a different local shortcode`;
+ }
+ return {
+ shortcode,
+ localShortcode: entry.shortcode
+ };
+ })
+ ]);
+
+ return patchRemoteEmojis({
+ action,
+ domain,
+ list: selectedShortcodes,
+ category
+ }).unwrap();
+ }).then(() => {
+ setEmojiState(makeEmojiState(emojiList, false));
+ resetCategory();
+ }).catch((e) => {
+ if (Array.isArray(e)) {
+ setError(e.map(([shortcode, msg]) => (
+ <div key={shortcode}>
+ {shortcode}: <span style={{ fontWeight: "initial" }}>{msg}</span>
+ </div>
+ )));
+ } else {
+ setError(e);
+ }
+ });
+ }
+
+ return (
+ <div className="parsed">
+ <span>This {type == "statuses" ? "toot" : "account"} uses the following custom emoji, select the ones you want to copy/disable:</span>
+ <div className="emoji-list">
+ <label className="header">
+ <input
+ ref={toggleAllRef}
+ type="checkbox"
+ onChange={toggleAll}
+ checked={toggleAllState === 1}
+ /> All
+ </label>
+ {emojiList.map((emoji) => (
+ <EmojiEntry
+ key={emoji.shortcode}
+ emoji={emoji}
+ localEmojiCodes={localEmojiCodes}
+ updateEmoji={(value) => updateEmoji(emoji.shortcode, value)}
+ checked={emojiState[emoji.shortcode].checked}
+ />
+ ))}
+ </div>
+
+ <CategorySelect
+ value={category}
+ categoryState={categoryState}
+ />
+
+ <div className="action-buttons row">
+ <button disabled={!someSelected} onClick={() => submit("copy")}>{patchResult.isLoading ? "Processing..." : "Copy to local emoji"}</button>
+ <button disabled={!someSelected} onClick={() => submit("disable")} className="danger">{patchResult.isLoading ? "Processing..." : "Disable"}</button>
+ </div>
+ {err && <div className="error">
+ {err}
+ </div>}
+ {patchResult.isSuccess && <div>
+ Action applied to {patchResult.data.length} emoji
+ </div>}
+ </div>
+ );
+}
+
+function EmojiEntry({ emoji, localEmojiCodes, updateEmoji, checked }) {
+ const [onShortcodeChange, _resetShortcode, { shortcode, shortcodeRef, shortcodeValid }] = useTextInput("shortcode", {
+ defaultValue: emoji.shortcode,
+ validator: function validateShortcode(code) {
+ return (checked && localEmojiCodes.has(code))
+ ? "Shortcode already in use"
+ : "";
+ }
+ });
+
+ React.useEffect(() => {
+ updateEmoji({ valid: shortcodeValid });
+ /* eslint-disable-next-line react-hooks/exhaustive-deps */
+ }, [shortcodeValid]);
+
+ return (
+ <label key={emoji.shortcode} className="row">
+ <input
+ type="checkbox"
+ onChange={(e) => updateEmoji({ checked: e.target.checked })}
+ checked={checked}
+ />
+ <img className="emoji" src={emoji.url} title={emoji.shortcode} />
+
+ <input
+ type="text"
+ id="shortcode"
+ name="Shortcode"
+ ref={shortcodeRef}
+ onChange={(e) => {
+ onShortcodeChange(e);
+ updateEmoji({ shortcode: e.target.value, checked: true });
+ }}
+ value={shortcode}
+ />
+ </label>
+ );
+} \ No newline at end of file
diff --git a/web/source/settings/admin/federation.js b/web/source/settings/admin/federation.js
index 753024e58..f1023d365 100644
--- a/web/source/settings/admin/federation.js
+++ b/web/source/settings/admin/federation.js
@@ -30,6 +30,7 @@ const api = require("../lib/api");
const adminActions = require("../redux/reducers/admin").actions;
const submit = require("../lib/submit");
const BackButton = require("../components/back-button");
+const Loading = require("../components/loading");
const base = "/settings/admin/federation";
@@ -56,7 +57,9 @@ module.exports = function AdminSettings() {
return (
<div>
<h1>Federation</h1>
- Loading...
+ <div>
+ <Loading/>
+ </div>
</div>
);
}
@@ -321,7 +324,7 @@ function InstancePage({domain, Form}) {
const [statusMsg, setStatus] = React.useState("");
if (entry == undefined) {
- return "Loading...";
+ return <Loading/>;
}
const updateBlock = submit(
diff --git a/web/source/settings/components/form/text.jsx b/web/source/settings/components/form/text.jsx
index a392239dc..c24628dd7 100644
--- a/web/source/settings/components/form/text.jsx
+++ b/web/source/settings/components/form/text.jsx
@@ -20,17 +20,14 @@
const React = require("react");
-module.exports = function useTextInput({name, Name}, {validator} = {}) {
- const [text, setText] = React.useState("");
+module.exports = function useTextInput({name, Name}, {validator, defaultValue=""} = {}) {
+ const [text, setText] = React.useState(defaultValue);
+ const [valid, setValid] = React.useState(true);
const textRef = React.useRef(null);
function onChange(e) {
let input = e.target.value;
setText(input);
-
- if (validator) {
- validator(input);
- }
}
function reset() {
@@ -39,7 +36,9 @@ module.exports = function useTextInput({name, Name}, {validator} = {}) {
React.useEffect(() => {
if (validator) {
- textRef.current.setCustomValidity(validator(text));
+ let res = validator(text);
+ setValid(res == "");
+ textRef.current.setCustomValidity(res);
textRef.current.reportValidity();
}
}, [text, textRef, validator]);
@@ -50,7 +49,8 @@ module.exports = function useTextInput({name, Name}, {validator} = {}) {
{
[name]: text,
[`${name}Ref`]: textRef,
- [`set${Name}`]: setText
+ [`set${Name}`]: setText,
+ [`${name}Valid`]: valid
}
];
}; \ No newline at end of file
diff --git a/web/source/settings/components/loading.jsx b/web/source/settings/components/loading.jsx
new file mode 100644
index 000000000..51ce3a18b
--- /dev/null
+++ b/web/source/settings/components/loading.jsx
@@ -0,0 +1,27 @@
+/*
+ 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");
+
+module.exports = function Loading() {
+ return (
+ <i className="fa fa-spin fa-refresh" aria-label="Loading" title="Loading"/>
+ );
+}; \ No newline at end of file
diff --git a/web/source/settings/index.js b/web/source/settings/index.js
index 58afe5d28..b087f945c 100644
--- a/web/source/settings/index.js
+++ b/web/source/settings/index.js
@@ -32,6 +32,7 @@ const oauth = require("./redux/reducers/oauth").actions;
const { AuthenticationError } = require("./lib/errors");
const Login = require("./components/login");
+const Loading = require("./components/loading");
require("./style.css");
@@ -46,7 +47,11 @@ const nav = {
"Instance Settings": require("./admin/settings.js"),
"Actions": require("./admin/actions"),
"Federation": require("./admin/federation.js"),
- "Custom Emoji": require("./admin/emoji"),
+ },
+ "Custom Emoji": {
+ adminOnly: true,
+ "Local": require("./admin/emoji/local"),
+ "Remote": require("./admin/emoji/remote"),
}
};
@@ -167,7 +172,7 @@ function App() {
function Main() {
return (
<Provider store={store}>
- <PersistGate loading={"loading..."} persistor={persistor}>
+ <PersistGate loading={<section><Loading/></section>} persistor={persistor}>
<App />
</PersistGate>
</Provider>
diff --git a/web/source/settings/lib/query/custom-emoji.js b/web/source/settings/lib/query/custom-emoji.js
index fa2a08db6..593556620 100644
--- a/web/source/settings/lib/query/custom-emoji.js
+++ b/web/source/settings/lib/query/custom-emoji.js
@@ -18,8 +18,18 @@
"use strict";
+const Promise = require("bluebird");
+
const base = require("./base");
+function unwrap(res) {
+ if (res.error != undefined) {
+ throw res.error;
+ } else {
+ return res.data;
+ }
+}
+
const endpoints = (build) => ({
getAllEmoji: build.query({
query: (params = {}) => ({
@@ -77,6 +87,93 @@ const endpoints = (build) => ({
url: `/api/v1/admin/custom_emojis/${id}`
}),
invalidatesTags: (res, error, id) => [{type: "Emojis", id}]
+ }),
+ searchStatusForEmoji: build.mutation({
+ query: (url) => ({
+ method: "GET",
+ url: `/api/v2/search?q=${encodeURIComponent(url)}&resolve=true&limit=1`
+ }),
+ transformResponse: (res) => {
+ /* Parses search response, prioritizing a toot result,
+ and returns referenced custom emoji
+ */
+ let type;
+
+ if (res.statuses.length > 0) {
+ type = "statuses";
+ } else if (res.accounts.length > 0) {
+ type = "accounts";
+ } else {
+ return {
+ type: "none"
+ };
+ }
+
+ let data = res[type][0];
+
+ return {
+ type,
+ domain: (new URL(data.url)).host, // to get WEB_DOMAIN, see https://github.com/superseriousbusiness/gotosocial/issues/1225
+ list: data.emojis
+ };
+ }
+ }),
+ patchRemoteEmojis: build.mutation({
+ queryFn: ({action, domain, list, category}, api, _extraOpts, baseQuery) => {
+ const data = [];
+ const errors = [];
+
+ return Promise.each(list, (emoji) => {
+ return Promise.try(() => {
+ return baseQuery({
+ method: "GET",
+ url: `/api/v1/admin/custom_emojis`,
+ params: {
+ filter: `domain:${domain},shortcode:${emoji.shortcode}`,
+ limit: 1
+ }
+ }).then(unwrap);
+ }).then(([lookup]) => {
+ if (lookup == undefined) { throw "not found"; }
+
+ let body = {
+ type: action
+ };
+
+ if (action == "copy") {
+ body.shortcode = emoji.localShortcode ?? emoji.shortcode;
+ if (category.trim().length != 0) {
+ body.category = category;
+ }
+ }
+
+ return baseQuery({
+ method: "PATCH",
+ url: `/api/v1/admin/custom_emojis/${lookup.id}`,
+ asForm: true,
+ body: body
+ }).then(unwrap);
+ }).then((res) => {
+ data.push([emoji.shortcode, res]);
+ }).catch((e) => {
+ console.error("emoji lookup for", emoji.shortcode, "failed:", e);
+ let msg = e.message ?? e;
+ if (e.data.error) {
+ msg = e.data.error;
+ }
+ errors.push([emoji.shortcode, msg]);
+ });
+ }).then(() => {
+ if (errors.length == 0) {
+ return { data };
+ } else {
+ return {
+ error: errors
+ };
+ }
+ });
+ },
+ invalidatesTags: () => [{type: "Emojis", id: "LIST"}]
})
});
diff --git a/web/source/settings/style.css b/web/source/settings/style.css
index 7a71c3278..7ed2fb725 100644
--- a/web/source/settings/style.css
+++ b/web/source/settings/style.css
@@ -598,4 +598,52 @@ span.form-info {
.left-border {
border-left: 0.2rem solid $border-accent;
padding-left: 0.4rem;
+}
+
+.parse-emoji {
+ .parsed {
+ margin-top: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ & > span {
+ margin-bottom: -1rem;
+ }
+
+ .action-buttons {
+ gap: 1rem;
+ }
+
+ .emoji-list {
+ display: flex;
+ flex-direction: column;
+
+ & > * {
+ gap: 1rem;
+ align-items: center;
+ padding: 0.5rem 1rem;
+ }
+
+ .header {
+ background: $gray2;
+ display: flex;
+ }
+
+ .row {
+ display: grid;
+ grid-template-columns: auto auto 1fr;
+
+ &:hover {
+ background: $settings-entry-hover-bg;
+ }
+ }
+
+ .emoji {
+ height: 2rem;
+ width: 2rem;
+ margin: 0;
+ }
+ }
+ }
} \ No newline at end of file