From 4b8d7bd952dd97091d1baddeab10213e9c38cef3 Mon Sep 17 00:00:00 2001 From: f0x52 Date: Sun, 11 Dec 2022 16:00:23 +0100 Subject: [PATCH] [frogend] Emoji copy "Steal this look" (#1222) * split emoji into local and remote, allow looking up remote emoji by toot url * optimize some/all filtering * fix local emoji routes * implement copy action * shortcode validation, don't wipe form on error * copy & disable PATCH * remove local toot acceptance for testing * unused import * parse emoji from account and status, get web_url from status uri * fix url parse * submit button loading info * actually send category * code cleanup, distinguish between account and status responses * use loading icons * fix loading icon on federation page * require Loading element * remove unused require * query explanation, small accessibility tweaks --- web/source/css/base.css | 10 + .../admin/emoji/{ => local}/detail.js | 17 +- .../settings/admin/emoji/{ => local}/index.js | 2 +- .../admin/emoji/{ => local}/new-emoji.js | 39 ++- .../admin/emoji/{ => local}/overview.js | 11 +- .../settings/admin/emoji/remote/index.js | 54 +++ .../admin/emoji/remote/parse-from-toot.js | 319 ++++++++++++++++++ web/source/settings/admin/federation.js | 7 +- web/source/settings/components/form/text.jsx | 16 +- web/source/settings/components/loading.jsx | 27 ++ web/source/settings/index.js | 9 +- web/source/settings/lib/query/custom-emoji.js | 97 ++++++ web/source/settings/style.css | 48 +++ 13 files changed, 623 insertions(+), 33 deletions(-) rename web/source/settings/admin/emoji/{ => local}/detail.js (91%) rename web/source/settings/admin/emoji/{ => local}/index.js (96%) rename web/source/settings/admin/emoji/{ => local}/new-emoji.js (78%) rename web/source/settings/admin/emoji/{ => local}/overview.js (88%) create mode 100644 web/source/settings/admin/emoji/remote/index.js create mode 100644 web/source/settings/admin/emoji/remote/parse-from-toot.js create mode 100644 web/source/settings/components/loading.jsx diff --git a/web/source/css/base.css b/web/source/css/base.css index d2fa95a3..73b53373 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 similarity index 91% rename from web/source/settings/admin/emoji/detail.js rename to web/source/settings/admin/emoji/local/detail.js index 51e29144..179ee7c7 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}) { ); } else if (isLoading) { - return "Loading..."; + return ( +
+ +
+ ); } else { return ; } diff --git a/web/source/settings/admin/emoji/index.js b/web/source/settings/admin/emoji/local/index.js similarity index 96% rename from web/source/settings/admin/emoji/index.js rename to web/source/settings/admin/emoji/local/index.js index 0fcda826..1ccdece7 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 similarity index 78% rename from web/source/settings/admin/emoji/new-emoji.js rename to web/source/settings/admin/emoji/local/new-emoji.js index 8cd604c0..985be2d3 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 similarity index 88% rename from web/source/settings/admin/emoji/overview.js rename to web/source/settings/admin/emoji/local/overview.js index b8ac87a0..7a5cfaad 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 ( <> -

Custom Emoji

+

Custom Emoji (local)

{error &&
{error}
} {isLoading - ? "Loading..." + ? : <> 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 00000000..ae59673a --- /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 . +*/ + +"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 ( + <> +

Custom Emoji (remote)

+ {error && +
{error}
+ } + {isLoading + ? + : <> + + + } + + ); +}; \ 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 00000000..75ff8bf7 --- /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 . +*/ + +"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 This is a local user/toot, all referenced emoji are already on your instance; + } + + if (data.list.length == 0) { + return This {data.type == "statuses" ? "toot" : "account"} doesn't use any custom emoji; + } + + return ( + + ); + }, [isSuccess, data, instanceDomain, emojiCodes]); + + function submitSearch(e) { + e.preventDefault(); + searchStatus(url); + } + + return ( +
+

Steal this look

+
+
+ +
+ +