diff --git a/web/source/settings/admin/emoji/remote/parse-from-toot.js b/web/source/settings/admin/emoji/remote/parse-from-toot.js index 309619ea4..84bbbdc92 100644 --- a/web/source/settings/admin/emoji/remote/parse-from-toot.js +++ b/web/source/settings/admin/emoji/remote/parse-from-toot.js @@ -129,14 +129,16 @@ function CopyEmojiForm({ localEmojiCodes, type, emojiList }) { title: "No emoji selected, cannot perform any actions" }; + const checkListExtraProps = React.useCallback(() => ({ localEmojiCodes }), [localEmojiCodes]); + return (
This {type == "statuses" ? "toot" : "account"} uses the following custom emoji, select the ones you want to copy/disable:
{ - onChange({ valid: shortcodeField.valid }); - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [shortcodeField.valid]); + if (emoji.valid != shortcodeField.valid) { + onChange({ valid: shortcodeField.valid }); + } + }, [onChange, emoji.valid, shortcodeField.valid]); + + React.useEffect(() => { + shortcodeField.validate(); + // only need this update if it's the emoji.checked that updated, not shortcodeField + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [emoji.checked]); return ( <> diff --git a/web/source/settings/admin/federation/import-export.js b/web/source/settings/admin/federation/import-export.js index f0b46c766..93077e5da 100644 --- a/web/source/settings/admin/federation/import-export.js +++ b/web/source/settings/admin/federation/import-export.js @@ -189,9 +189,6 @@ function ImportList({ list, data: blockedInstances }) { }, [list]); const showComment = useTextInput("showComment", { defaultValue: hasComment.type ?? "public_comment" }); - let commentName = ""; - if (showComment.value == "public_comment") { commentName = "Public comment"; } - if (showComment.value == "private_comment") { commentName = "Private comment"; } const form = { domains: useCheckListInput("domains", { @@ -235,16 +232,8 @@ function ImportList({ list, data: blockedInstances }) { } /> } - - Domain - - {commentName} - - } blockedInstances={blockedInstances} commentType={showComment.value} /> @@ -280,31 +269,55 @@ function ImportList({ list, data: blockedInstances }) { ); } -function DomainEntry({ entry, onChange, blockedInstances, commentType }) { +function DomainCheckList({ field, blockedInstances, commentType }) { + const getExtraProps = React.useCallback((entry) => { + return { + comment: entry[commentType], + alreadyExists: blockedInstances[entry.domain] != undefined + }; + }, [blockedInstances, commentType]); + + return ( + + Domain + + + {commentType == "public_comment" && "Public comment"} + {commentType == "private_comment" && "Private comment"} + + } + EntryComponent={DomainEntry} + getExtraProps={getExtraProps} + /> + ); +} + +function domainValidationError(isValid) { + return isValid ? "" : "Invalid domain"; +} + +function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment } }) { const domainField = useTextInput("domain", { defaultValue: entry.domain, - validator: (value) => { - return (entry.checked && !isValidDomain(value, { wildcard: true, allowUnicode: true })) - ? "Invalid domain" - : ""; - } + initValidation: domainValidationError(entry.valid), + validator: (value) => domainValidationError( + !entry.checked || isValidDomain(value, { wildcard: true, allowUnicode: true }) + ) }); React.useEffect(() => { - onChange({ valid: domainField.valid }); - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [domainField.valid]); + if (entry.valid != domainField.valid) { + onChange({ valid: domainField.valid }); + } + }, [onChange, entry.valid, domainField.valid]); - let icon = null; - - if (blockedInstances[domainField.value] != undefined) { - icon = ( - <> - - Domain block already exists. - - ); - } + React.useEffect(() => { + domainField.validate(); + // only need this update if it's the entry.checked that updated, not domainField + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [entry.checked]); return ( <> @@ -315,8 +328,11 @@ function DomainEntry({ entry, onChange, blockedInstances, commentType }) { onChange({ domain: e.target.value, checked: true }); }} /> - {icon} -

{entry[commentType]}

+ {alreadyExists && <> + + Domain block already exists. + } +

{comment}

); } \ No newline at end of file diff --git a/web/source/settings/components/check-list.jsx b/web/source/settings/components/check-list.jsx index 9692652db..e7ed55ef3 100644 --- a/web/source/settings/components/check-list.jsx +++ b/web/source/settings/components/check-list.jsx @@ -20,15 +20,15 @@ const React = require("react"); -module.exports = function CheckList({ field, header = "All", renderEntry }) { - performance.mark("RENDER_CHECKLIST"); +module.exports = function CheckList({ field, header = "All", EntryComponent, getExtraProps }) { return (
{header}
); @@ -47,38 +47,45 @@ function CheckListHeader({ toggleAll, children }) { ); } -const CheckListEntries = React.memo(function CheckListEntries({ entries, renderEntry, updateValue }) { - const deferredEntries = React.useDeferredValue(entries); +const CheckListEntries = React.memo( + function CheckListEntries({ entries, updateValue, EntryComponent, getExtraProps }) { + const deferredEntries = React.useDeferredValue(entries); - return Object.values(deferredEntries).map((entry) => ( - - )); -}); + return Object.values(deferredEntries).map((entry) => ( + + )); + } +); /* React.memo is a performance optimization that only re-renders a CheckListEntry when it's props actually change, instead of every time anything in the list (CheckListEntries) updates */ -const CheckListEntry = React.memo(function CheckListEntry({ entry, updateValue, renderEntry }) { - const onChange = React.useCallback( - (value) => updateValue(entry.key, value), - [updateValue, entry.key] - ); +const CheckListEntry = React.memo( + function CheckListEntry({ entry, updateValue, getExtraProps, EntryComponent }) { + const onChange = React.useCallback( + (value) => updateValue(entry.key, value), + [updateValue, entry.key] + ); - return ( - - ); -}); \ No newline at end of file + const extraProps = React.useMemo(() => getExtraProps?.(entry), [getExtraProps, entry]); + + return ( + + ); + } +); \ No newline at end of file diff --git a/web/source/settings/lib/form/check-list.jsx b/web/source/settings/lib/form/check-list.jsx index 726284c28..2011e37d4 100644 --- a/web/source/settings/lib/form/check-list.jsx +++ b/web/source/settings/lib/form/check-list.jsx @@ -81,7 +81,6 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke const toggleAllRef = React.useRef(null); React.useEffect(() => { - performance.mark("GoToSocial-useCheckListInput-useEffect-start"); /* Updates (un)check all checkbox, based on shortcode checkboxes Can be 0 (not checked), 1 (checked) or 2 (indeterminate) */ @@ -108,8 +107,6 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke setToggleAllState(all ? 1 : 0); toggleAllRef.current.indeterminate = false; } - performance.mark("GoToSocial-useCheckListInput-useEffect-finish"); - performance.measure("GoToSocial-useCheckListInput-useEffect-processed", "GoToSocial-useCheckListInput-useEffect-start", "GoToSocial-useCheckListInput-useEffect-finish"); }, [state, toggleAllRef]); const reset = React.useCallback( @@ -137,7 +134,8 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke function selectedValues() { return syncpipe(state, [ (_) => Object.values(_), - (_) => _.filter((entry) => entry.checked) + (_) => _.filter((entry) => entry.checked), + (_) => _.map((entry) => ({ ...entry })) ]); } diff --git a/web/source/settings/lib/form/text.jsx b/web/source/settings/lib/form/text.jsx index dde42d6f1..c5dbf27f2 100644 --- a/web/source/settings/lib/form/text.jsx +++ b/web/source/settings/lib/form/text.jsx @@ -87,6 +87,7 @@ module.exports = function useTextInput({ name, Name }, { ref: textRef, setter: setText, valid, + validate: () => setValidation(validator(text)), hasChanged: () => text != defaultValue }); }; \ No newline at end of file