Add Channel Mention selection ability #7151

Merged
saltrafael merged 9 commits from channel-mention into master 2021-09-30 23:30:32 +02:00
4 changed files with 90 additions and 35 deletions
Showing only changes of commit ac93273d59 - Show all commits

View file

@ -11,16 +11,24 @@ const select = (state, props) => {
const topLevelComments = makeSelectTopLevelCommentsForUri(props.uri)(state); const topLevelComments = makeSelectTopLevelCommentsForUri(props.uri)(state);
const commentorUris = []; const commentorUris = [];
// Avoid repeated commentors
topLevelComments.map(({ channel_url }) => !commentorUris.includes(channel_url) && commentorUris.push(channel_url)); topLevelComments.map(({ channel_url }) => !commentorUris.includes(channel_url) && commentorUris.push(channel_url));
const getUnresolved = (uris) => const getUnresolved = (uris) =>
uris.map((uri) => !makeSelectClaimForUri(uri)(state) && uri).filter((uri) => uri !== false); uris.map((uri) => !makeSelectClaimForUri(uri)(state) && uri).filter((uri) => uri !== false);
const getCanonical = (uris) =>
uris
.map((uri) => makeSelectClaimForUri(uri)(state) && makeSelectClaimForUri(uri)(state).canonical_url)
.filter((uri) => Boolean(uri));
return { return {
commentorUris, commentorUris,
subscriptionUris, subscriptionUris,
unresolvedCommentors: getUnresolved(commentorUris), unresolvedCommentors: getUnresolved(commentorUris),
unresolvedSubscriptions: getUnresolved(subscriptionUris), unresolvedSubscriptions: getUnresolved(subscriptionUris),
canonicalCreator: getCanonical([props.creatorUri])[0],
canonicalCommentors: getCanonical(commentorUris),
canonicalSubscriptions: getCanonical(subscriptionUris),
showMature: selectShowMatureContent(state), showMature: selectShowMatureContent(state),
}; };
}; };

View file

@ -19,12 +19,15 @@ type Props = {
mentionTerm: string, mentionTerm: string,
noTopSuggestion?: boolean, noTopSuggestion?: boolean,
showMature: boolean, showMature: boolean,
creatorUri: string,
isLivestream: boolean, isLivestream: boolean,
creatorUri: string,
commentorUris: Array<string>, commentorUris: Array<string>,
unresolvedCommentors: Array<string>,
subscriptionUris: Array<string>, subscriptionUris: Array<string>,
unresolvedCommentors: Array<string>,
unresolvedSubscriptions: Array<string>, unresolvedSubscriptions: Array<string>,
canonicalCreator: string,
canonicalCommentors: Array<string>,
canonicalSubscriptions: Array<string>,
doResolveUris: (Array<string>) => void, doResolveUris: (Array<string>) => void,
customSelectAction?: (string, number) => void, customSelectAction?: (string, number) => void,
}; };
@ -33,6 +36,7 @@ export default function ChannelMentionSuggestions(props: Props) {
const { const {
unresolvedCommentors, unresolvedCommentors,
unresolvedSubscriptions, unresolvedSubscriptions,
canonicalCreator,
isLivestream, isLivestream,
creatorUri, creatorUri,
inputRef, inputRef,
@ -44,27 +48,31 @@ export default function ChannelMentionSuggestions(props: Props) {
} = props; } = props;
const comboboxInputRef: ElementRef<any> = React.useRef(); const comboboxInputRef: ElementRef<any> = React.useRef();
const comboboxListRef: ElementRef<any> = React.useRef(); const comboboxListRef: ElementRef<any> = React.useRef();
const [debouncedTerm, setDebouncedTerm] = React.useState('');
const mainEl = document.querySelector('.channel-mention__suggestions'); const mainEl = document.querySelector('.channel-mention__suggestions');
const [debouncedTerm, setDebouncedTerm] = React.useState('');
const [mostSupported, setMostSupported] = React.useState('');
const [canonicalResults, setCanonicalResults] = React.useState([]);
const isRefFocused = (ref) => ref && ref.current === document.activeElement; const isRefFocused = (ref) => ref && ref.current === document.activeElement;
const subscriptionUris = props.subscriptionUris.filter((uri) => uri !== creatorUri); const subscriptionUris = props.subscriptionUris.filter((uri) => uri !== creatorUri);
const canonicalSubscriptions = props.canonicalSubscriptions.filter((uri) => uri !== canonicalCreator);
const commentorUris = props.commentorUris.filter((uri) => uri !== creatorUri && !subscriptionUris.includes(uri)); const commentorUris = props.commentorUris.filter((uri) => uri !== creatorUri && !subscriptionUris.includes(uri));
const canonicalCommentors = props.canonicalCommentors.filter(
(uri) => uri !== canonicalCreator && !canonicalSubscriptions.includes(uri)
);
const termToMatch = mentionTerm && mentionTerm.replace('@', '').toLowerCase(); const termToMatch = mentionTerm && mentionTerm.replace('@', '').toLowerCase();
const allShownUris = [creatorUri, ...subscriptionUris, ...commentorUris]; const allShownUris = [creatorUri, ...subscriptionUris, ...commentorUris];
const allShownCanonical = [canonicalCreator, ...canonicalSubscriptions, ...canonicalCommentors];
const possibleMatches = allShownUris.filter((uri) => { const possibleMatches = allShownUris.filter((uri) => {
try { try {
const { channelName } = parseURI(uri); const { channelName } = parseURI(uri);
return channelName.toLowerCase().includes(termToMatch); return channelName.toLowerCase().includes(termToMatch);
} catch (e) {} } catch (e) {}
}); });
const hasSubscriptionsResolved =
subscriptionUris &&
!subscriptionUris.every((uri) => unresolvedSubscriptions && unresolvedSubscriptions.includes(uri));
const hasCommentorsShown =
commentorUris.length > 0 && commentorUris.some((uri) => possibleMatches && possibleMatches.includes(uri));
const searchSize = 5; const searchSize = 5;
const additionalOptions = { isBackgroundSearch: false, [SEARCH_OPTIONS.CLAIM_TYPE]: SEARCH_OPTIONS.INCLUDE_CHANNELS }; const additionalOptions = { isBackgroundSearch: false, [SEARCH_OPTIONS.CLAIM_TYPE]: SEARCH_OPTIONS.INCLUDE_CHANNELS };
@ -93,7 +101,7 @@ export default function ChannelMentionSuggestions(props: Props) {
}, INPUT_DEBOUNCE_MS); }, INPUT_DEBOUNCE_MS);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [isTyping, mentionTerm, hasMinLength, possibleMatches.length]); }, [hasMinLength, isTyping, mentionTerm]);
React.useEffect(() => { React.useEffect(() => {
if (!mainEl) return; if (!mainEl) return;
@ -140,7 +148,9 @@ export default function ChannelMentionSuggestions(props: Props) {
if (activeValue) { if (activeValue) {
handleSelect(activeValue, keyCode); handleSelect(activeValue, keyCode);
} else if (possibleMatches.length) { } else if (possibleMatches.length) {
handleSelect(possibleMatches[0], keyCode); // $FlowFixMe
const suggest = allShownCanonical.find((matchUri) => possibleMatches.find((uri) => uri.includes(matchUri)));
if (suggest) handleSelect(suggest, keyCode);
} else if (results) { } else if (results) {
handleSelect(mentionTerm, keyCode); handleSelect(mentionTerm, keyCode);
} }
@ -155,16 +165,25 @@ export default function ChannelMentionSuggestions(props: Props) {
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleSelect, inputRef, mentionTerm, possibleMatches, results]); }, [allShownCanonical, handleSelect, inputRef, mentionTerm, possibleMatches, results]);
React.useEffect(() => { React.useEffect(() => {
if (!stringifiedResults) return; if (!stringifiedResults) return;
const arrayResults = JSON.parse(stringifiedResults); const arrayResults = JSON.parse(stringifiedResults);
if (arrayResults && arrayResults.length > 0) doResolveUris(arrayResults); if (arrayResults && arrayResults.length > 0) {
// $FlowFixMe
doResolveUris(arrayResults).then((response) => {
try {
// $FlowFixMe
const canonical_urls = Object.values(response).map(({ canonical_url }) => canonical_url);
setCanonicalResults(canonical_urls);
} catch (e) {}
});
}
}, [doResolveUris, stringifiedResults]); }, [doResolveUris, stringifiedResults]);
// Only resolve commentors on Livestreams if actually mentioning/looking for it // Only resolve commentors on Livestreams when actually mentioning/looking for it
React.useEffect(() => { React.useEffect(() => {
if (isLivestream && unresolvedCommentors && mentionTerm) doResolveUris(unresolvedCommentors); if (isLivestream && unresolvedCommentors && mentionTerm) doResolveUris(unresolvedCommentors);
}, [doResolveUris, isLivestream, mentionTerm, unresolvedCommentors]); }, [doResolveUris, isLivestream, mentionTerm, unresolvedCommentors]);
@ -185,12 +204,23 @@ export default function ChannelMentionSuggestions(props: Props) {
if (urisToResolve.length > 0) doResolveUris(urisToResolve); if (urisToResolve.length > 0) doResolveUris(urisToResolve);
}, [doResolveUris, hasMinLength, isTyping, possibleMatches, subscriptionUris, unresolvedSubscriptions]); }, [doResolveUris, hasMinLength, isTyping, possibleMatches, subscriptionUris, unresolvedSubscriptions]);
const suggestionsRow = (label: string, suggestions: Array<string>, hasSuggestionsBelow: boolean) => { const suggestionsRow = (
if (mentionTerm !== '@' && suggestions !== results) { label: string,
suggestions: Array<string>,
canonical: Array<string>,
hasSuggestionsBelow: boolean
) => {
if (mentionTerm.length > 1 && suggestions !== results) {
suggestions = suggestions.filter((uri) => possibleMatches.includes(uri)); suggestions = suggestions.filter((uri) => possibleMatches.includes(uri));
} else if (suggestions === results) { } else if (suggestions === results) {
suggestions = suggestions.filter((uri) => !allShownUris.includes(uri)); suggestions = suggestions
.filter((uri) => !allShownUris.includes(uri))
.filter((uri) => !uri.includes(mostSupported));
} }
// $FlowFixMe
suggestions = suggestions
.map((matchUri) => canonical.find((uri) => matchUri.includes(uri)))
.filter((uri) => Boolean(uri));
return !suggestions.length ? null : ( return !suggestions.length ? null : (
<> <>
@ -214,20 +244,36 @@ export default function ChannelMentionSuggestions(props: Props) {
suggestionsRow( suggestionsRow(
__('Creator'), __('Creator'),
[creatorUri], [creatorUri],
hasSubscriptionsResolved || hasCommentorsShown || !showPlaceholder [canonicalCreator],
canonicalSubscriptions.length > 0 || commentorUris.length > 0 || !showPlaceholder
)} )}
{hasSubscriptionsResolved && {canonicalSubscriptions.length > 0 &&
suggestionsRow(__('Following'), subscriptionUris, hasCommentorsShown || !showPlaceholder)} suggestionsRow(
{commentorUris.length > 0 && suggestionsRow(__('From comments'), commentorUris, !showPlaceholder)} __('Following'),
subscriptionUris,
canonicalSubscriptions,
commentorUris.length > 0 || !showPlaceholder
)}
{commentorUris.length > 0 &&
suggestionsRow(__('From comments'), commentorUris, canonicalCommentors, !showPlaceholder)}
{showPlaceholder {hasMinLength &&
? hasMinLength && <Spinner type="small" /> (showPlaceholder ? (
: results && ( <Spinner type="small" />
) : (
results && (
<> <>
{!noTopSuggestion && <ChannelMentionTopSuggestion query={debouncedTerm} />} {!noTopSuggestion && (
{suggestionsRow(__('From search'), results, false)} <ChannelMentionTopSuggestion
query={debouncedTerm}
shownUris={allShownCanonical}
setMostSupported={(winningUri) => setMostSupported(winningUri)}
/>
)}
{suggestionsRow(__('From search'), results, canonicalResults, false)}
</> </>
)} )
))}
</ComboboxList> </ComboboxList>
</ComboboxPopover> </ComboboxPopover>
)} )}

View file

@ -7,16 +7,22 @@ type Props = {
uriFromQuery: string, uriFromQuery: string,
winningUri: string, winningUri: string,
isResolvingUri: boolean, isResolvingUri: boolean,
shownUris: Array<string>,
setMostSupported: (string) => void,
doResolveUri: (string) => void, doResolveUri: (string) => void,
}; };
export default function ChannelMentionTopSuggestion(props: Props) { export default function ChannelMentionTopSuggestion(props: Props) {
const { uriFromQuery, winningUri, isResolvingUri, doResolveUri } = props; const { uriFromQuery, winningUri, isResolvingUri, shownUris, setMostSupported, doResolveUri } = props;
React.useEffect(() => { React.useEffect(() => {
if (uriFromQuery) doResolveUri(uriFromQuery); if (uriFromQuery) doResolveUri(uriFromQuery);
}, [doResolveUri, uriFromQuery]); }, [doResolveUri, uriFromQuery]);
React.useEffect(() => {
if (winningUri) setMostSupported(winningUri);
}, [setMostSupported, winningUri]);
if (isResolvingUri) { if (isResolvingUri) {
return ( return (
<div className="channel-mention__winning-claim"> <div className="channel-mention__winning-claim">
@ -31,7 +37,7 @@ export default function ChannelMentionTopSuggestion(props: Props) {
); );
} }
return !winningUri ? null : ( return !winningUri || shownUris.includes(winningUri) ? null : (
<> <>
<div className="channel-mention__label"> <div className="channel-mention__label">
<LbcSymbol prefix={__('Most Supported')} /> <LbcSymbol prefix={__('Most Supported')} />

View file

@ -86,7 +86,7 @@ export function CommentCreate(props: Props) {
} = props; } = props;
const formFieldRef: ElementRef<any> = React.useRef(); const formFieldRef: ElementRef<any> = React.useRef();
const formFieldInputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input; const formFieldInputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input;
const selectionIndex = formFieldInputRef && formFieldInputRef.current.selectionStart; const selectionIndex = formFieldInputRef && formFieldInputRef.current && formFieldInputRef.current.selectionStart;
const buttonRef: ElementRef<any> = React.useRef(); const buttonRef: ElementRef<any> = React.useRef();
const { const {
push, push,
@ -178,12 +178,7 @@ export function CommentCreate(props: Props) {
function handleSelectMention(mentionValue, key) { function handleSelectMention(mentionValue, key) {
let newMentionValue = mentionValue.replace('lbry://', ''); let newMentionValue = mentionValue.replace('lbry://', '');
if (newMentionValue.includes('#')) { if (newMentionValue.includes('#')) newMentionValue = newMentionValue.replace('#', ':');
const fullId = newMentionValue.substring(newMentionValue.indexOf('#') + 1, newMentionValue.length);
newMentionValue = newMentionValue
.substring(0, newMentionValue.indexOf('#') + (fullId.length > 2 ? 2 : newMentionValue.length))
.replace('#', ':');
}
if (livestream && key !== KEYCODES.TAB) setPauseQuickSend(true); if (livestream && key !== KEYCODES.TAB) setPauseQuickSend(true);
setCommentValue( setCommentValue(