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 = ;
+ }
+
+ 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 (
+ //
+
+
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+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}
+
+
+
+ );
+};
\ 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 = (
<>
Browse
- {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"