Add Channel Mention selection ability #7151
4 changed files with 90 additions and 35 deletions
|
@ -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),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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')} />
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in a new issue