summaryrefslogtreecommitdiff
path: root/web/source/settings/admin/emoji/local
diff options
context:
space:
mode:
Diffstat (limited to 'web/source/settings/admin/emoji/local')
-rw-r--r--web/source/settings/admin/emoji/local/detail.js173
-rw-r--r--web/source/settings/admin/emoji/local/index.js40
-rw-r--r--web/source/settings/admin/emoji/local/new-emoji.js168
-rw-r--r--web/source/settings/admin/emoji/local/overview.js90
4 files changed, 471 insertions, 0 deletions
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 <http://www.gnu.org/licenses/>.
+*/
+
+"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 <Redirect to={base}/>;
+ } else {
+ return (
+ <div className="emoji-detail">
+ <Link to={base}><a>&lt; go back</a></Link>
+ <EmojiDetailData emojiId={params.emojiId}/>
+ </div>
+ );
+ }
+};
+
+function EmojiDetailData({emojiId}) {
+ const {currentData: emoji, isLoading, error} = query.useGetEmojiQuery(emojiId);
+
+ if (error) {
+ return (
+ <div className="error accent">
+ {error.status}: {error.data.error}
+ </div>
+ );
+ } else if (isLoading) {
+ return (
+ <div>
+ <Loading/>
+ </div>
+ );
+ } else {
+ return <EmojiDetail emoji={emoji}/>;
+ }
+}
+
+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 (
+ <>
+ <div className="emoji-header">
+ <img src={emoji.url} alt={emoji.shortcode} title={emoji.shortcode}/>
+ <div>
+ <h2>{emoji.shortcode}</h2>
+ <DeleteButton id={emoji.id}/>
+ </div>
+ </div>
+
+ <div className="left-border">
+ <h2>Modify this emoji {modifyResult.isLoading && "(processing..)"}</h2>
+
+ {modifyResult.error && <div className="error">
+ {modifyResult.error.status}: {modifyResult.error.data.error}
+ </div>}
+
+ <div className="update-category">
+ <CategorySelect
+ value={category}
+ categoryState={categoryState}
+ setIsNew={setIsNewCategory}
+ >
+ <button style={{visibility: (isNewCategory ? "initial" : "hidden")}} onClick={modifyCategory}>
+ Create
+ </button>
+ </CategorySelect>
+ </div>
+
+ <div className="update-image">
+ <b>Image</b>
+ <div className="form-field file">
+ <label className="file-input button" htmlFor="image">
+ Browse
+ </label>
+ {imageInfo}
+ <input
+ className="hidden"
+ type="file"
+ id="image"
+ name="Image"
+ accept="image/png,image/gif"
+ onChange={onFileChange}
+ />
+ </div>
+
+ <button onClick={modifyImage} disabled={image == undefined}>Replace image</button>
+
+ <FakeToot>
+ Look at this new custom emoji <img
+ className="emoji"
+ src={imageURL ?? emoji.url}
+ title={`:${emoji.shortcode}:`}
+ alt={emoji.shortcode}
+ /> isn&apos;t it cool?
+ </FakeToot>
+ </div>
+ </div>
+ </>
+ );
+}
+
+function DeleteButton({id}) {
+ // TODO: confirmation dialog?
+ const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation();
+
+ let text = "Delete";
+ if (deleteResult.isLoading) {
+ text = "Deleting...";
+ }
+
+ if (deleteResult.isSuccess) {
+ return <Redirect to={base}/>;
+ }
+
+ return (
+ <button className="danger" onClick={() => deleteEmoji(id)} disabled={deleteResult.isLoading}>{text}</button>
+ );
+} \ 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 <http://www.gnu.org/licenses/>.
+*/
+
+"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 (
+ <>
+ <Switch>
+ <Route path={`${base}/:emojiId`}>
+ <EmojiDetail />
+ </Route>
+ <EmojiOverview />
+ </Switch>
+ </>
+ );
+};
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 <http://www.gnu.org/licenses/>.
+*/
+
+"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 = <img
+ className="emoji"
+ src={imageURL}
+ title={`:${shortcode}:`}
+ alt={shortcode}
+ />;
+ }
+
+ return (
+ <div>
+ <h2>Add new custom emoji</h2>
+
+ <FakeToot>
+ Look at this new custom emoji {emojiOrShortcode} isn&apos;t it cool?
+ </FakeToot>
+
+ <form onSubmit={uploadEmoji} className="form-flex">
+ <div className="form-field file">
+ <label className="file-input button" htmlFor="image">
+ Browse
+ </label>
+ {imageInfo}
+ <input
+ className="hidden"
+ type="file"
+ id="image"
+ name="Image"
+ accept="image/png,image/gif"
+ onChange={onFileChange}
+ />
+ </div>
+
+ <div className="form-field text">
+ <label htmlFor="shortcode">
+ Shortcode, must be unique among the instance's local emoji
+ </label>
+ <input
+ type="text"
+ id="shortcode"
+ name="Shortcode"
+ ref={shortcodeRef}
+ onChange={onShortcodeChange}
+ value={shortcode}
+ />
+ </div>
+
+ <CategorySelect
+ value={category}
+ categoryState={categoryState}
+ />
+
+ <MutateButton text="Upload emoji" result={result} />
+ </form>
+ </div>
+ );
+}; \ 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 <http://www.gnu.org/licenses/>.
+*/
+
+"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 (
+ <>
+ <h1>Custom Emoji (local)</h1>
+ {error &&
+ <div className="error accent">{error}</div>
+ }
+ {isLoading
+ ? <Loading/>
+ : <>
+ <EmojiList emoji={emoji}/>
+ <NewEmojiForm emoji={emoji}/>
+ </>
+ }
+ </>
+ );
+};
+
+function EmojiList({emoji}) {
+ const emojiByCategory = useEmojiByCategory(emoji);
+
+ return (
+ <div>
+ <h2>Overview</h2>
+ <div className="list emoji-list">
+ {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>
+ </div>
+ );
+}
+
+function EmojiCategory({category, entries}) {
+ return (
+ <div className="entry">
+ <b>{category}</b>
+ <div className="emoji-group">
+ {entries.map((e) => {
+ return (
+ <Link key={e.id} to={`${base}/${e.id}`}>
+ {/* <Link key={e.static_url} to={`${base}`}> */}
+ <a>
+ <img src={e.url} alt={e.shortcode} title={`:${e.shortcode}:`}/>
+ </a>
+ </Link>
+ );
+ })}
+ </div>
+ </div>
+ );
+} \ No newline at end of file