diff --git a/web/source/css/_colors.css b/web/source/css/_colors.css index ca17f5798..86ef80c20 100644 --- a/web/source/css/_colors.css +++ b/web/source/css/_colors.css @@ -47,7 +47,11 @@ $blue3: #89caff; /* hover/selected accent to $blue2, can be used with $gray1 (7. $error1: #860000; /* Error border/foreground text, can be used with $error2 (5.0), $white1 (10), $white2 (5.1) */ $error2: #ff9796; /* Error background text, can be used with $error1 (5.0), $gray1 (6.6), $gray2 (5.3), $gray3 (4.8) */ $error3: #dd2c2c; /* Error button background text, can be used with $white1 (4.51) */ -$error-link: #185F8C; /* Error link text, can be used with $error2 (5.54) */ +$error-link: #01318C; /* Error link text, can be used with $error2 (5.56) */ + +$info-fg: $gray1; +$info-bg: #b3ddff; +$info-link: $error-link; $fg: $white1; $bg: $gray1; @@ -92,6 +96,7 @@ $avatar-border: $orange2; $input-bg: $gray4; $input-disabled-bg: $gray2; $input-border: $blue1; +$input-error-border: $error3; $input-focus-border: $blue3; $settings-nav-bg: $bg-accent; @@ -107,5 +112,6 @@ $settings-nav-bg-active: $gray2; $error-fg: $error1; $error-bg: $error2; -$settings-entry-bg: $gray3; +$settings-entry-bg: $gray2; +$settings-entry-alternate-bg: $gray3; $settings-entry-hover-bg: $gray4; \ No newline at end of file diff --git a/web/source/css/base.css b/web/source/css/base.css index 340052b5a..e0350577d 100644 --- a/web/source/css/base.css +++ b/web/source/css/base.css @@ -311,12 +311,16 @@ input, select, textarea, .input { font-size: 1rem; padding: 0.3rem; - &:focus { + &:focus, &:active { border-color: $input-focus-border; } + &:invalid { + border-color: $input-error-border; + } + &:disabled { - background: $input-disabled-bg; + background: transparent; } } diff --git a/web/source/settings/admin/actions.js b/web/source/settings/admin/actions.js index 7bbdf548c..b91d81e14 100644 --- a/web/source/settings/admin/actions.js +++ b/web/source/settings/admin/actions.js @@ -28,7 +28,7 @@ const { TextInput } = require("../components/form/inputs"); const MutationButton = require("../components/form/mutation-button"); module.exports = function AdminActionPanel() { - const daysField = useTextInput("days", {defaultValue: 30}); + const daysField = useTextInput("days", { defaultValue: 30 }); const [mediaCleanup, mediaCleanupResult] = query.useMediaCleanupMutation(); @@ -54,7 +54,7 @@ module.exports = function AdminActionPanel() { min="0" placeholder="30" /> - + ); diff --git a/web/source/settings/admin/emoji/local/detail.js b/web/source/settings/admin/emoji/local/detail.js index 0687680b4..c509ab4f4 100644 --- a/web/source/settings/admin/emoji/local/detail.js +++ b/web/source/settings/admin/emoji/local/detail.js @@ -34,19 +34,19 @@ const base = "/settings/custom-emoji/local"; module.exports = function EmojiDetailRoute() { let [_match, params] = useRoute(`${base}/:emojiId`); if (params?.emojiId == undefined) { - return ; + return ; } else { return (
< go back - +
); } }; -function EmojiDetailData({emojiId}) { - const {currentData: emoji, isLoading, error} = query.useGetEmojiQuery(emojiId); +function EmojiDetailData({ emojiId }) { + const { currentData: emoji, isLoading, error } = query.useGetEmojiQuery(emojiId); if (error) { return ( @@ -57,20 +57,20 @@ function EmojiDetailData({emojiId}) { } else if (isLoading) { return (
- +
); } else { - return ; + return ; } } -function EmojiDetail({emoji}) { +function EmojiDetail({ emoji }) { const [modifyEmoji, modifyResult] = query.useEditEmojiMutation(); const [isNewCategory, setIsNewCategory] = React.useState(false); - const [categoryState, _resetCategory, { category }] = useComboBoxInput("category", {defaultValue: emoji.category}); + const [categoryState, _resetCategory, { category }] = useComboBoxInput("category", { defaultValue: emoji.category }); const [onFileChange, _resetFile, { image, imageURL, imageInfo }] = useFileInput("image", { withPreview: true, @@ -78,26 +78,26 @@ function EmojiDetail({emoji}) { }); function modifyCategory() { - modifyEmoji({id: emoji.id, category: category.trim()}); + modifyEmoji({ id: emoji.id, category: category.trim() }); } function modifyImage() { - modifyEmoji({id: emoji.id, image: image}); + modifyEmoji({ id: emoji.id, image: image }); } React.useEffect(() => { - if (category != emoji.category && !categoryState.open && !isNewCategory && category.trim().length > 0) { - modifyEmoji({id: emoji.id, category: category.trim()}); + if (category != emoji.category && !categoryState.open && !isNewCategory && category?.trim().length > 0) { + modifyEmoji({ id: emoji.id, category: category.trim() }); } }, [isNewCategory, category, categoryState.open, emoji.category, emoji.id, modifyEmoji]); return ( <>
- {emoji.shortcode} + {emoji.shortcode}

{emoji.shortcode}

- +
@@ -114,7 +114,7 @@ function EmojiDetail({emoji}) { categoryState={categoryState} setIsNew={setIsNewCategory} > - @@ -153,7 +153,7 @@ function EmojiDetail({emoji}) { ); } -function DeleteButton({id}) { +function DeleteButton({ id }) { // TODO: confirmation dialog? const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation(); @@ -163,7 +163,7 @@ function DeleteButton({id}) { } if (deleteResult.isSuccess) { - return ; + return ; } return ( diff --git a/web/source/settings/admin/emoji/local/new-emoji.js b/web/source/settings/admin/emoji/local/new-emoji.js index e340e8559..3a539686b 100644 --- a/web/source/settings/admin/emoji/local/new-emoji.js +++ b/web/source/settings/admin/emoji/local/new-emoji.js @@ -50,7 +50,7 @@ module.exports = function NewEmojiForm({ emoji }) { const [onShortcodeChange, resetShortcode, { shortcode, setShortcode, shortcodeRef }] = useTextInput("shortcode", { validator: function validateShortcode(code) { // technically invalid, but hacky fix to prevent validation error on page load - if (shortcode == "") {return "";} + if (shortcode == "") { return ""; } if (emojiCodes.has(code)) { return "Shortcode already in use"; @@ -161,7 +161,7 @@ module.exports = function NewEmojiForm({ emoji }) { categoryState={categoryState} /> - + ); diff --git a/web/source/settings/admin/federation.js b/web/source/settings/admin/federation.js deleted file mode 100644 index 5cfe2254c..000000000 --- a/web/source/settings/admin/federation.js +++ /dev/null @@ -1,386 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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"); -const React = require("react"); -const Redux = require("react-redux"); -const {Switch, Route, Link, Redirect, useRoute, useLocation} = require("wouter"); -const fileDownload = require("js-file-download"); - -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 Loading = require("../components/loading"); -const { matchSorter } = require("match-sorter"); - -const base = "/settings/admin/federation"; - -module.exports = function AdminSettings() { - const dispatch = Redux.useDispatch(); - const loadedBlockedInstances = Redux.useSelector(state => state.admin.loadedBlockedInstances); - - React.useEffect(() => { - if (!loadedBlockedInstances ) { - Promise.try(() => { - return dispatch(api.admin.fetchDomainBlocks()); - }); - } - }, [dispatch, loadedBlockedInstances]); - - if (!loadedBlockedInstances) { - return ( -
-

Federation

-
- -
-
- ); - } - - return ( - - - - - - - ); -}; - -function InstanceOverview() { - const [filter, setFilter] = React.useState(""); - const blockedInstances = Redux.useSelector(state => state.admin.blockedInstances); - const [_location, setLocation] = useLocation(); - - const filteredInstances = React.useMemo(() => { - return matchSorter(Object.values(blockedInstances), filter, {keys: ["domain"]}); - }, [blockedInstances, filter]); - - function filterFormSubmit(e) { - e.preventDefault(); - setLocation(`${base}/${filter}`); - } - - return ( - <> -

Federation

- Here you can see an overview of blocked instances. - -
-

Blocked instances

-
- setFilter(e.target.value)}/> - Add block -
-
- {filteredInstances.map((entry) => { - return ( - - - - {entry.domain} - - - {new Date(entry.created_at).toLocaleString()} - - - - ); - })} -
-
- - - - ); -} - -const Bulk = formFields(adminActions.updateBulkBlockVal, (state) => state.admin.bulkBlock); -function BulkBlocking() { - const dispatch = Redux.useDispatch(); - const {bulkBlock, blockedInstances} = Redux.useSelector(state => state.admin); - - const [errorMsg, setError] = React.useState(""); - const [statusMsg, setStatus] = React.useState(""); - - function importBlocks() { - setStatus("Processing"); - setError(""); - return Promise.try(() => { - return dispatch(api.admin.bulkDomainBlock()); - }).then(({success, invalidDomains}) => { - return Promise.try(() => { - return resetBulk(); - }).then(() => { - dispatch(adminActions.updateBulkBlockVal(["list", invalidDomains.join("\n")])); - - let stat = ""; - if (success == 0) { - return setError("No valid domains in import"); - } else if (success == 1) { - stat = "Imported 1 domain"; - } else { - stat = `Imported ${success} domains`; - } - - if (invalidDomains.length > 0) { - if (invalidDomains.length == 1) { - stat += ", input contained 1 invalid domain."; - } else { - stat += `, input contained ${invalidDomains.length} invalid domains.`; - } - } else { - stat += "!"; - } - - setStatus(stat); - }); - }).catch((e) => { - setError(e.message); - setStatus(""); - }); - } - - function exportBlocks() { - return Promise.try(() => { - setStatus("Exporting"); - setError(""); - let asJSON = bulkBlock.exportType.startsWith("json"); - let _asCSV = bulkBlock.exportType.startsWith("csv"); - - let exportList = Object.values(blockedInstances).map((entry) => { - if (asJSON) { - return { - domain: entry.domain, - public_comment: entry.public_comment - }; - } else { - return entry.domain; - } - }); - - if (bulkBlock.exportType == "json") { - return dispatch(adminActions.updateBulkBlockVal(["list", JSON.stringify(exportList)])); - } else if (bulkBlock.exportType == "json-download") { - return fileDownload(JSON.stringify(exportList), "block-export.json"); - } else if (bulkBlock.exportType == "plain") { - return dispatch(adminActions.updateBulkBlockVal(["list", exportList.join("\n")])); - } - }).then(() => { - setStatus("Exported!"); - }).catch((e) => { - setError(e.message); - setStatus(""); - }); - } - - function resetBulk(e) { - if (e != undefined) { - e.preventDefault(); - } - return dispatch(adminActions.resetBulkBlockVal()); - } - - function disableInfoFields(props={}) { - if (bulkBlock.list[0] == "[") { - return { - ...props, - disabled: true, - placeHolder: "Domain list is a JSON import, input disabled" - }; - } else { - return props; - } - } - - return ( -
-

Import / Export reset

- - - - - - - - -
- -
- -
-
- -
- -
- - - - - - - - - - }/> -
-
-
- {errorMsg.length > 0 && -
{errorMsg}
- } - {statusMsg.length > 0 && -
{statusMsg}
- } -
-
-
- ); -} - -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, - inputs get re-created on every change, causing them to lose focus, and bad performance - */ - let [_match, {domain}] = useRoute(`${base}/:domain`); - - if (domain == "view") { // from form field submission - let realDomain = (new URL(document.location)).searchParams.get("domain"); - if (realDomain == undefined) { - return ; - } else { - domain = realDomain; - } - } - - function alterDomain([key, val]) { - return adminActions.updateDomainBlockVal([domain, key, val]); - } - - const fields = formFields(alterDomain, (state) => state.admin.newInstanceBlocks[domain]); - - return ; -} - -function InstancePage({domain, Form}) { - const dispatch = Redux.useDispatch(); - const entry = Redux.useSelector(state => state.admin.newInstanceBlocks[domain]); - const [_location, setLocation] = useLocation(); - - React.useEffect(() => { - if (entry == undefined) { - dispatch(api.admin.getEditableDomainBlock(domain)); - } - }, [dispatch, domain, entry]); - - const [errorMsg, setError] = React.useState(""); - const [statusMsg, setStatus] = React.useState(""); - - if (entry == undefined) { - return ; - } - - const updateBlock = submit( - () => dispatch(api.admin.updateDomainBlock(domain)), - {setStatus, setError} - ); - - const removeBlock = submit( - () => dispatch(api.admin.removeDomainBlock(domain)), - {setStatus, setError, startStatus: "Removing", successStatus: "Removed!", onSuccess: () => { - setLocation(base); - }} - ); - - return ( -
-

Federation settings for: {domain}

- {entry.new - ? "No stored block yet, you can add one below:" - : Editing domain blocks is not implemented yet, check here for progress. - } - - - - - - - -
- {entry.new - ? - : - } - - {errorMsg.length > 0 && -
{errorMsg}
- } - {statusMsg.length > 0 && -
{statusMsg}
- } -
-
- ); -} \ No newline at end of file diff --git a/web/source/settings/admin/federation/detail.js b/web/source/settings/admin/federation/detail.js new file mode 100644 index 000000000..eb33960a7 --- /dev/null +++ b/web/source/settings/admin/federation/detail.js @@ -0,0 +1,146 @@ +/* + 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 { useRoute, Redirect } = require("wouter"); + +const query = require("../../lib/query"); + +const { useTextInput, useBoolInput } = require("../../lib/form"); + +const useFormSubmit = require("../../lib/form/submit"); + +const { TextInput, Checkbox, TextArea } = require("../../components/form/inputs"); + +const Loading = require("../../components/loading"); +const BackButton = require("../../components/back-button"); +const MutationButton = require("../../components/form/mutation-button"); + +module.exports = function InstanceDetail({ baseUrl }) { + const { data: blockedInstances = [], isLoading } = query.useInstanceBlocksQuery(); + + let [_match, { domain }] = useRoute(`${baseUrl}/:domain`); + + if (domain == "view") { // from form field submission + domain = (new URL(document.location)).searchParams.get("domain"); + } + + const existingBlock = React.useMemo(() => { + return blockedInstances.find((block) => block.domain == domain); + }, [blockedInstances, domain]); + + if (domain == undefined) { + return ; + } + + let infoContent = null; + + if (isLoading) { + infoContent = ; + } else if (existingBlock == undefined) { + infoContent = No stored block yet, you can add one below:; + } else { + infoContent = ( +
+ + Editing domain blocks isn't implemented yet, check here for progress +
+ ); + } + + return ( +
+

Federation settings for: {domain}

+ {infoContent} + +
+ ); +}; + +function DomainBlockForm({ defaultDomain, block = {} }) { + const isExistingBlock = block.domain != undefined; + + const disabledForm = isExistingBlock + ? { + disabled: true, + title: "Domain suspensions currently cannot be edited." + } + : {}; + + const form = { + domain: useTextInput("domain", { defaultValue: block.domain ?? defaultDomain }), + obfuscate: useBoolInput("obfuscate", { defaultValue: block.obfuscate }), + commentPrivate: useTextInput("private_comment", { defaultValue: block.private_comment }), + commentPublic: useTextInput("public_comment", { defaultValue: block.public_comment }) + }; + + const [submitForm, addResult] = useFormSubmit(form, query.useAddInstanceBlockMutation(), { changedOnly: false }); + + const [removeBlock, removeResult] = query.useRemoveInstanceBlockMutation({ fixedCacheKey: block.id }); + + return ( +
+ + + + + } - + ); } \ No newline at end of file diff --git a/web/source/settings/user/settings.js b/web/source/settings/user/settings.js index 3c11a04b1..c6fd035e1 100644 --- a/web/source/settings/user/settings.js +++ b/web/source/settings/user/settings.js @@ -48,7 +48,7 @@ module.exports = function UserSettings() { ); }; -function UserSettingsForm({data: {source}}) { +function UserSettingsForm({ data: { source } }) { /* form keys - string source[privacy] - bool source[sensitive] @@ -57,20 +57,20 @@ function UserSettingsForm({data: {source}}) { */ const form = { - defaultPrivacy: useTextInput("source[privacy]", {defaultValue: source.privacy ?? "unlisted"}), - isSensitive: useBoolInput("source[sensitive]", {defaultValue: source.sensitive}), - language: useTextInput("source[language]", {defaultValue: source.language ?? "EN"}), - format: useTextInput("source[status_format]", {defaultValue: source.status_format ?? "plain"}), + defaultPrivacy: useTextInput("source[privacy]", { defaultValue: source.privacy ?? "unlisted" }), + isSensitive: useBoolInput("source[sensitive]", { defaultValue: source.sensitive }), + language: useTextInput("source[language]", { defaultValue: source.language ?? "EN" }), + format: useTextInput("source[status_format]", { defaultValue: source.status_format ?? "plain" }), }; - const [result, submitForm] = useFormSubmit(form, query.useUpdateCredentialsMutation()); + const [submitForm, result] = useFormSubmit(form, query.useUpdateCredentialsMutation()); return ( <>

Post settings