From 2a3fa58e11a6b6b42fcba08f04538e0983e26281 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Wed, 8 Sep 2021 17:08:20 +0800 Subject: [PATCH 1/3] Swap "Moderator" and "Muted words" location. Quick fix for the lack of scroll lines for the upcoming "moderator search popup". The proper fix would be to move the popup above the input when we are at the extreme bottom. --- ui/page/settingsCreator/view.jsx | 34 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/ui/page/settingsCreator/view.jsx b/ui/page/settingsCreator/view.jsx index a067d6478..92bb24b9c 100644 --- a/ui/page/settingsCreator/view.jsx +++ b/ui/page/settingsCreator/view.jsx @@ -400,23 +400,6 @@ export default function SettingsCreatorPage(props: Props) { /> - -
- -
-
-
+ + +
+ +
+
} /> -- 2.45.3 From d370cc37a8e886e7f72b9d11709f3e91469e7b40 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Fri, 27 Aug 2021 16:20:09 +0800 Subject: [PATCH 2/3] SearchChannelField - Factor out for re-use in upcoming Shared Blocklist - Improvements: - Uses floating popup to show the suggestion/result rather than inline. - Users can now press Enter to select the suggestion, instead of having to use the mouse. - Users now don't need to enter '@' for channel names. They will still need to enter the full channel name, and disambiguate with claim_id if necessary. - Fix jumpiness in position as the user types. --- ui/component/searchChannelField/index.js | 9 + ui/component/searchChannelField/view.jsx | 199 +++++++++++++++++++++++ ui/scss/component/_search.scss | 37 +++++ 3 files changed, 245 insertions(+) create mode 100644 ui/component/searchChannelField/index.js create mode 100644 ui/component/searchChannelField/view.jsx diff --git a/ui/component/searchChannelField/index.js b/ui/component/searchChannelField/index.js new file mode 100644 index 000000000..ed70eff7a --- /dev/null +++ b/ui/component/searchChannelField/index.js @@ -0,0 +1,9 @@ +import { connect } from 'react-redux'; +import { doToast } from 'redux/actions/notifications'; +import SearchChannelField from './view'; + +const perform = (dispatch) => ({ + doToast: (options) => dispatch(doToast(options)), +}); + +export default connect(null, perform)(SearchChannelField); diff --git a/ui/component/searchChannelField/view.jsx b/ui/component/searchChannelField/view.jsx new file mode 100644 index 000000000..89fa419bd --- /dev/null +++ b/ui/component/searchChannelField/view.jsx @@ -0,0 +1,199 @@ +// @flow +import React from 'react'; +import { isNameValid, parseURI } from 'lbry-redux'; +import Button from 'component/button'; +import ClaimPreview from 'component/claimPreview'; +import { FormField } from 'component/common/form-components/form-field'; +import Icon from 'component/common/icon'; +import TagsSearch from 'component/tagsSearch'; +import * as ICONS from 'constants/icons'; +import { getUriForSearchTerm } from 'util/search'; + +type Props = { + label: string, + labelAddNew: string, + labelFoundAction: string, + values: Array, // [ 'name#id', 'name#id' ] + onAdd?: (channelUri: string) => void, + onRemove?: (channelUri: string) => void, + // --- perform --- + doToast: ({ message: string }) => void, +}; + +export default function SearchChannelField(props: Props) { + const { label, labelAddNew, labelFoundAction, values, onAdd, onRemove, doToast } = props; + + const [searchTerm, setSearchTerm] = React.useState(''); + const [searchTermError, setSearchTermError] = React.useState(''); + const [searchUri, setSearchUri] = React.useState(''); + const addTagRef = React.useRef(); + + function parseUri(name: string) { + try { + return parseURI(name); + } catch (e) {} + + return undefined; + } + + function addTag(newTags: Array) { + // Ignoring multiple entries for now, although supports it. + const uri = parseUri(newTags[0].name); + + if (uri && uri.isChannel && uri.claimName && uri.claimId) { + if (!values.includes(newTags[0].name)) { + if (onAdd) { + onAdd(newTags[0].name); + } + } + } else { + doToast({ message: __('Invalid channel URL "%url%"', { url: newTags[0].name }), isError: true }); + } + } + + function removeTag(tagToRemove: Tag) { + const uri = parseUri(tagToRemove.name); + + if (uri && uri.isChannel && uri.claimName && uri.claimId) { + if (values.includes(tagToRemove.name)) { + if (onRemove) { + onRemove(tagToRemove.name); + } + } + } + } + + function clearSearchTerm() { + setSearchTerm(''); + setSearchTermError(''); + setSearchUri(''); + } + + function handleKeyPress(e) { + // We have to use 'key' instead of 'keyCode' in this event. + if (e.key === 'Enter' && addTagRef && addTagRef.current && addTagRef.current.click) { + e.preventDefault(); + addTagRef.current.click(); + } + } + + function getFoundChannelRenderActionsFn() { + function handleFoundChannelClick(claim) { + if (claim && claim.name && claim.claim_id) { + addTag([{ name: claim.name + '#' + claim.claim_id }]); + clearSearchTerm(); + } + } + + return (claim) => { + return ( +
+ ); +} + +// prettier-ignore +const HELP = { + CHANNEL_SEARCH: 'Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8', +}; diff --git a/ui/scss/component/_search.scss b/ui/scss/component/_search.scss index 1fcb5f2c0..4a520c067 100644 --- a/ui/scss/component/_search.scss +++ b/ui/scss/component/_search.scss @@ -137,3 +137,40 @@ max-width: 15rem; } } + +.search__channel { + margin-bottom: var(--spacing-l); + + fieldset-section { + margin-top: 0; + } +} + +.search__channel--popup { + position: relative; + display: inline-block; + width: 100%; +} + +.search__channel--popup-results { + position: absolute; + z-index: 99; + top: 100%; + left: 0; + right: 0; + background: #21252980; + color: white; + + .claim-preview__title { + color: #eff1f4; + } + + .button--uri-indicator, + .media__subtitle { + color: #d8dde1; + } + + .icon--help { + vertical-align: middle; + } +} -- 2.45.3 From de90d2fda6adea3d4be6183b98832e6fbb616da3 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Wed, 8 Sep 2021 20:32:13 +0800 Subject: [PATCH 3/3] Convert "moderator search" to use the new widget --- ui/page/settingsCreator/index.js | 2 - ui/page/settingsCreator/view.jsx | 190 +++++++------------------------ ui/scss/component/section.scss | 8 -- 3 files changed, 43 insertions(+), 157 deletions(-) diff --git a/ui/page/settingsCreator/index.js b/ui/page/settingsCreator/index.js index 8420dc985..0a5813069 100644 --- a/ui/page/settingsCreator/index.js +++ b/ui/page/settingsCreator/index.js @@ -16,7 +16,6 @@ import { selectFetchingBlockedWords, selectModerationDelegatesById, } from 'redux/selectors/comments'; -import { doToast } from 'redux/actions/notifications'; const select = (state) => ({ activeChannelClaim: selectActiveChannelClaim(state), @@ -36,7 +35,6 @@ const perform = (dispatch) => ({ commentModRemoveDelegate: (modChanId, modChanName, creatorChannelClaim) => dispatch(doCommentModRemoveDelegate(modChanId, modChanName, creatorChannelClaim)), commentModListDelegates: (creatorChannelClaim) => dispatch(doCommentModListDelegates(creatorChannelClaim)), - doToast: (options) => dispatch(doToast(options)), }); export default connect(select, perform)(SettingsCreatorPage); diff --git a/ui/page/settingsCreator/view.jsx b/ui/page/settingsCreator/view.jsx index 92bb24b9c..dd8c5b2ff 100644 --- a/ui/page/settingsCreator/view.jsx +++ b/ui/page/settingsCreator/view.jsx @@ -1,21 +1,17 @@ // @flow -import * as ICONS from 'constants/icons'; import * as React from 'react'; import Card from 'component/common/card'; import TagsSearch from 'component/tagsSearch'; import Page from 'component/page'; -import Button from 'component/button'; import ChannelSelector from 'component/channelSelector'; +import SearchChannelField from 'component/searchChannelField'; import SettingsRow from 'component/settingsRow'; import Spinner from 'component/spinner'; import { FormField } from 'component/common/form-components/form-field'; -import Icon from 'component/common/icon'; import LbcSymbol from 'component/common/lbc-symbol'; import I18nMessage from 'component/i18nMessage'; -import { isNameValid, parseURI } from 'lbry-redux'; -import ClaimPreview from 'component/claimPreview'; +import { parseURI } from 'lbry-redux'; import debounce from 'util/debounce'; -import { getUriForSearchTerm } from 'util/search'; const DEBOUNCE_REFRESH_MS = 1000; @@ -54,23 +50,19 @@ export default function SettingsCreatorPage(props: Props) { commentModListDelegates, fetchCreatorSettings, updateCreatorSettings, - doToast, } = props; const [commentsEnabled, setCommentsEnabled] = React.useState(true); const [mutedWordTags, setMutedWordTags] = React.useState([]); - const [moderatorTags, setModeratorTags] = React.useState([]); - const [moderatorSearchTerm, setModeratorSearchTerm] = React.useState(''); - const [moderatorSearchError, setModeratorSearchError] = React.useState(''); - const [moderatorSearchClaimUri, setModeratorSearchClaimUri] = React.useState(''); + const [moderatorUris, setModeratorUris] = React.useState([]); const [minTip, setMinTip] = React.useState(0); const [minSuper, setMinSuper] = React.useState(0); const [slowModeMin, setSlowModeMin] = React.useState(0); const [lastUpdated, setLastUpdated] = React.useState(1); - const pushSlowModeMinDebounced = React.useMemo(() => debounce(pushSlowModeMin, 1000), []); - const pushMinTipDebounced = React.useMemo(() => debounce(pushMinTip, 1000), []); - const pushMinSuperDebounced = React.useMemo(() => debounce(pushMinSuper, 1000), []); + const pushSlowModeMinDebounced = React.useMemo(() => debounce(pushSlowModeMin, 1000), []); // eslint-disable-line react-hooks/exhaustive-deps + const pushMinTipDebounced = React.useMemo(() => debounce(pushMinTip, 1000), []); // eslint-disable-line react-hooks/exhaustive-deps + const pushMinSuperDebounced = React.useMemo(() => debounce(pushMinSuper, 1000), []); // eslint-disable-line react-hooks/exhaustive-deps // ************************************************************************** // ************************************************************************** @@ -79,8 +71,8 @@ export default function SettingsCreatorPage(props: Props) { * Updates corresponding GUI states with the given PerChannelSettings values. * * @param settings - * @param fullSync If true, update all states and consider 'undefined' settings as "cleared/false"; - * if false, only update defined settings. + * @param fullSync If true, update all states and consider 'undefined' + * settings as "cleared/false"; if false, only update defined settings. */ function settingsToStates(settings: PerChannelSettings, fullSync: boolean) { const doSetMutedWordTags = (words: Array) => { @@ -137,6 +129,31 @@ export default function SettingsCreatorPage(props: Props) { updateCreatorSettings(activeChannelClaim, { min_tip_amount_super_chat: value }); } + function parseModUri(uri) { + try { + return parseURI(uri); + } catch (e) {} + return undefined; + } + + function handleModeratorAdded(channelUri: string) { + setModeratorUris([...moderatorUris, channelUri]); + const parsedUri = parseModUri(channelUri); + if (parsedUri && parsedUri.claimId && parsedUri.claimName) { + commentModAddDelegate(parsedUri.claimId, parsedUri.claimName, activeChannelClaim); + setLastUpdated(Date.now()); + } + } + + function handleModeratorRemoved(channelUri: string) { + setModeratorUris(moderatorUris.filter((x) => x !== channelUri)); + const parsedUri = parseModUri(channelUri); + if (parsedUri && parsedUri.claimId && parsedUri.claimName) { + commentModRemoveDelegate(parsedUri.claimId, parsedUri.claimName, activeChannelClaim); + setLastUpdated(Date.now()); + } + } + function addMutedWords(newTags: Array) { const validatedNewTags = []; newTags.forEach((newTag) => { @@ -162,74 +179,9 @@ export default function SettingsCreatorPage(props: Props) { setLastUpdated(Date.now()); } - function addModerator(newTags: Array) { - // Ignoring multiple entries for now, although supports it. - let modUri; - try { - modUri = parseURI(newTags[0].name); - } catch (e) {} - - if (modUri && modUri.isChannel && modUri.claimName && modUri.claimId) { - if (!moderatorTags.some((modTag) => modTag.name === newTags[0].name)) { - setModeratorTags([...moderatorTags, newTags[0]]); - commentModAddDelegate(modUri.claimId, modUri.claimName, activeChannelClaim); - setLastUpdated(Date.now()); - } - } else { - doToast({ message: __('Invalid channel URL "%url%"', { url: newTags[0].name }), isError: true }); - } - } - - function removeModerator(tagToRemove: Tag) { - let modUri; - try { - modUri = parseURI(tagToRemove.name); - } catch (e) {} - - if (modUri && modUri.isChannel && modUri.claimName && modUri.claimId) { - const newModeratorTags = moderatorTags.slice().filter((t) => t.name !== tagToRemove.name); - setModeratorTags(newModeratorTags); - commentModRemoveDelegate(modUri.claimId, modUri.claimName, activeChannelClaim); - setLastUpdated(Date.now()); - } - } - - function handleChannelSearchSelect(claim) { - if (claim && claim.name && claim.claim_id) { - addModerator([{ name: claim.name + '#' + claim.claim_id }]); - } - } - // ************************************************************************** // ************************************************************************** - // 'moderatorSearchTerm' to 'moderatorSearchClaimUri' - React.useEffect(() => { - if (!moderatorSearchTerm) { - setModeratorSearchError(''); - setModeratorSearchClaimUri(''); - } else { - const [searchUri, error] = getUriForSearchTerm(moderatorSearchTerm); - setModeratorSearchError(error ? __('Something not quite right..') : ''); - - try { - const { streamName, channelName, isChannel } = parseURI(searchUri); - - if (!isChannel && streamName && isNameValid(streamName)) { - setModeratorSearchError(__('Not a channel (prefix with "@", or enter the channel URL)')); - setModeratorSearchClaimUri(''); - } else if (isChannel && channelName && isNameValid(channelName)) { - setModeratorSearchClaimUri(searchUri); - } - } catch (e) { - if (moderatorSearchTerm !== '@') { - setModeratorSearchError(''); - } - setModeratorSearchClaimUri(''); - } - } - }, [moderatorSearchTerm, setModeratorSearchError]); - // Update local moderator states with data from API. React.useEffect(() => { commentModListDelegates(activeChannelClaim); @@ -239,15 +191,9 @@ export default function SettingsCreatorPage(props: Props) { if (activeChannelClaim) { const delegates = moderationDelegatesById[activeChannelClaim.claim_id]; if (delegates) { - setModeratorTags( - delegates.map((d) => { - return { - name: d.channelName + '#' + d.channelId, - }; - }) - ); + setModeratorUris(delegates.map((d) => `${d.channelName}#${d.channelId}`)); } else { - setModeratorTags([]); + setModeratorUris([]); } } }, [activeChannelClaim, moderationDelegatesById]); @@ -401,64 +347,14 @@ export default function SettingsCreatorPage(props: Props) {
-
- - - {__('Search channel')} - - - } - placeholder={__('Enter a @username or URL')} - value={moderatorSearchTerm} - onChange={(e) => setModeratorSearchTerm(e.target.value)} - error={moderatorSearchError} - /> - {moderatorSearchClaimUri && ( -
- { - return ( -
- )} -
+
diff --git a/ui/scss/component/section.scss b/ui/scss/component/section.scss index a45b88a7e..9d77d8a8c 100644 --- a/ui/scss/component/section.scss +++ b/ui/scss/component/section.scss @@ -230,10 +230,6 @@ .settings__row--value { width: 100%; - fieldset-section:not(:only-child) { - margin-top: var(--spacing-s); - } - fieldset-section.radio { margin-top: var(--spacing-s); } @@ -242,10 +238,6 @@ margin-top: var(--spacing-m); } - .tags--remove { - margin-bottom: 0; - } - .tags__input-wrapper { .tag__input { height: unset; -- 2.45.3