From 4b8d7bd952dd97091d1baddeab10213e9c38cef3 Mon Sep 17 00:00:00 2001 From: f0x52 Date: Sun, 11 Dec 2022 16:00:23 +0100 Subject: [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/settings/admin/emoji/local/detail.js | 173 +++++++++++++++++++++ web/source/settings/admin/emoji/local/index.js | 40 +++++ web/source/settings/admin/emoji/local/new-emoji.js | 168 ++++++++++++++++++++ web/source/settings/admin/emoji/local/overview.js | 90 +++++++++++ 4 files changed, 471 insertions(+) create mode 100644 web/source/settings/admin/emoji/local/detail.js create mode 100644 web/source/settings/admin/emoji/local/index.js create mode 100644 web/source/settings/admin/emoji/local/new-emoji.js create mode 100644 web/source/settings/admin/emoji/local/overview.js (limited to 'web/source/settings/admin/emoji/local') diff --git a/web/source/settings/admin/emoji/local/detail.js b/web/source/settings/admin/emoji/local/detail.js new file mode 100644 index 000000000..179ee7c7c --- /dev/null +++ b/web/source/settings/admin/emoji/local/detail.js @@ -0,0 +1,173 @@ +/* + 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 { useRoute, Link, Redirect } = require("wouter"); + +const { CategorySelect } = require("../category-select"); +const { useComboBoxInput, useFileInput } = require("../../../components/form"); + +const query = require("../../../lib/query"); +const FakeToot = require("../../../components/fake-toot"); +const Loading = require("../../../components/loading"); + +const base = "/settings/custom-emoji/local"; + +module.exports = function EmojiDetailRoute() { + let [_match, params] = useRoute(`${base}/:emojiId`); + if (params?.emojiId == undefined) { + return ; + } else { + return ( +
+ < go back + +
+ ); + } +}; + +function EmojiDetailData({emojiId}) { + const {currentData: emoji, isLoading, error} = query.useGetEmojiQuery(emojiId); + + if (error) { + return ( +
+ {error.status}: {error.data.error} +
+ ); + } else if (isLoading) { + return ( +
+ +
+ ); + } else { + return ; + } +} + +function EmojiDetail({emoji}) { + 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 && category.trim().length > 0) { + console.log("updating to", category); + modifyEmoji({id: emoji.id, category: category.trim()}); + } + }, [isNewCategory, category, categoryState.open, emoji.category, emoji.id, modifyEmoji]); + + return ( + <> +
+ {emoji.shortcode} +
+

{emoji.shortcode}

+ +
+
+ +
+

Modify this emoji {modifyResult.isLoading && "(processing..)"}

+ + {modifyResult.error &&
+ {modifyResult.error.status}: {modifyResult.error.data.error} +
} + +
+ + + +
+ +
+ Image +
+ + {imageInfo} + +
+ + + + + Look at this new custom emoji {emoji.shortcode} isn't it cool? + +
+
+ + ); +} + +function DeleteButton({id}) { + // TODO: confirmation dialog? + const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation(); + + let text = "Delete"; + if (deleteResult.isLoading) { + text = "Deleting..."; + } + + if (deleteResult.isSuccess) { + return ; + } + + return ( + + ); +} \ No newline at end of file diff --git a/web/source/settings/admin/emoji/local/index.js b/web/source/settings/admin/emoji/local/index.js new file mode 100644 index 000000000..1ccdece72 --- /dev/null +++ b/web/source/settings/admin/emoji/local/index.js @@ -0,0 +1,40 @@ +/* + 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 {Switch, Route} = require("wouter"); + +const EmojiOverview = require("./overview"); +const EmojiDetail = require("./detail"); + +const base = "/settings/custom-emoji/local"; + +module.exports = function CustomEmoji() { + return ( + <> + + + + + + + + ); +}; diff --git a/web/source/settings/admin/emoji/local/new-emoji.js b/web/source/settings/admin/emoji/local/new-emoji.js new file mode 100644 index 000000000..985be2d32 --- /dev/null +++ b/web/source/settings/admin/emoji/local/new-emoji.js @@ -0,0 +1,168 @@ +/* + 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 FakeToot = require("../../../components/fake-toot"); +const MutateButton = require("../../../components/mutation-button"); + +const { + useTextInput, + useFileInput, + useComboBoxInput +} = require("../../../components/form"); + +const query = require("../../../lib/query"); +const { CategorySelect } = require('../category-select'); + +const shortcodeRegex = /^[a-z0-9_]+$/; + +module.exports = function NewEmojiForm({ emoji }) { + 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", { + withPreview: true, + maxSize: 50 * 1024 + }); + + const [onShortcodeChange, resetShortcode, { shortcode, setShortcode, shortcodeRef }] = useTextInput("shortcode", { + validator: function validateShortcode(code) { + // 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 ""; + } + }); + + const [categoryState, resetCategory, { category }] = useComboBoxInput("category"); + + React.useEffect(() => { + if (shortcode.length == 0) { + if (image != undefined) { + let [name, _ext] = image.name.split("."); + 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]); + + function uploadEmoji(e) { + if (e) { + e.preventDefault(); + } + + Promise.try(() => { + return addEmoji({ + image, + shortcode, + category + }).unwrap(); + }).then(() => { + resetFile(); + resetShortcode(); + resetCategory(); + }).catch((e) => { + console.error("Emoji upload error:", e); + }); + } + + let emojiOrShortcode = `:${shortcode}:`; + + if (imageURL != undefined) { + emojiOrShortcode = {shortcode}; + } + + return ( +
+

Add new custom emoji

+ + + Look at this new custom emoji {emojiOrShortcode} isn't it cool? + + +
+
+ + {imageInfo} + +
+ +
+ + +
+ + + + + +
+ ); +}; \ No newline at end of file diff --git a/web/source/settings/admin/emoji/local/overview.js b/web/source/settings/admin/emoji/local/overview.js new file mode 100644 index 000000000..7a5cfaad6 --- /dev/null +++ b/web/source/settings/admin/emoji/local/overview.js @@ -0,0 +1,90 @@ +/* + 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 {Link} = require("wouter"); + +const NewEmojiForm = require("./new-emoji"); + +const query = require("../../../lib/query"); +const { useEmojiByCategory } = require("../category-select"); +const Loading = require("../../../components/loading"); + +const base = "/settings/custom-emoji/local"; + +module.exports = function EmojiOverview() { + const { + data: emoji = [], + isLoading, + error + } = query.useGetAllEmojiQuery({filter: "domain:local"}); + + return ( + <> +

Custom Emoji (local)

+ {error && +
{error}
+ } + {isLoading + ? + : <> + + + + } + + ); +}; + +function EmojiList({emoji}) { + const emojiByCategory = useEmojiByCategory(emoji); + + return ( +
+

Overview

+
+ {emoji.length == 0 && "No local emoji yet, add one below"} + {Object.entries(emojiByCategory).map(([category, entries]) => { + return ; + })} +
+
+ ); +} + +function EmojiCategory({category, entries}) { + return ( +
+ {category} +
+ {entries.map((e) => { + return ( + + {/* */} + + {e.shortcode} + + + ); + })} +
+
+ ); +} \ No newline at end of file -- cgit v1.3