mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-10-31 22:40:01 +00:00
emoji uploader
This commit is contained in:
parent
b984641108
commit
dd9a46a412
14 changed files with 407 additions and 39 deletions
|
@ -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{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<h1>Custom Emoji</h1>
|
||||
<div>
|
||||
<EmojiOverview/>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Upload</h2>
|
||||
</div>
|
||||
</>
|
||||
<Switch>
|
||||
<Route path={`${base}/:emojiId`}>
|
||||
<EmojiDetailWrapped />
|
||||
</Route>
|
||||
<EmojiOverview />
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
<h1>Custom Emoji</h1>
|
||||
Loading...
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<h1>Custom Emoji</h1>
|
||||
<EmojiList/>
|
||||
<NewEmoji/>
|
||||
{errorMsg.length > 0 &&
|
||||
<div className="error accent">{errorMsg}</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 = <img
|
||||
className="emoji"
|
||||
src={newEmojiForm.image}
|
||||
title={`:${newEmojiForm.shortcode}:`}
|
||||
alt={newEmojiForm.shortcode}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Add new custom emoji</h2>
|
||||
|
||||
<FakeToot content="bazinga">
|
||||
Look at this new custom emoji {emojiOrShortcode} isn't it cool?
|
||||
</FakeToot>
|
||||
|
||||
<NewEmojiForm.File
|
||||
id="image"
|
||||
name="Image"
|
||||
fileType="image/png,image/gif"
|
||||
showSize={true}
|
||||
maxSize={50 * 1000}
|
||||
/>
|
||||
|
||||
<NewEmojiForm.TextInput
|
||||
id="shortcode"
|
||||
name="Shortcode (without : :), must be unique on the instance"
|
||||
placeHolder="blobcat"
|
||||
/>
|
||||
|
||||
<Submit onClick={uploadEmoji} label="Upload" errorMsg={errorMsg} statusMsg={statusMsg} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmojiList() {
|
||||
const emoji = Redux.useSelector((state) => state.admin.emoji);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Overview</h2>
|
||||
<div className="list emoji-list">
|
||||
{Object.entries(emoji).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.static_url} to={`${base}/${e.shortcode}`}>
|
||||
<Link key={e.static_url} to={`${base}`}>
|
||||
<a>
|
||||
<img src={e.static_url} alt={e.shortcode} title={`:${e.shortcode}:`}/>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 <EmojiDetail id={emojiId} Form={fields} />;
|
||||
}
|
||||
|
||||
function EmojiDetail({id, Form}) {
|
||||
return (
|
||||
"Not implemented yet"
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
|
|
43
web/source/settings-panel/components/fake-toot.jsx
Normal file
43
web/source/settings-panel/components/fake-toot.jsx
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
"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 (
|
||||
<div className="toot expanded">
|
||||
<div className="contentgrid">
|
||||
<span className="avatar">
|
||||
<img src="http://localhost:8080/assets/default_avatars/GoToSocial_icon6.png" alt=""/>
|
||||
</span>
|
||||
<span className="displayname">{account.display_name.trim().length > 0 ? account.display_name : account.username}</span>
|
||||
<span className="username">@{account.username}</span>
|
||||
<div className="text">
|
||||
<div className="content">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -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 = <span className="error-text">{size}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
field = (
|
||||
<>
|
||||
<label htmlFor={id} className="file-input button">Browse</label>
|
||||
<span>{file ? file.name : "no file selected"}</span>
|
||||
<span>
|
||||
{file ? file.name : "no file selected"} {size}
|
||||
</span>
|
||||
{/* <a onClick={removeFile("header")}>remove</a> */}
|
||||
<input className="hidden" id={id} type="file" accept={fileType} onChange={onFileChange(id, withPreview)} {...inputProps}/>
|
||||
</>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = {
|
||||
|
|
49
web/source/settings-panel/lib/submit.js
Normal file
49
web/source/settings-panel/lib/submit.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
"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);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
},
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue