summaryrefslogtreecommitdiff
path: root/web/source/settings
diff options
context:
space:
mode:
Diffstat (limited to 'web/source/settings')
-rw-r--r--web/source/settings/admin/actions.js1
-rw-r--r--web/source/settings/admin/emoji.js229
-rw-r--r--web/source/settings/admin/emoji/detail.js86
-rw-r--r--web/source/settings/admin/emoji/index.js40
-rw-r--r--web/source/settings/admin/emoji/new-emoji.js133
-rw-r--r--web/source/settings/admin/emoji/overview.js99
-rw-r--r--web/source/settings/admin/federation.js4
-rw-r--r--web/source/settings/admin/settings.js3
-rw-r--r--web/source/settings/components/error.jsx1
-rw-r--r--web/source/settings/components/form-fields.jsx2
-rw-r--r--web/source/settings/components/form/file.jsx78
-rw-r--r--web/source/settings/components/form/index.js36
-rw-r--r--web/source/settings/components/form/text.jsx56
-rw-r--r--web/source/settings/components/mutation-button.jsx42
-rw-r--r--web/source/settings/index.js4
-rw-r--r--web/source/settings/lib/api/admin.js27
-rw-r--r--web/source/settings/lib/api/index.js44
-rw-r--r--web/source/settings/lib/get-views.js3
-rw-r--r--web/source/settings/lib/panel.js134
-rw-r--r--web/source/settings/lib/query/base.js55
-rw-r--r--web/source/settings/lib/query/custom-emoji.js66
-rw-r--r--web/source/settings/lib/query/index.js24
-rw-r--r--web/source/settings/redux/index.js46
-rw-r--r--web/source/settings/redux/reducers/admin.js43
-rw-r--r--web/source/settings/style.css33
-rw-r--r--web/source/settings/user/profile.js1
26 files changed, 801 insertions, 489 deletions
diff --git a/web/source/settings/admin/actions.js b/web/source/settings/admin/actions.js
index d4980d021..915b8aee1 100644
--- a/web/source/settings/admin/actions.js
+++ b/web/source/settings/admin/actions.js
@@ -18,7 +18,6 @@
"use strict";
-const Promise = require("bluebird");
const React = require("react");
const Redux = require("react-redux");
diff --git a/web/source/settings/admin/emoji.js b/web/source/settings/admin/emoji.js
deleted file mode 100644
index ad7fcab06..000000000
--- a/web/source/settings/admin/emoji.js
+++ /dev/null
@@ -1,229 +0,0 @@
-/*
- 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 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 BackButton = require("../components/back-button");
-
-const base = "/settings/admin/custom-emoji";
-
-module.exports = function CustomEmoji() {
- const dispatch = Redux.useDispatch();
- const [loaded, setLoaded] = React.useState(false);
-
- const [errorMsg, setError] = React.useState("");
-
- React.useEffect(() => {
- 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 (
- <>
- {errorMsg.length > 0 &&
- <div className="error accent">{errorMsg}</div>
- }
- <Switch>
- <Route path={`${base}/:emojiId`}>
- <EmojiDetailWrapped />
- </Route>
- <EmojiOverview />
- </Switch>
- </>
- );
-};
-
-function EmojiOverview() {
- return (
- <>
- <h1>Custom Emoji</h1>
- <EmojiList/>
- <NewEmoji/>
- </>
- );
-}
-
-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>
- Look at this new custom emoji {emojiOrShortcode} isn&apos;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.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>
- );
-}
-
-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`);
- const emojiById = Redux.useSelector((state) => state.admin.emojiById);
- const emoji = emojiById[emojiId];
- if (emoji == undefined) {
- return (
- <h1><BackButton to={base}/> Custom Emoji: </h1>
- );
- }
-
- function alterEmoji([key, val]) {
- return adminActions.updateDomainBlockVal([emojiId, key, val]);
- }
-
- const fields = formFields(alterEmoji, (state) => state.admin.blockedInstances[emojiId]);
-
- return <EmojiDetail emoji={emoji} Form={fields} />;
-}
-
-function EmojiDetail({emoji, Form}) {
- return (
- <div>
- <h1><BackButton to={base}/> Custom Emoji: {emoji.shortcode}</h1>
- <p>
- Editing custom emoji isn&apos;t implemented yet.<br/>
- <a target="_blank" rel="noreferrer" href="https://github.com/superseriousbusiness/gotosocial/issues/797">View implementation progress.</a>
- </p>
- <img src={emoji.url} alt={emoji.shortcode} title={`:${emoji.shortcode}:`}/>
- </div>
- );
-} \ No newline at end of file
diff --git a/web/source/settings/admin/emoji/detail.js b/web/source/settings/admin/emoji/detail.js
new file mode 100644
index 000000000..cc0f8e73c
--- /dev/null
+++ b/web/source/settings/admin/emoji/detail.js
@@ -0,0 +1,86 @@
+/*
+ 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 BackButton = require("../../components/back-button");
+
+const query = require("../../lib/query");
+
+const base = "/settings/admin/custom-emoji";
+
+/* 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
+*/
+module.exports = function EmojiDetailWrapped() {
+ let [_match, {emojiId}] = useRoute(`${base}/:emojiId`);
+ const {currentData: emoji, isLoading, error} = query.useGetEmojiQuery(emojiId);
+
+ return (<>
+ {error && <div className="error accent">{error.status}: {error.data.error}</div>}
+ {isLoading
+ ? "Loading..."
+ : <EmojiDetail emoji={emoji}/>
+ }
+ </>);
+};
+
+function EmojiDetail({emoji}) {
+ if (emoji == undefined) {
+ return (<>
+ <Link to={base}>
+ <a className="button">go back</a>
+ </Link>
+ </>);
+ }
+
+ return (
+ <div>
+ <h1><BackButton to={base}/> Custom Emoji: {emoji.shortcode}</h1>
+ <DeleteButton id={emoji.id}/>
+ <p>
+ Editing custom emoji isn&apos;t implemented yet.<br/>
+ <a target="_blank" rel="noreferrer" href="https://github.com/superseriousbusiness/gotosocial/issues/797">View implementation progress.</a>
+ </p>
+ <img src={emoji.url} alt={emoji.shortcode} title={`:${emoji.shortcode}:`}/>
+ </div>
+ );
+}
+
+function DeleteButton({id}) {
+ // TODO: confirmation dialog?
+ const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation();
+
+ let text = "Delete this emoji";
+ if (deleteResult.isLoading) {
+ text = "processing...";
+ }
+
+ if (deleteResult.isSuccess) {
+ return <Redirect to={base}/>;
+ }
+
+ return (
+ <button onClick={() => deleteEmoji(id)} disabled={deleteResult.isLoading}>{text}</button>
+ );
+} \ No newline at end of file
diff --git a/web/source/settings/admin/emoji/index.js b/web/source/settings/admin/emoji/index.js
new file mode 100644
index 000000000..0fcda8264
--- /dev/null
+++ b/web/source/settings/admin/emoji/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/admin/custom-emoji";
+
+module.exports = function CustomEmoji() {
+ return (
+ <>
+ <Switch>
+ <Route path={`${base}/:emojiId`}>
+ <EmojiDetail />
+ </Route>
+ <EmojiOverview />
+ </Switch>
+ </>
+ );
+};
diff --git a/web/source/settings/admin/emoji/new-emoji.js b/web/source/settings/admin/emoji/new-emoji.js
new file mode 100644
index 000000000..e5bc8893d
--- /dev/null
+++ b/web/source/settings/admin/emoji/new-emoji.js
@@ -0,0 +1,133 @@
+/*
+ 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
+} = require("../../components/form");
+
+const query = require("../../lib/query");
+
+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) {
+ return emojiCodes.has(code)
+ ? "Shortcode already in use"
+ : "";
+ }
+ });
+
+ React.useEffect(() => {
+ if (shortcode.length == 0) {
+ if (image != undefined) {
+ let [name, _ext] = image.name.split(".");
+ setShortcode(name);
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [image]);
+
+ function uploadEmoji(e) {
+ if (e) {
+ e.preventDefault();
+ }
+
+ Promise.try(() => {
+ return addEmoji({
+ image,
+ shortcode
+ });
+ }).then(() => {
+ resetFile();
+ resetShortcode();
+ });
+ }
+
+ 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>
+
+ <MutateButton text="Upload emoji" result={result}/>
+ </form>
+ </div>
+ );
+}; \ No newline at end of file
diff --git a/web/source/settings/admin/emoji/overview.js b/web/source/settings/admin/emoji/overview.js
new file mode 100644
index 000000000..028276da2
--- /dev/null
+++ b/web/source/settings/admin/emoji/overview.js
@@ -0,0 +1,99 @@
+/*
+ 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 defaultValue = require('default-value');
+
+const NewEmojiForm = require("./new-emoji");
+
+const query = require("../../lib/query");
+
+const base = "/settings/admin/custom-emoji";
+
+module.exports = function EmojiOverview() {
+ const {
+ data: emoji,
+ isLoading,
+ error
+ } = query.useGetAllEmojiQuery({filter: "domain:local"});
+
+ return (
+ <>
+ <h1>Custom Emoji</h1>
+ {error &&
+ <div className="error accent">{error}</div>
+ }
+ {isLoading
+ ? "Loading..."
+ : <>
+ <EmojiList emoji={emoji}/>
+ <NewEmojiForm emoji={emoji}/>
+ </>
+ }
+ </>
+ );
+};
+
+function EmojiList({emoji}) {
+ const byCategory = React.useMemo(() => {
+ const categories = {};
+
+ emoji.forEach((emoji) => {
+ let cat = defaultValue(emoji.category, "Unsorted");
+ categories[cat] = defaultValue(categories[cat], []);
+ categories[cat].push(emoji);
+ });
+
+ return categories;
+ }, [emoji]);
+
+ return (
+ <div>
+ <h2>Overview</h2>
+ <div className="list emoji-list">
+ {emoji.length == 0 && "No local emoji yet"}
+ {Object.entries(byCategory).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
diff --git a/web/source/settings/admin/federation.js b/web/source/settings/admin/federation.js
index 99f10e69e..449d2156f 100644
--- a/web/source/settings/admin/federation.js
+++ b/web/source/settings/admin/federation.js
@@ -50,7 +50,7 @@ module.exports = function AdminSettings() {
return dispatch(api.admin.fetchDomainBlocks());
});
}
- }, []);
+ }, [dispatch, loadedBlockedInstances]);
if (!loadedBlockedInstances) {
return (
@@ -315,7 +315,7 @@ function InstancePage({domain, Form}) {
if (entry == undefined) {
dispatch(api.admin.getEditableDomainBlock(domain));
}
- }, []);
+ }, [dispatch, domain, entry]);
const [errorMsg, setError] = React.useState("");
const [statusMsg, setStatus] = React.useState("");
diff --git a/web/source/settings/admin/settings.js b/web/source/settings/admin/settings.js
index 845a1f924..eb6e8b267 100644
--- a/web/source/settings/admin/settings.js
+++ b/web/source/settings/admin/settings.js
@@ -18,7 +18,6 @@
"use strict";
-const Promise = require("bluebird");
const React = require("react");
const Redux = require("react-redux");
@@ -32,7 +31,7 @@ const adminActions = require("../redux/reducers/instances").actions;
const {
TextInput,
TextArea,
- File
+ _File
} = require("../components/form-fields").formFields(adminActions.setAdminSettingsVal, (state) => state.instances.adminSettings);
module.exports = function AdminSettings() {
diff --git a/web/source/settings/components/error.jsx b/web/source/settings/components/error.jsx
index 13dc686b7..dd1551c36 100644
--- a/web/source/settings/components/error.jsx
+++ b/web/source/settings/components/error.jsx
@@ -18,7 +18,6 @@
"use strict";
-const Promise = require("bluebird");
const React = require("react");
module.exports = function ErrorFallback({error, resetErrorBoundary}) {
diff --git a/web/source/settings/components/form-fields.jsx b/web/source/settings/components/form-fields.jsx
index cb402c3b2..6393b1d5c 100644
--- a/web/source/settings/components/form-fields.jsx
+++ b/web/source/settings/components/form-fields.jsx
@@ -119,7 +119,7 @@ module.exports = {
field = (
<>
<label htmlFor={id} className="file-input button">Browse</label>
- <span>
+ <span className="form-info">
{file ? file.name : "no file selected"} {size}
</span>
{/* <a onClick={removeFile("header")}>remove</a> */}
diff --git a/web/source/settings/components/form/file.jsx b/web/source/settings/components/form/file.jsx
new file mode 100644
index 000000000..0d28cb699
--- /dev/null
+++ b/web/source/settings/components/form/file.jsx
@@ -0,0 +1,78 @@
+/*
+ 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 prettierBytes = require("prettier-bytes");
+
+module.exports = function useFileInput({name, _Name}, {
+ withPreview,
+ maxSize,
+ initialInfo = "no file selected"
+}) {
+ const [file, setFile] = React.useState();
+ const [imageURL, setImageURL] = React.useState();
+ const [info, setInfo] = React.useState();
+
+ function onChange(e) {
+ let file = e.target.files[0];
+ setFile(file);
+
+ URL.revokeObjectURL(imageURL);
+
+ if (file != undefined) {
+ if (withPreview) {
+ setImageURL(URL.createObjectURL(file));
+ }
+
+ let size = prettierBytes(file.size);
+ if (maxSize && file.size > maxSize) {
+ size = <span className="error-text">{size}</span>;
+ }
+
+ setInfo(<>
+ {file.name} ({size})
+ </>);
+ } else {
+ setInfo();
+ }
+ }
+
+ function reset() {
+ URL.revokeObjectURL(imageURL);
+ setImageURL();
+ setFile();
+ setInfo();
+ }
+
+ return [
+ onChange,
+ reset,
+ {
+ [name]: file,
+ [`${name}URL`]: imageURL,
+ [`${name}Info`]: <span className="form-info">
+ {info
+ ? info
+ : initialInfo
+ }
+ </span>
+ }
+ ];
+}; \ No newline at end of file
diff --git a/web/source/settings/components/form/index.js b/web/source/settings/components/form/index.js
new file mode 100644
index 000000000..5edc52364
--- /dev/null
+++ b/web/source/settings/components/form/index.js
@@ -0,0 +1,36 @@
+/*
+ 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";
+
+function capitalizeFirst(str) {
+ return str.slice(0,1).toUpperCase()+str.slice(1);
+}
+
+function makeHook(func) {
+ return (name, ...args) => func({
+ name,
+ Name: capitalizeFirst(name)
+ },
+ ...args);
+}
+
+module.exports = {
+ useTextInput: makeHook(require("./text")),
+ useFileInput: makeHook(require("./file"))
+}; \ No newline at end of file
diff --git a/web/source/settings/components/form/text.jsx b/web/source/settings/components/form/text.jsx
new file mode 100644
index 000000000..a392239dc
--- /dev/null
+++ b/web/source/settings/components/form/text.jsx
@@ -0,0 +1,56 @@
+/*
+ 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");
+
+module.exports = function useTextInput({name, Name}, {validator} = {}) {
+ const [text, setText] = React.useState("");
+ const textRef = React.useRef(null);
+
+ function onChange(e) {
+ let input = e.target.value;
+ setText(input);
+
+ if (validator) {
+ validator(input);
+ }
+ }
+
+ function reset() {
+ setText("");
+ }
+
+ React.useEffect(() => {
+ if (validator) {
+ textRef.current.setCustomValidity(validator(text));
+ textRef.current.reportValidity();
+ }
+ }, [text, textRef, validator]);
+
+ return [
+ onChange,
+ reset,
+ {
+ [name]: text,
+ [`${name}Ref`]: textRef,
+ [`set${Name}`]: setText
+ }
+ ];
+}; \ No newline at end of file
diff --git a/web/source/settings/components/mutation-button.jsx b/web/source/settings/components/mutation-button.jsx
new file mode 100644
index 000000000..9a8c9d089
--- /dev/null
+++ b/web/source/settings/components/mutation-button.jsx
@@ -0,0 +1,42 @@
+/*
+ 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");
+
+module.exports = function MutateButton({text, result}) {
+ let buttonText = text;
+
+ if (result.isLoading) {
+ buttonText = "Processing...";
+ }
+
+ return (<div>
+ {result.error &&
+ <section className="error">{result.error.status}: {result.error.data.error}</section>
+ }
+ <input
+ className="button"
+ type="submit"
+ disabled={result.isLoading}
+ value={buttonText}
+ />
+ </div>
+ );
+}; \ No newline at end of file
diff --git a/web/source/settings/index.js b/web/source/settings/index.js
index 34720e818..58afe5d28 100644
--- a/web/source/settings/index.js
+++ b/web/source/settings/index.js
@@ -46,7 +46,7 @@ const nav = {
"Instance Settings": require("./admin/settings.js"),
"Actions": require("./admin/actions"),
"Federation": require("./admin/federation.js"),
- "Custom Emoji": require("./admin/emoji.js"),
+ "Custom Emoji": require("./admin/emoji"),
}
};
@@ -94,7 +94,7 @@ function App() {
console.error(e);
});
}
- }, []);
+ }, [loginState, dispatch]);
let ErrorElement = null;
if (errorMsg != undefined) {
diff --git a/web/source/settings/lib/api/admin.js b/web/source/settings/lib/api/admin.js
index 56513b900..5f4fa1d1f 100644
--- a/web/source/settings/lib/api/admin.js
+++ b/web/source/settings/lib/api/admin.js
@@ -160,33 +160,6 @@ module.exports = function ({ apiCall, getChanges }) {
});
};
},
-
- fetchCustomEmoji: function fetchCustomEmoji() {
- return function (dispatch, _getState) {
- return Promise.try(() => {
- return dispatch(apiCall("GET", "/api/v1/admin/custom_emojis?filter=domain:local&limit=0"));
- }).then((emoji) => {
- 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;
}; \ No newline at end of file
diff --git a/web/source/settings/lib/api/index.js b/web/source/settings/lib/api/index.js
index e699011bd..8af7d5b43 100644
--- a/web/source/settings/lib/api/index.js
+++ b/web/source/settings/lib/api/index.js
@@ -24,14 +24,12 @@ const d = require("dotty");
const { APIError, AuthenticationError } = require("../errors");
const { setInstanceInfo, setNamedInstanceInfo } = require("../../redux/reducers/instances").actions;
-const oauth = require("../../redux/reducers/oauth").actions;
function apiCall(method, route, payload, type = "json") {
return function (dispatch, getState) {
const state = getState();
let base = state.oauth.instance;
let auth = state.oauth.token;
- console.log(method, base, route, "auth:", auth != undefined);
return Promise.try(() => {
let url = new URL(base);
@@ -51,21 +49,7 @@ function apiCall(method, route, payload, type = "json") {
headers["Content-Type"] = "application/json";
body = JSON.stringify(payload);
} else if (type == "form") {
- const formData = new FormData();
- Object.entries(payload).forEach(([key, val]) => {
- if (isPlainObject(val)) {
- Object.entries(val).forEach(([key2, val2]) => {
- if (val2 != undefined) {
- formData.set(`${key}[${key2}]`, val2);
- }
- });
- } else {
- if (val != undefined) {
- formData.set(key, val);
- }
- }
- });
- body = formData;
+ body = convertToForm(payload);
}
}
@@ -100,6 +84,28 @@ function apiCall(method, route, payload, type = "json") {
};
}
+/*
+ Takes an object with (nested) keys, and transforms it into
+ a FormData object to be sent over the API
+*/
+function convertToForm(payload) {
+ const formData = new FormData();
+ Object.entries(payload).forEach(([key, val]) => {
+ if (isPlainObject(val)) {
+ Object.entries(val).forEach(([key2, val2]) => {
+ if (val2 != undefined) {
+ formData.set(`${key}[${key2}]`, val2);
+ }
+ });
+ } else {
+ if (val != undefined) {
+ formData.set(key, val);
+ }
+ }
+ });
+ return formData;
+}
+
function getChanges(state, keys) {
const { formKeys = [], fileKeys = [], renamedKeys = {} } = keys;
const update = {};
@@ -129,7 +135,8 @@ function getChanges(state, keys) {
}
function getCurrentUrl() {
- return `${window.location.origin}${window.location.pathname}`;
+ let [pre, _past] = window.location.pathname.split("/settings");
+ return `${window.location.origin}${pre}/settings`;
}
function fetchInstanceWithoutStore(domain) {
@@ -181,5 +188,6 @@ module.exports = {
user: require("./user")(submoduleArgs),
admin: require("./admin")(submoduleArgs),
apiCall,
+ convertToForm,
getChanges
}; \ No newline at end of file
diff --git a/web/source/settings/lib/get-views.js b/web/source/settings/lib/get-views.js
index 39f627435..96ab17404 100644
--- a/web/source/settings/lib/get-views.js
+++ b/web/source/settings/lib/get-views.js
@@ -19,8 +19,7 @@
"use strict";
const React = require("react");
-const Redux = require("react-redux");
-const { Link, Route, Switch, Redirect } = require("wouter");
+const { Link, Route, Redirect } = require("wouter");
const { ErrorBoundary } = require("react-error-boundary");
const ErrorFallback = require("../components/error");
diff --git a/web/source/settings/lib/panel.js b/web/source/settings/lib/panel.js
deleted file mode 100644
index df723bc74..000000000
--- a/web/source/settings/lib/panel.js
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- 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 ReactDom = require("react-dom");
-
-const oauthLib = require("./oauth");
-
-module.exports = function createPanel(clientName, scope, Component) {
- ReactDom.render(<Panel/>, document.getElementById("root"));
-
- function Panel() {
- const [oauth, setOauth] = React.useState();
- const [hasAuth, setAuth] = React.useState(false);
- const [oauthState, setOauthState] = React.useState(localStorage.getItem("oauth"));
-
- React.useEffect(() => {
- let state = localStorage.getItem("oauth");
- if (state != undefined) {
- state = JSON.parse(state);
- let restoredOauth = oauthLib(state.config, state);
- Promise.try(() => {
- return restoredOauth.callback();
- }).then(() => {
- setAuth(true);
- });
- setOauth(restoredOauth);
- }
- }, [setAuth, setOauth]);
-
- if (!hasAuth && oauth && oauth.isAuthorized()) {
- setAuth(true);
- }
-
- if (oauth && oauth.isAuthorized()) {
- return <Component oauth={oauth} />;
- } else if (oauthState != undefined) {
- return "processing oauth...";
- } else {
- return <Auth setOauth={setOauth} />;
- }
- }
-
- function Auth({setOauth}) {
- const [ instance, setInstance ] = React.useState("");
-
- React.useEffect(() => {
- let isStillMounted = true;
- // check if current domain runs an instance
- let thisUrl = new URL(window.location.origin);
- thisUrl.pathname = "/api/v1/instance";
- Promise.try(() => {
- return fetch(thisUrl.href);
- }).then((res) => {
- if (res.status == 200) {
- return res.json();
- }
- }).then((json) => {
- if (json && json.uri && isStillMounted) {
- setInstance(json.uri);
- }
- }).catch((e) => {
- console.log("error checking instance response:", e);
- });
-
- return () => {
- // cleanup function
- isStillMounted = false;
- };
- }, []);
-
- function doAuth() {
- return Promise.try(() => {
- return new URL(instance);
- }).catch(TypeError, () => {
- return new URL(`https://${instance}`);
- }).then((parsedURL) => {
- let url = parsedURL.toString();
- let oauth = oauthLib({
- instance: url,
- client_name: clientName,
- scope: scope,
- website: window.location.href
- });
- setOauth(oauth);
- setInstance(url);
- return oauth.register().then(() => {
- return oauth;
- });
- }).then((oauth) => {
- return oauth.authorize();
- }).catch((e) => {
- console.log("error authenticating:", e);
- });
- }
-
- function updateInstance(e) {
- if (e.key == "Enter") {
- doAuth();
- } else {
- setInstance(e.target.value);
- }
- }
-
- return (
- <section className="login">
- <h1>OAUTH Login:</h1>
- <form onSubmit={(e) => e.preventDefault()}>
- <label htmlFor="instance">Instance: </label>
- <input value={instance} onChange={updateInstance} id="instance"/>
- <button onClick={doAuth}>Authenticate</button>
- </form>
- </section>
- );
- }
-}; \ No newline at end of file
diff --git a/web/source/settings/lib/query/base.js b/web/source/settings/lib/query/base.js
new file mode 100644
index 000000000..4023e9fbd
--- /dev/null
+++ b/web/source/settings/lib/query/base.js
@@ -0,0 +1,55 @@
+/*
+ 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 { createApi, fetchBaseQuery } = require("@reduxjs/toolkit/query/react");
+
+const { convertToForm } = require("../api");
+
+function instanceBasedQuery(args, api, extraOptions) {
+ const state = api.getState();
+ const {instance, token} = state.oauth;
+
+ if (args.baseUrl == undefined) {
+ args.baseUrl = instance;
+ }
+
+ if (args.asForm) {
+ delete args.asForm;
+ args.body = convertToForm(args.body);
+ }
+
+ return fetchBaseQuery({
+ baseUrl: args.baseUrl,
+ prepareHeaders: (headers) => {
+ if (token != undefined) {
+ headers.set('Authorization', token);
+ }
+ headers.set("Accept", "application/json");
+ return headers;
+ },
+ })(args, api, extraOptions);
+}
+
+module.exports = createApi({
+ reducerPath: "api",
+ baseQuery: instanceBasedQuery,
+ tagTypes: ["Emojis"],
+ endpoints: () => ({})
+}); \ No newline at end of file
diff --git a/web/source/settings/lib/query/custom-emoji.js b/web/source/settings/lib/query/custom-emoji.js
new file mode 100644
index 000000000..a26da75ca
--- /dev/null
+++ b/web/source/settings/lib/query/custom-emoji.js
@@ -0,0 +1,66 @@
+/*
+ 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 base = require("./base");
+
+const endpoints = (build) => ({
+ getAllEmoji: build.query({
+ query: (params = {}) => ({
+ url: "/api/v1/admin/custom_emojis",
+ params: {
+ limit: 0,
+ ...params
+ }
+ }),
+ providesTags: (res) =>
+ res
+ ? [...res.map((emoji) => ({type: "Emojis", id: emoji.id})), {type: "Emojis", id: "LIST"}]
+ : [{type: "Emojis", id: "LIST"}]
+ }),
+ getEmoji: build.query({
+ query: (id) => ({
+ url: `/api/v1/admin/custom_emojis/${id}`
+ }),
+ providesTags: (res, error, id) => [{type: "Emojis", id}]
+ }),
+ addEmoji: build.mutation({
+ query: (form) => {
+ return {
+ method: "POST",
+ url: `/api/v1/admin/custom_emojis`,
+ asForm: true,
+ body: form
+ };
+ },
+ invalidatesTags: (res) =>
+ res
+ ? [{type: "Emojis", id: "LIST"}, {type: "Emojis", id: res.id}]
+ : [{type: "Emojis", id: "LIST"}]
+ }),
+ deleteEmoji: build.mutation({
+ query: (id) => ({
+ method: "DELETE",
+ url: `/api/v1/admin/custom_emojis/${id}`
+ }),
+ invalidatesTags: (res, error, id) => [{type: "Emojis", id}]
+ })
+});
+
+module.exports = base.injectEndpoints({endpoints}); \ No newline at end of file
diff --git a/web/source/settings/lib/query/index.js b/web/source/settings/lib/query/index.js
new file mode 100644
index 000000000..5b15fd252
--- /dev/null
+++ b/web/source/settings/lib/query/index.js
@@ -0,0 +1,24 @@
+/*
+ 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";
+
+module.exports = {
+ ...require("./base"),
+ ...require("./custom-emoji.js")
+}; \ No newline at end of file
diff --git a/web/source/settings/redux/index.js b/web/source/settings/redux/index.js
index f18b8e452..43f5c007d 100644
--- a/web/source/settings/redux/index.js
+++ b/web/source/settings/redux/index.js
@@ -18,18 +18,20 @@
"use strict";
-const { createStore, combineReducers, applyMiddleware } = require("redux");
-const { persistStore, persistReducer } = require("redux-persist");
-const thunk = require("redux-thunk").default;
-const { composeWithDevTools } = require("redux-devtools-extension");
+const { combineReducers } = require("redux");
+const { configureStore } = require("@reduxjs/toolkit");
+const {
+ persistStore,
+ persistReducer,
+ FLUSH,
+ REHYDRATE,
+ PAUSE,
+ PERSIST,
+ PURGE,
+ REGISTER,
+} = require("redux-persist");
-const persistConfig = {
- key: "gotosocial-settings",
- storage: require("redux-persist/lib/storage").default,
- stateReconciler: require("redux-persist/lib/stateReconciler/autoMergeLevel2").default,
- whitelist: ["oauth"],
- blacklist: ["temporary"]
-};
+const query = require("../lib/query/base");
const combinedReducers = combineReducers({
oauth: require("./reducers/oauth").reducer,
@@ -37,13 +39,27 @@ const combinedReducers = combineReducers({
temporary: require("./reducers/temporary").reducer,
user: require("./reducers/user").reducer,
admin: require("./reducers/admin").reducer,
+ [query.reducerPath]: query.reducer
});
-const persistedReducer = persistReducer(persistConfig, combinedReducers);
-const composedEnhancer = composeWithDevTools(applyMiddleware(thunk));
+const persistedReducer = persistReducer({
+ key: "gotosocial-settings",
+ storage: require("redux-persist/lib/storage").default,
+ stateReconciler: require("redux-persist/lib/stateReconciler/autoMergeLevel2").default,
+ whitelist: ["oauth"],
+}, combinedReducers);
+
+const store = configureStore({
+ reducer: persistedReducer,
+ middleware: (getDefaultMiddleware) => {
+ return getDefaultMiddleware({
+ serializableCheck: {
+ ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER, "temporary/setScrollElement"]
+ }
+ }).concat(query.middleware);
+ }
+});
-// TODO: change to configureStore
-const store = createStore(persistedReducer, composedEnhancer);
const persistor = persistStore(store);
module.exports = { store, persistor }; \ No newline at end of file
diff --git a/web/source/settings/redux/reducers/admin.js b/web/source/settings/redux/reducers/admin.js
index 666286178..57ca83d7e 100644
--- a/web/source/settings/redux/reducers/admin.js
+++ b/web/source/settings/redux/reducers/admin.js
@@ -19,7 +19,6 @@
"use strict";
const { createSlice } = require("@reduxjs/toolkit");
-const defaultValue = require("default-value");
function sortBlocks(blocks) {
return blocks.sort((a, b) => { // alphabetical sort
@@ -35,13 +34,6 @@ function emptyBlock() {
};
}
-function emptyEmojiForm() {
- return {
- id: Date.now(),
- shortcode: ""
- };
-}
-
module.exports = createSlice({
name: "admin",
initialState: {
@@ -52,10 +44,7 @@ module.exports = createSlice({
exportType: "plain",
...emptyBlock()
},
- newInstanceBlocks: {},
- emoji: {},
- emojiById: {},
- newEmoji: emptyEmojiForm()
+ newInstanceBlocks: {}
},
reducers: {
setBlockedInstances: (state, { payload }) => {
@@ -105,34 +94,6 @@ module.exports = createSlice({
state.bulkBlock.list = Object.values(state.blockedInstances).map((entry) => {
return entry.domain;
}).join("\n");
- },
-
- setEmoji: (state, {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);
- state.emojiById[emoji.id] = emoji;
- });
- },
-
- updateNewEmojiVal: (state, { payload: [key, val] }) => {
- state.newEmoji[key] = val;
- },
-
- addEmoji: (state, {payload: emoji}) => {
- if (emoji.category == undefined) {
- emoji.category = "Unsorted";
- }
- if (emoji.id == undefined) {
- emoji.id = Date.now();
- }
- state.emoji[emoji.category] = defaultValue(state.emoji[emoji.category], []);
- state.emoji[emoji.category].push(emoji);
- state.emojiById[emoji.id] = emoji;
- },
+ }
}
}); \ No newline at end of file
diff --git a/web/source/settings/style.css b/web/source/settings/style.css
index 35d11fa08..93e52f680 100644
--- a/web/source/settings/style.css
+++ b/web/source/settings/style.css
@@ -283,6 +283,12 @@ section.with-sidebar > div {
}
}
+.form-flex {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
.file-upload > div {
display: flex;
gap: 1rem;
@@ -330,19 +336,6 @@ section.with-sidebar > div {
flex-direction: column;
justify-content: center;
- div.form-field {
- width: 100%;
- display: flex;
-
- span {
- flex: 1 1 auto;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- padding: 0.3rem 0;
- }
- }
-
h3 {
margin-top: 0;
margin-bottom: 0.5rem;
@@ -363,6 +356,20 @@ section.with-sidebar > div {
font-weight: bold;
}
+.form-field.file {
+ width: 100%;
+ display: flex;
+}
+
+
+span.form-info {
+ flex: 1 1 auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding: 0.3rem 0;
+}
+
.list {
display: flex;
flex-direction: column;
diff --git a/web/source/settings/user/profile.js b/web/source/settings/user/profile.js
index 3162fa0dd..21e86ce66 100644
--- a/web/source/settings/user/profile.js
+++ b/web/source/settings/user/profile.js
@@ -18,7 +18,6 @@
"use strict";
-const Promise = require("bluebird");
const React = require("react");
const Redux = require("react-redux");