From dd9a46a412ed1157c79285ddddc95ae3cb321359 Mon Sep 17 00:00:00 2001 From: f0x Date: Fri, 16 Sep 2022 18:50:25 +0200 Subject: [PATCH] emoji uploader --- internal/web/settings-panel.go | 1 + web/source/css/base.css | 7 + web/source/package.json | 3 + web/source/settings-panel/admin/emoji.js | 182 ++++++++++++++++-- web/source/settings-panel/admin/federation.js | 1 - .../settings-panel/components/fake-toot.jsx | 43 +++++ .../settings-panel/components/form-fields.jsx | 17 +- web/source/settings-panel/index.js | 2 +- web/source/settings-panel/lib/api/admin.js | 17 ++ web/source/settings-panel/lib/api/index.js | 4 +- web/source/settings-panel/lib/submit.js | 49 +++++ .../settings-panel/redux/reducers/admin.js | 33 +++- web/source/settings-panel/style.css | 80 ++++++-- web/source/yarn.lock | 7 +- 14 files changed, 407 insertions(+), 39 deletions(-) create mode 100644 web/source/settings-panel/components/fake-toot.jsx create mode 100644 web/source/settings-panel/lib/submit.js diff --git a/internal/web/settings-panel.go b/internal/web/settings-panel.go index d290e5441..3ba396998 100644 --- a/internal/web/settings-panel.go +++ b/internal/web/settings-panel.go @@ -42,6 +42,7 @@ func (m *Module) SettingsPanelHandler(c *gin.Context) { assetsPathPrefix + "/dist/_colors.css", assetsPathPrefix + "/dist/base.css", assetsPathPrefix + "/dist/profile.css", + assetsPathPrefix + "/dist/status.css", assetsPathPrefix + "/dist/settings-panel-style.css", }, "javascript": []string{ diff --git a/web/source/css/base.css b/web/source/css/base.css index d50195465..d6ed44df0 100644 --- a/web/source/css/base.css +++ b/web/source/css/base.css @@ -278,6 +278,13 @@ section.error { } } +.error-text { + color: $error1; + background: $error2; + border-radius: 0.1rem; + font-weight: bold; +} + input, select, textarea { box-sizing: border-box; border: 0.15rem solid $input-border; diff --git a/web/source/package.json b/web/source/package.json index 01e661fb2..6e8deba09 100644 --- a/web/source/package.json +++ b/web/source/package.json @@ -18,6 +18,7 @@ "browserlist": "^1.0.1", "create-error": "^0.3.1", "css-extract": "^2.0.0", + "default-value": "^1.0.0", "dotty": "^0.1.2", "eslint-plugin-react": "^7.24.0", "express": "^4.18.1", @@ -35,6 +36,8 @@ "postcss-nested": "^5.0.6", "postcss-scss": "^4.0.4", "postcss-strip-inline-comments": "^0.1.5", + "prettier-bytes": "^1.0.4", + "pretty-bytes": "4", "react": "18", "react-dom": "18", "react-error-boundary": "^3.1.4", diff --git a/web/source/settings-panel/admin/emoji.js b/web/source/settings-panel/admin/emoji.js index e4fff2104..6cf78ad39 100644 --- a/web/source/settings-panel/admin/emoji.js +++ b/web/source/settings-panel/admin/emoji.js @@ -21,36 +21,192 @@ const Promise = require("bluebird"); const React = require("react"); const Redux = require("react-redux"); +const {Switch, Route, Link, Redirect, useRoute, useLocation} = require("wouter"); + +const Submit = require("../components/submit"); +const FakeToot = require("../components/fake-toot"); +const { formFields } = require("../components/form-fields"); const api = require("../lib/api"); const adminActions = require("../redux/reducers/admin").actions; +const submit = require("../lib/submit"); + +const base = "/settings/admin/custom-emoji"; module.exports = function CustomEmoji() { return ( - <> -

Custom Emoji

-
- -
-
-

Upload

-
- + + + + + + ); }; function EmojiOverview() { const dispatch = Redux.useDispatch(); - const emoji = Redux.useSelector((state) => state.admin.emoji); - console.log(emoji); + const [loaded, setLoaded] = React.useState(false); + + const [errorMsg, setError] = React.useState(""); React.useEffect(() => { - dispatch(api.admin.fetchCustomEmoji()); + if (!loaded) { + Promise.try(() => { + return dispatch(api.admin.fetchCustomEmoji()); + }).then(() => { + setLoaded(true); + }).catch((e) => { + setLoaded(true); + setError(e.message); + }); + } }, []); + if (!loaded) { + return ( + <> +

Custom Emoji

+ Loading... + + ); + } + return ( <> - +

Custom Emoji

+ + + {errorMsg.length > 0 && +
{errorMsg}
+ } ); +} + +const NewEmojiForm = formFields(adminActions.updateNewEmojiVal, (state) => state.admin.newEmoji); +function NewEmoji() { + const dispatch = Redux.useDispatch(); + const newEmojiForm = Redux.useSelector((state) => state.admin.newEmoji); + + const [errorMsg, setError] = React.useState(""); + const [statusMsg, setStatus] = React.useState(""); + + const uploadEmoji = submit( + () => dispatch(api.admin.newEmoji()), + { + setStatus, setError, + onSuccess: function() { + URL.revokeObjectURL(newEmojiForm.image); + return Promise.all([ + dispatch(adminActions.updateNewEmojiVal(["image", undefined])), + dispatch(adminActions.updateNewEmojiVal(["imageFile", undefined])), + dispatch(adminActions.updateNewEmojiVal(["shortcode", ""])), + ]); + } + } + ); + + React.useEffect(() => { + if (newEmojiForm.shortcode.length == 0) { + if (newEmojiForm.imageFile != undefined) { + let [name, ext] = newEmojiForm.imageFile.name.split("."); + dispatch(adminActions.updateNewEmojiVal(["shortcode", name])); + } + } + }); + + let emojiOrShortcode = `:${newEmojiForm.shortcode}:`; + + if (newEmojiForm.image != undefined) { + emojiOrShortcode = {newEmojiForm.shortcode}; + } + + return ( +
+

Add new custom emoji

+ + + Look at this new custom emoji {emojiOrShortcode} isn't it cool? + + + + + + + +
+ ); +} + +function EmojiList() { + const emoji = Redux.useSelector((state) => state.admin.emoji); + + return ( +
+

Overview

+
+ {Object.entries(emoji).map(([category, entries]) => { + return ; + })} +
+
+ ); +} + +function EmojiCategory({category, entries}) { + return ( +
+ {category} +
+ {entries.map((e) => { + return ( + // + + + {e.shortcode} + + + ); + })} +
+
+ ); +} + +function EmojiDetailWrapped() { + /* 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 + */ + let [_match, {emojiId}] = useRoute(`${base}/:emojiId`); + + function alterEmoji([key, val]) { + return adminActions.updateDomainBlockVal([emojiId, key, val]); + } + + const fields = formFields(alterEmoji, (state) => state.admin.blockedInstances[emojiId]); + + return ; +} + +function EmojiDetail({id, Form}) { + return ( + "Not implemented yet" + ); } \ No newline at end of file diff --git a/web/source/settings-panel/admin/federation.js b/web/source/settings-panel/admin/federation.js index 3911099d7..1c5070efc 100644 --- a/web/source/settings-panel/admin/federation.js +++ b/web/source/settings-panel/admin/federation.js @@ -287,7 +287,6 @@ function BackButton() { ); } - function InstancePageWrapped() { /* 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, diff --git a/web/source/settings-panel/components/fake-toot.jsx b/web/source/settings-panel/components/fake-toot.jsx new file mode 100644 index 000000000..d66da66be --- /dev/null +++ b/web/source/settings-panel/components/fake-toot.jsx @@ -0,0 +1,43 @@ +/* + 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 Redux = require("react-redux"); + +module.exports = function FakeToot({children}) { + const account = Redux.useSelector((state) => state.user.profile); + + return ( +
+
+ + + + {account.display_name.trim().length > 0 ? account.display_name : account.username} + @{account.username} +
+
+ {children} +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/web/source/settings-panel/components/form-fields.jsx b/web/source/settings-panel/components/form-fields.jsx index 8c48ba1a9..0ecaa2dc4 100644 --- a/web/source/settings-panel/components/form-fields.jsx +++ b/web/source/settings-panel/components/form-fields.jsx @@ -21,6 +21,7 @@ const React = require("react"); const Redux = require("react-redux"); const d = require("dotty"); +const prettierBytes = require("prettier-bytes"); function eventListeners(dispatch, setter, obj) { return { @@ -78,7 +79,7 @@ module.exports = { formFields: function formFields(setter, selector) { function FormField({ type, id, name, className="", placeHolder="", fileType="", children=null, - options=null, inputProps={}, withPreview=true + options=null, inputProps={}, withPreview=true, showSize=false, maxSize=Infinity }) { const dispatch = Redux.useDispatch(); let state = Redux.useSelector(selector); @@ -105,10 +106,22 @@ module.exports = { } else if (type == "file") { defaultLabel = false; let file = get(state, `${id}File`); + + let size = null; + if (showSize && file) { + size = `(${prettierBytes(file.size)})`; + + if (file.size > maxSize) { + size = {size}; + } + } + field = ( <> - {file ? file.name : "no file selected"} + + {file ? file.name : "no file selected"} {size} + {/* remove */} diff --git a/web/source/settings-panel/index.js b/web/source/settings-panel/index.js index c2f3b0f8a..91b889626 100644 --- a/web/source/settings-panel/index.js +++ b/web/source/settings-panel/index.js @@ -92,7 +92,7 @@ function App() { e.message = "Stored OAUTH token no longer valid, please log in again."; } setErrorMsg(e); - console.error(e.message); + console.error(e); }); } }, []); diff --git a/web/source/settings-panel/lib/api/admin.js b/web/source/settings-panel/lib/api/admin.js index 67b170f12..7873975f4 100644 --- a/web/source/settings-panel/lib/api/admin.js +++ b/web/source/settings-panel/lib/api/admin.js @@ -157,6 +157,23 @@ module.exports = function ({ apiCall, getChanges }) { return dispatch(admin.setEmoji(emoji)); }); }; + }, + + newEmoji: function newEmoji() { + return function (dispatch, getState) { + return Promise.try(() => { + const state = getState().admin.newEmoji; + + const update = getChanges(state, { + formKeys: ["shortcode"], + fileKeys: ["image"] + }); + + return dispatch(apiCall("POST", "/api/v1/admin/custom_emojis", update, "form")); + }).then((emoji) => { + return dispatch(admin.addEmoji(emoji)); + }); + }; } }; return adminAPI; diff --git a/web/source/settings-panel/lib/api/index.js b/web/source/settings-panel/lib/api/index.js index 25b54d252..e699011bd 100644 --- a/web/source/settings-panel/lib/api/index.js +++ b/web/source/settings-panel/lib/api/index.js @@ -37,7 +37,9 @@ function apiCall(method, route, payload, type = "json") { let url = new URL(base); let [path, query] = route.split("?"); url.pathname = path; - url.search = query; + if (query != undefined) { + url.search = query; + } let body; let headers = { diff --git a/web/source/settings-panel/lib/submit.js b/web/source/settings-panel/lib/submit.js new file mode 100644 index 000000000..4092b292b --- /dev/null +++ b/web/source/settings-panel/lib/submit.js @@ -0,0 +1,49 @@ +/* + 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"); + +module.exports = function submit(func, { + setStatus, setError, + startStatus="PATCHing", successStatus="Saved!", + onSuccess, + onError +}) { + return function() { + setStatus(startStatus); + setError(""); + return Promise.try(() => { + return func(); + }).then(() => { + setStatus(successStatus); + if (onSuccess != undefined) { + console.log("running", onSuccess); + return onSuccess(); + } + }).catch((e) => { + setError(e.message); + setStatus(""); + console.error(e); + if (onError != undefined) { + onError(e); + } + }); + }; +}; \ No newline at end of file diff --git a/web/source/settings-panel/redux/reducers/admin.js b/web/source/settings-panel/redux/reducers/admin.js index 07863cf11..e534b1a3d 100644 --- a/web/source/settings-panel/redux/reducers/admin.js +++ b/web/source/settings-panel/redux/reducers/admin.js @@ -19,6 +19,7 @@ "use strict"; const { createSlice } = require("@reduxjs/toolkit"); +const defaultValue = require("default-value"); function sortBlocks(blocks) { return blocks.sort((a, b) => { // alphabetical sort @@ -34,6 +35,12 @@ function emptyBlock() { }; } +function emptyEmojiForm() { + return { + shortcode: "" + }; +} + module.exports = createSlice({ name: "admin", initialState: { @@ -44,7 +51,8 @@ module.exports = createSlice({ exportType: "plain", ...emptyBlock() }, - emoji: [] + emoji: {}, + newEmoji: emptyEmojiForm() }, reducers: { setBlockedInstances: (state, { payload }) => { @@ -90,7 +98,26 @@ module.exports = createSlice({ }, setEmoji: (state, {payload}) => { - state.emoji = payload; - } + state.emoji = {}; + payload.forEach((emoji) => { + if (emoji.category == undefined) { + emoji.category = "Unsorted"; + } + state.emoji[emoji.category] = defaultValue(state.emoji[emoji.category], []); + state.emoji[emoji.category].push(emoji); + }); + }, + + updateNewEmojiVal: (state, { payload: [key, val] }) => { + state.newEmoji[key] = val; + }, + + addEmoji: (state, {payload: emoji}) => { + if (emoji.category == undefined) { + emoji.category = "Unsorted"; + } + state.emoji[emoji.category] = defaultValue(state.emoji[emoji.category], []); + state.emoji[emoji.category].push(emoji); + }, } }); \ No newline at end of file diff --git a/web/source/settings-panel/style.css b/web/source/settings-panel/style.css index cd40930c0..09f532c02 100644 --- a/web/source/settings-panel/style.css +++ b/web/source/settings-panel/style.css @@ -192,9 +192,14 @@ input, select, textarea { } button, .button { - margin-top: 1rem; - margin-right: 1rem; white-space: nowrap; + margin-right: 1rem; + } +} + +.messagebutton > div { + button, .button { + margin-top: 1rem; } } @@ -359,6 +364,23 @@ section.with-sidebar > div { font-weight: bold; } +.list { + display: flex; + flex-direction: column; + margin-top: 0.5rem; + max-height: 40rem; + overflow: auto; + + .entry { + display: flex; + background: $settings-entry-bg; + + &:hover { + background: $settings-entry-hover-bg; + } + } +} + .instance-list { .filter { display: flex; @@ -370,20 +392,9 @@ section.with-sidebar > div { } } - .list { - display: flex; - flex-direction: column; - margin-top: 0.5rem; - max-height: 40rem; - overflow: auto; - } - .entry { padding: 0.3rem; margin: 0.2rem 0; - background: $settings-entry-bg; - - display: flex; #domain { flex: 1 1 auto; @@ -391,10 +402,6 @@ section.with-sidebar > div { white-space: nowrap; text-overflow: ellipsis; } - - &:hover { - background: $settings-entry-hover-bg; - } } } @@ -402,3 +409,42 @@ section.with-sidebar > div { display: flex; justify-content: space-between; } + +.emoji-list { + background: $settings-entry-bg; + + .entry { + padding: 0.5rem; + flex-direction: column; + + .emoji-group { + display: flex; + + a { + border-radius: $br; + padding: 0.4rem; + line-height: 0; + + img { + height: 2rem; + width: 2rem; + } + + &:hover { + background: $settings-entry-hover-bg; + } + } + } + + &:hover { + background: inherit; + } + } +} + +.toot { + padding-top: 0.5rem; + .contentgrid { + padding: 0 0.5rem; + } +} diff --git a/web/source/yarn.lock b/web/source/yarn.lock index 85a375ff0..830ca5055 100644 --- a/web/source/yarn.lock +++ b/web/source/yarn.lock @@ -4492,11 +4492,16 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== -prettier-bytes@^1.0.3: +prettier-bytes@^1.0.3, prettier-bytes@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/prettier-bytes/-/prettier-bytes-1.0.4.tgz#994b02aa46f699c50b6257b5faaa7fe2557e62d6" integrity sha512-dLbWOa4xBn+qeWeIF60qRoB6Pk2jX5P3DIVgOQyMyvBpu931Q+8dXz8X0snJiFkQdohDDLnZQECjzsAj75hgZQ== +pretty-bytes@4: + version "4.0.2" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" + integrity sha512-yJAF+AjbHKlxQ8eezMd/34Mnj/YTQ3i6kLzvVsH4l/BfIFtp444n0wVbnsn66JimZ9uBofv815aRp1zCppxlWw== + pretty-ms@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-2.1.0.tgz#4257c256df3fb0b451d6affaab021884126981dc"