Move channel mentioning to use @mui/Autocomplete combobox without search functionality

This commit is contained in:
Rafael 2021-12-06 14:28:36 -03:00 committed by Thomas Zarebczan
parent a459e98cab
commit ea84d1af56
10 changed files with 213 additions and 263 deletions

View file

@ -84,7 +84,6 @@ export function CommentCreate(props: Props) {
settingsByChannelId, settingsByChannelId,
shouldFetchComment, shouldFetchComment,
supportDisabled, supportDisabled,
uri,
createComment, createComment,
doFetchCreatorSettings, doFetchCreatorSettings,
doToast, doToast,
@ -156,7 +155,10 @@ export function CommentCreate(props: Props) {
} }
function altEnterListener(e: SyntheticKeyboardEvent<*>) { function altEnterListener(e: SyntheticKeyboardEvent<*>) {
if ((isLivestream || e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) { // $FlowFixMe
const isTyping = e.target.attributes['term'];
if (((isLivestream && !isTyping) || e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
e.preventDefault(); e.preventDefault();
buttonRef.current.click(); buttonRef.current.click();
} }
@ -443,10 +445,11 @@ export function CommentCreate(props: Props) {
closeSelector={() => setShowEmotes(false)} closeSelector={() => setShowEmotes(false)}
/> />
)} )}
<FormField <FormField
autoFocus={isReply} autoFocus={isReply}
charCount={charCount} charCount={charCount}
className={isReply ? 'create__reply' : 'create___comment'} className={isReply ? 'create__reply' : 'create__comment'}
disabled={isFetchingChannels} disabled={isFetchingChannels}
isLivestream={isLivestream} isLivestream={isLivestream}
label={ label={
@ -457,7 +460,7 @@ export function CommentCreate(props: Props) {
<SelectChannel tiny /> <SelectChannel tiny />
</div> </div>
} }
name={isReply ? 'create__reply' : 'create___comment'} name={isReply ? 'create__reply' : 'create__comment'}
onBlur={() => window.removeEventListener('keydown', altEnterListener)} onBlur={() => window.removeEventListener('keydown', altEnterListener)}
onChange={(e) => setCommentValue(SIMPLE_SITE || !advancedEditor || isReply ? e.target.value : e)} onChange={(e) => setCommentValue(SIMPLE_SITE || !advancedEditor || isReply ? e.target.value : e)}
onFocus={() => window.addEventListener('keydown', altEnterListener)} onFocus={() => window.addEventListener('keydown', altEnterListener)}
@ -470,7 +473,6 @@ export function CommentCreate(props: Props) {
ref={formFieldRef} ref={formFieldRef}
textAreaMaxLength={isLivestream ? FF_MAX_CHARS_IN_LIVESTREAM_COMMENT : FF_MAX_CHARS_IN_COMMENT} textAreaMaxLength={isLivestream ? FF_MAX_CHARS_IN_LIVESTREAM_COMMENT : FF_MAX_CHARS_IN_COMMENT}
type={!SIMPLE_SITE && advancedEditor && !isReply ? 'markdown' : 'textarea'} type={!SIMPLE_SITE && advancedEditor && !isReply ? 'markdown' : 'textarea'}
uri={uri}
value={commentValue} value={commentValue}
/> />
</> </>

View file

@ -39,7 +39,6 @@ type Props = {
stretch?: boolean, stretch?: boolean,
textAreaMaxLength?: number, textAreaMaxLength?: number,
type?: string, type?: string,
uri?: string,
value?: string | number, value?: string | number,
onChange?: (any) => any, onChange?: (any) => any,
openEmoteMenu?: () => void, openEmoteMenu?: () => void,
@ -86,7 +85,6 @@ export class FormField extends React.PureComponent<Props> {
stretch, stretch,
textAreaMaxLength, textAreaMaxLength,
type, type,
uri,
openEmoteMenu, openEmoteMenu,
quickActionHandler, quickActionHandler,
render, render,
@ -254,8 +252,6 @@ export class FormField extends React.PureComponent<Props> {
id={name} id={name}
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT} maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
inputRef={this.input} inputRef={this.input}
hideSuggestions={hideSuggestions}
uri={uri}
isLivestream={isLivestream} isLivestream={isLivestream}
{...inputProps} {...inputProps}
/> />

View file

@ -1,10 +1,9 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectClaimForUri, selectIsUriResolving } from 'redux/selectors/claims'; import { selectClaimForUri } from 'redux/selectors/claims';
import TextareaSuggestionsItem from './view'; import TextareaSuggestionsItem from './view';
const select = (state, props) => ({ const select = (state, props) => ({
claim: props.uri && selectClaimForUri(state, props.uri), claim: props.uri && selectClaimForUri(state, props.uri),
isResolvingUri: props.uri && selectIsUriResolving(state, props.uri),
}); });
export default connect(select)(TextareaSuggestionsItem); export default connect(select)(TextareaSuggestionsItem);

View file

@ -1,43 +1,27 @@
// @flow // @flow
import { ComboboxOption, ComboboxOptionText } from '@reach/combobox';
import ChannelThumbnail from 'component/channelThumbnail'; import ChannelThumbnail from 'component/channelThumbnail';
import React from 'react'; import React from 'react';
type Props = { type Props = {
claim?: Claim, claim?: Claim,
isResolvingUri: boolean,
uri?: string, uri?: string,
}; };
export default function TextareaSuggestionsItem(props: Props) { export default function TextareaSuggestionsItem(props: Props) {
const { claim, isResolvingUri, uri } = props; const { claim, uri, ...autocompleteProps } = props;
if (!claim) return null; if (!claim) return null;
if (isResolvingUri) { const value = claim.canonical_url.replace('lbry://', '').replace('#', ':');
return (
<div className="textareaSuggestion">
<div className="media__thumb media__thumb--resolving" />
</div>
);
}
const canonicalMention = claim.canonical_url.replace('lbry://', '').replace('#', ':');
return ( return (
<ComboboxOption value={canonicalMention}> <div {...autocompleteProps}>
<div className="textareaSuggestion"> <ChannelThumbnail xsmall uri={uri} />
<ChannelThumbnail xsmall uri={uri} />
<div className="textareaSuggestion__label"> <div className="textareaSuggestion__label">
<span className="textareaSuggestion__title"> <span className="textareaSuggestion__title">{(claim.value && claim.value.title) || value}</span>
{(claim.value && claim.value.title) || <ComboboxOptionText />} <span className="textareaSuggestion__value">{value}</span>
</span>
<span className="textareaSuggestion__value">
<ComboboxOptionText />
</span>
</div>
</div> </div>
</ComboboxOption> </div>
); );
} }

View file

@ -32,7 +32,7 @@ export default function TextareaTopSuggestion(props: Props) {
} }
return filteredTop && filteredTop.length > 0 ? ( return filteredTop && filteredTop.length > 0 ? (
<div className="textareaSuggestions__row"> <div className="textareaSuggestions__group">
<div className="textareaSuggestions__label"> <div className="textareaSuggestions__label">
<LbcSymbol prefix={__('Most Supported')} /> <LbcSymbol prefix={__('Most Supported')} />
</div> </div>

View file

@ -1,7 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doResolveUris } from 'redux/actions/claims'; import { doResolveUris } from 'redux/actions/claims';
import { getChannelFromClaim } from 'util/claim';
import { makeSelectClaimForUri } from 'redux/selectors/claims';
import { MAX_LIVESTREAM_COMMENTS } from 'constants/livestream'; import { MAX_LIVESTREAM_COMMENTS } from 'constants/livestream';
import { selectChannelMentionData } from 'redux/selectors/comments'; import { selectChannelMentionData } from 'redux/selectors/comments';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
@ -9,14 +7,16 @@ import { withRouter } from 'react-router';
import TextareaWithSuggestions from './view'; import TextareaWithSuggestions from './view';
const select = (state, props) => { const select = (state, props) => {
const claim = makeSelectClaimForUri(props.uri)(state); const { pathname } = props.location;
const maxComments = props.isLivestream ? MAX_LIVESTREAM_COMMENTS : -1; const maxComments = props.isLivestream ? MAX_LIVESTREAM_COMMENTS : -1;
const data = selectChannelMentionData(state, props.uri, maxComments); const uri = `lbry:/${pathname.replaceAll(':', '#')}`;
const { canonicalCommentors, canonicalSubscriptions, commentorUris } = data;
const data = selectChannelMentionData(state, uri, maxComments);
const { canonicalCommentors, canonicalCreatorUri, canonicalSubscriptions, commentorUris } = data;
return { return {
canonicalCommentors, canonicalCommentors,
canonicalCreatorUri: getChannelFromClaim(claim) && getChannelFromClaim(claim).canonical_url, canonicalCreatorUri,
canonicalSubscriptions, canonicalSubscriptions,
commentorUris, commentorUris,
showMature: selectShowMatureContent(state), showMature: selectShowMatureContent(state),

View file

@ -1,108 +1,100 @@
// @flow // @flow
import { Combobox, ComboboxInput, ComboboxPopover, ComboboxList } from '@reach/combobox';
import { matchSorter } from 'match-sorter'; import { matchSorter } from 'match-sorter';
import { SEARCH_OPTIONS } from 'constants/search';
import * as KEYCODES from 'constants/keycodes'; import * as KEYCODES from 'constants/keycodes';
import { regexInvalidURI } from 'util/lbryURI'; import Autocomplete from '@mui/material/Autocomplete';
import React from 'react'; import React from 'react';
import Spinner from 'component/spinner';
import TextareaSuggestionsItem from 'component/textareaSuggestionsItem'; import TextareaSuggestionsItem from 'component/textareaSuggestionsItem';
import TextareaTopSuggestion from 'component/textareaTopSuggestion'; import TextField from '@mui/material/TextField';
import type { ElementRef } from 'react';
import useLighthouse from 'effects/use-lighthouse';
import useThrottle from 'effects/use-throttle'; import useThrottle from 'effects/use-throttle';
const mentionRegex = /@[^\s"=?!@$%^&*;,{}<>/\\]*/gm; const mentionRegex = /@[^\s=&#:$@%?;/\\"<>%{}|^~[]*/gm;
const INPUT_DEBOUNCE_MS = 1000;
const LIGHTHOUSE_MIN_CHARACTERS = 4;
const SEARCH_SIZE = 10;
type Props = { type Props = {
canonicalCommentors: Array<string>, canonicalCommentors?: Array<string>,
canonicalCreatorUri: string, canonicalCreatorUri?: string,
canonicalSubscriptions: Array<string>, canonicalSubscriptions?: Array<string>,
className?: string, className?: string,
commentorUris: Array<string>, commentorUris?: Array<string>,
doResolveUris: (Array<string>) => void, disabled?: boolean,
hideSuggestions?: boolean, id: string,
inputRef: any, inputRef: any,
isLivestream?: boolean, isLivestream?: boolean,
maxLength?: number, maxLength?: number,
name: string,
noTopSuggestion?: boolean,
placeholder?: string, placeholder?: string,
showMature: boolean,
type?: string, type?: string,
uri?: string,
value: any, value: any,
doResolveUris: (Array<string>) => void,
onBlur: (any) => any,
onChange: (any) => any, onChange: (any) => any,
onFocus: (any) => any,
}; };
export default function TextareaWithSuggestions(props: Props) { export default function TextareaWithSuggestions(props: Props) {
const { const {
canonicalCommentors, canonicalCommentors,
canonicalCreatorUri, canonicalCreatorUri,
canonicalSubscriptions, canonicalSubscriptions: canonicalSubs,
className, className,
commentorUris, commentorUris,
doResolveUris, disabled,
hideSuggestions, id,
inputRef, inputRef,
isLivestream, isLivestream,
maxLength, maxLength,
name,
noTopSuggestion,
placeholder, placeholder,
showMature,
type, type,
value: commentValue, value: commentValue,
doResolveUris,
onBlur,
onChange, onChange,
onFocus,
} = props; } = props;
const inputProps = { className, placeholder }; const inputDefaultProps = { className, placeholder, maxLength, type, disabled };
const comboboxListRef: ElementRef<any> = React.useRef();
const [suggestionValue, setSuggestionValue] = React.useState(undefined); const [suggestionValue, setSuggestionValue] = React.useState(undefined);
const [debouncedTerm, setDebouncedTerm] = React.useState(''); const [selectedValue, setSelectedValue] = React.useState(undefined);
const [topSuggestion, setTopSuggestion] = React.useState(''); const [highlightedSuggestion, setHighlightedSuggestion] = React.useState('');
const [canonicalSearchUris, setCanonicalSearchUris] = React.useState([]); const [shouldClose, setClose] = React.useState();
const suggestionTerm = suggestionValue && suggestionValue.term; const suggestionTerm = suggestionValue && suggestionValue.term;
const isUriFromTermValid = suggestionTerm && !regexInvalidURI.test(suggestionTerm.substring(1));
const additionalOptions = { isBackgroundSearch: false, [SEARCH_OPTIONS.CLAIM_TYPE]: SEARCH_OPTIONS.INCLUDE_CHANNELS }; const shouldFilter = (uri, previous) => uri !== canonicalCreatorUri && (!previous || !previous.includes(uri));
const { results, loading } = useLighthouse(debouncedTerm, showMature, SEARCH_SIZE, additionalOptions, 0); const filteredCommentors = canonicalCommentors && canonicalCommentors.filter((uri) => shouldFilter(uri));
const stringifiedResults = JSON.stringify(results); const filteredSubs = canonicalSubs && canonicalSubs.filter((uri) => shouldFilter(uri, filteredCommentors));
const shouldFilter = (uri, previousLists) => const allOptions = [];
uri !== canonicalCreatorUri && (!previousLists || !previousLists.includes(uri)); if (canonicalCreatorUri) allOptions.push(canonicalCreatorUri);
if (filteredSubs) allOptions.push(...filteredSubs);
if (filteredCommentors) allOptions.push(...filteredCommentors);
const filteredCommentors = canonicalCommentors.filter((uri) => shouldFilter(uri)); const allOptionsGrouped =
const filteredSubs = canonicalSubscriptions.filter((uri) => shouldFilter(uri, filteredCommentors)); allOptions.length > 0
const filteredTop = shouldFilter(topSuggestion, [...filteredCommentors, ...filteredSubs]) && topSuggestion; ? allOptions.map((option) => {
const filteredSearch = const groupName =
canonicalSearchUris && (canonicalCreatorUri === option && __('Creator')) ||
canonicalSearchUris.filter((uri) => shouldFilter(uri, [...filteredCommentors, ...filteredSubs, filteredTop || ''])); (filteredSubs && filteredSubs.includes(option) && __('Following')) ||
(filteredCommentors && filteredCommentors.includes(option) && __('From comments'));
const creatorUriMatch = useSuggestionMatch(suggestionTerm || '', [canonicalCreatorUri]); return {
const subscriptionsMatch = useSuggestionMatch(suggestionTerm || '', filteredSubs); uri: option.replace('lbry://', '').replace('#', ':'),
const commentorsMatch = useSuggestionMatch(suggestionTerm || '', filteredCommentors); group: groupName,
};
})
: [];
const hasMinSearchLength = suggestionTerm && suggestionTerm.length >= LIGHTHOUSE_MIN_CHARACTERS; const allMatches = useSuggestionMatch(
const isTyping = suggestionValue && debouncedTerm !== suggestionValue.term; suggestionTerm || '',
const showPlaceholder = hasMinSearchLength && (isTyping || loading); allOptionsGrouped.map(({ uri }) => uri)
);
/** --------- **/ /** --------- **/
/** Functions **/ /** Functions **/
/** --------- **/ /** --------- **/
function handleChange(e: SyntheticInputEvent<*>) { function handleInputChange(value: string) {
onChange(e); onChange({ target: { value } });
if (hideSuggestions) return;
const { value } = e.target;
const cursorIndex = inputRef && inputRef.current && inputRef.current.selectionStart; const cursorIndex = inputRef && inputRef.current && inputRef.current.selectionStart;
const mentionMatches = value.match(mentionRegex); const mentionMatches = value.match(mentionRegex);
@ -139,17 +131,24 @@ export default function TextareaWithSuggestions(props: Props) {
const handleSelect = React.useCallback( const handleSelect = React.useCallback(
(selectedValue: string) => { (selectedValue: string) => {
setSelectedValue(selectedValue);
if (!suggestionValue) return; if (!suggestionValue) return;
const elem = inputRef && inputRef.current;
const newCursorPos = suggestionValue.index + selectedValue.length + 1;
const newValue = const newValue =
commentValue.substring(0, suggestionValue.index) + // 1) From start of comment value until term start commentValue.substring(0, suggestionValue.index) + // 1) From start of comment value until term start
`${selectedValue}` + // 2) Add the selected value `${selectedValue}` + // 2) Add the selected value
(commentValue.length > suggestionValue.lastIndex // 3) If there is more content until the the end of the comment value: (commentValue.length > suggestionValue.lastIndex // 3) If there is more content until the the end of the comment value:
? commentValue.substring(suggestionValue.index + 1, commentValue.length) // 3.a) from term end, add the rest of comment value ? commentValue.substring(suggestionValue.lastIndex, commentValue.length) // 3.a) from term end, add the rest of comment value
: ' '); // 3.b) or else, add a space for new input after : ' '); // 3.b) or else, add a space for new input after
onChange({ target: { value: newValue } }); onChange({ target: { value: newValue } });
inputRef.current.focus(); setSuggestionValue(undefined);
elem.focus();
elem.setSelectionRange(newCursorPos, newCursorPos);
}, },
[commentValue, inputRef, onChange, suggestionValue] [commentValue, inputRef, onChange, suggestionValue]
); );
@ -158,132 +157,89 @@ export default function TextareaWithSuggestions(props: Props) {
/** Effects **/ /** Effects **/
/** ------- **/ /** ------- **/
// For disabling sending on Enter on Livestream chat
React.useEffect(() => { React.useEffect(() => {
const timer = setTimeout(() => { if (!isLivestream) return;
if (isTyping && suggestionValue) setDebouncedTerm(!hasMinSearchLength ? '' : suggestionValue.term);
}, INPUT_DEBOUNCE_MS);
return () => clearTimeout(timer); if (suggestionTerm && inputRef) {
}, [hasMinSearchLength, isTyping, suggestionValue]); inputRef.current.setAttribute('term', suggestionTerm);
} else {
React.useEffect(() => { inputRef.current.removeAttribute('term');
if (!stringifiedResults) return;
const arrayResults = JSON.parse(stringifiedResults);
if (doResolveUris && arrayResults && arrayResults.length > 0) {
// $FlowFixMe
doResolveUris(arrayResults)
.then((response) => {
try {
// $FlowFixMe
const canonical_urls = Object.values(response).map(({ canonical_url }) => canonical_url);
setCanonicalSearchUris(canonical_urls);
} catch (e) {}
})
.catch((e) => {});
} }
}, [doResolveUris, stringifiedResults]); }, [inputRef, isLivestream, suggestionTerm]);
// Only resolve commentors on Livestreams when actually mentioning/looking for it // Only resolve commentors on Livestreams when first trying to mention/looking for it
React.useEffect(() => { React.useEffect(() => {
if (isLivestream && commentorUris && suggestionValue) doResolveUris(commentorUris); if (isLivestream && commentorUris && suggestionTerm) doResolveUris(commentorUris);
}, [commentorUris, doResolveUris, isLivestream, suggestionValue]); }, [commentorUris, doResolveUris, isLivestream, suggestionTerm]);
// Allow selecting with TAB key
React.useEffect(() => { React.useEffect(() => {
if (!inputRef || !suggestionValue) return;
function handleKeyDown(e: SyntheticKeyboardEvent<*>) { function handleKeyDown(e: SyntheticKeyboardEvent<*>) {
const { keyCode } = e; const { keyCode } = e;
const activeSelection = document.querySelector('[data-reach-combobox-option][data-highlighted]'); if (highlightedSuggestion && keyCode === KEYCODES.TAB) {
const firstValue = document.querySelectorAll('[data-reach-combobox-option] .textareaSuggestion__value')[0];
if (keyCode === KEYCODES.UP || keyCode === KEYCODES.DOWN) {
const selectedId = activeSelection && activeSelection.getAttribute('id');
const selectedItem = selectedId && document.querySelector(`li[id="${selectedId}"]`);
if (selectedItem) selectedItem.scrollIntoView({ block: 'nearest', inline: 'nearest' });
} else if (keyCode === KEYCODES.TAB) {
e.preventDefault(); e.preventDefault();
handleSelect(highlightedSuggestion.uri);
const activeValue = document.querySelector(
'[data-reach-combobox-option][data-highlighted] .textareaSuggestion__value'
);
if (activeValue && activeValue.innerText) {
handleSelect(activeValue.innerText);
} else if (firstValue && firstValue.innerText) {
handleSelect(firstValue.innerText);
}
} }
} }
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleSelect, inputRef, suggestionValue]); }, [handleSelect, highlightedSuggestion]);
/** ------ **/ /** ------ **/
/** Render **/ /** Render **/
/** ------ **/ /** ------ **/
const suggestionsRow = (label: string, suggestions: any) => ( const renderGroup = (group: string, children: any) => (
<div className="textareaSuggestions__row"> <div className="textareaSuggestions__group">
<div className="textareaSuggestions__label">{label}</div> <label className="textareaSuggestions__label">{group}</label>
{children}
{suggestions.map((suggestion) => (
<TextareaSuggestionsItem key={suggestion} uri={suggestion} />
))}
<hr className="textareaSuggestions__topSeparator" /> <hr className="textareaSuggestions__topSeparator" />
</div> </div>
); );
const renderInput = (params: any) => {
const { InputProps, disabled, fullWidth, id, inputProps: autocompleteInputProps } = params;
const inputProps = { ...autocompleteInputProps, ...inputDefaultProps };
const autocompleteProps = { InputProps, disabled, fullWidth, id, inputProps };
return <TextField inputRef={inputRef} multiline select={false} {...autocompleteProps} />;
};
return ( return (
<Combobox onSelect={handleSelect} aria-label={name}> <Autocomplete
{/* Regular Textarea Field */} autoHighlight
<ComboboxInput disableClearable
{...inputProps} filterOptions={(options) => options.filter(({ uri }) => allMatches && allMatches.includes(uri))}
value={commentValue} freeSolo
as="textarea" fullWidth
id={name} getOptionLabel={(option) => option.uri}
maxLength={maxLength} groupBy={(option) => option.group}
onChange={(e) => handleChange(e)} id={id}
ref={inputRef} inputValue={commentValue}
selectOnClick loading={!allMatches || allMatches.length === 0}
type={type} loadingText={__('Nothing found')}
autocomplete={false} onBlur={() => onBlur()}
/> /* Different from onInputChange, onChange is only used for the selected value,
so here it is acting simply as a selection handler (see it as onSelect) */
{/* Possible Suggestions Box */} onChange={(event, value) => handleSelect(value.uri)}
{suggestionValue && isUriFromTermValid && ( onClose={(event, reason) => reason !== 'selectOption' && setClose(true)}
<ComboboxPopover persistSelection className="textarea__suggestions"> onFocus={() => onFocus()}
<ComboboxList ref={comboboxListRef}> onHighlightChange={(event, option) => setHighlightedSuggestion(option)}
{creatorUriMatch && creatorUriMatch.length > 0 && suggestionsRow(__('Creator'), creatorUriMatch)} onInputChange={(event, value, reason) => reason === 'input' && handleInputChange(value)}
{subscriptionsMatch && subscriptionsMatch.length > 0 && suggestionsRow(__('Following'), subscriptionsMatch)} onOpen={() => suggestionTerm && setClose(false)}
{commentorsMatch && commentorsMatch.length > 0 && suggestionsRow(__('From comments'), commentorsMatch)} /* 'open' is for the popper box component, set to check for a valid term
or else it will be displayed all the time as empty */
{hasMinSearchLength && open={!!suggestionTerm && !shouldClose}
(showPlaceholder ? ( options={allOptionsGrouped}
<Spinner type="small" /> renderGroup={({ group, children }) => renderGroup(group, children)}
) : ( renderInput={(params) => renderInput(params)}
results && ( renderOption={(optionProps, option) => <TextareaSuggestionsItem uri={option.uri} {...optionProps} />}
<> value={selectedValue}
{!noTopSuggestion && ( />
<TextareaTopSuggestion
query={debouncedTerm}
filteredTop={filteredTop}
setTopSuggestion={setTopSuggestion}
/>
)}
{filteredSearch && filteredSearch.length > 0 && suggestionsRow(__('From search'), filteredSearch)}
</>
)
))}
</ComboboxList>
</ComboboxPopover>
)}
</Combobox>
); );
} }

View file

@ -11,7 +11,7 @@ import {
selectClaimIdForUri, selectClaimIdForUri,
selectClaimIdsByUri, selectClaimIdsByUri,
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
import { isClaimNsfw } from 'util/claim'; import { isClaimNsfw, getChannelFromClaim } from 'util/claim';
import { selectSubscriptionUris } from 'redux/selectors/subscriptions'; import { selectSubscriptionUris } from 'redux/selectors/subscriptions';
type State = { claims: any, comments: CommentsState }; type State = { claims: any, comments: CommentsState };
@ -264,7 +264,7 @@ export const selectRepliesForParentId = createCachedSelector(
* @param filterInputs Values returned by filterCommentsDepOnList. * @param filterInputs Values returned by filterCommentsDepOnList.
*/ */
const filterComments = (comments: Array<Comment>, claimId?: string, filterInputs: any) => { const filterComments = (comments: Array<Comment>, claimId?: string, filterInputs: any) => {
const filterProps = filterInputs.reduce(function (acc, cur, i) { const filterProps = filterInputs.reduce((acc, cur, i) => {
acc[filterCommentsPropKeys[i]] = cur; acc[filterCommentsPropKeys[i]] = cur;
return acc; return acc;
}, {}); }, {});
@ -392,30 +392,38 @@ export const selectSuperChatTotalAmountForUri = (state: State, uri: string) => {
}; };
export const selectChannelMentionData = createCachedSelector( export const selectChannelMentionData = createCachedSelector(
(state, uri) => uri,
selectClaimIdsByUri, selectClaimIdsByUri,
selectClaimsById, selectClaimsById,
selectTopLevelCommentsForUri, selectTopLevelCommentsForUri,
selectSubscriptionUris, selectSubscriptionUris,
(claimIdsByUri, claimsById, topLevelComments, subscriptionUris) => { (uri, claimIdsByUri, claimsById, topLevelComments, subscriptionUris) => {
let canonicalCreatorUri;
const commentorUris = []; const commentorUris = [];
const canonicalCommentors = []; const canonicalCommentors = [];
const canonicalSubscriptions = []; const canonicalSubscriptions = [];
topLevelComments.forEach((comment) => { if (uri) {
const uri = comment.channel_url; const claimId = claimIdsByUri[uri];
const claim = claimsById[claimId];
const channelFromClaim = claim && getChannelFromClaim(claim);
canonicalCreatorUri = channelFromClaim && channelFromClaim.canonical_url;
if (!commentorUris.includes(uri)) { topLevelComments.forEach(({ channel_url: uri }) => {
// Update: commentorUris // Check: if there are duplicate commentors
commentorUris.push(uri); if (!commentorUris.includes(uri)) {
// Update: commentorUris
commentorUris.push(uri);
// Update: canonicalCommentors // Update: canonicalCommentors
const claimId = claimIdsByUri[uri]; const claimId = claimIdsByUri[uri];
const claim = claimsById[claimId]; const claim = claimsById[claimId];
if (claim && claim.canonical_url) { if (claim && claim.canonical_url) {
canonicalCommentors.push(claim.canonical_url); canonicalCommentors.push(claim.canonical_url);
}
} }
} });
}); }
subscriptionUris.forEach((uri) => { subscriptionUris.forEach((uri) => {
// Update: canonicalSubscriptions // Update: canonicalSubscriptions
@ -426,11 +434,6 @@ export const selectChannelMentionData = createCachedSelector(
} }
}); });
return { return { canonicalCommentors, canonicalCreatorUri, canonicalSubscriptions, commentorUris };
topLevelComments,
commentorUris,
canonicalCommentors,
canonicalSubscriptions,
};
} }
)((state, uri, maxCount) => `${String(uri)}:${maxCount}`); )((state, uri, maxCount) => `${String(uri)}:${maxCount}`);

View file

@ -3,7 +3,7 @@
$thumbnailWidth: 1.5rem; $thumbnailWidth: 1.5rem;
$thumbnailWidthSmall: 1rem; $thumbnailWidthSmall: 1rem;
.create___comment { .create__comment {
position: relative; position: relative;
} }

View file

@ -1,16 +1,45 @@
.textarea__suggestions { .MuiAutocomplete-inputRoot {
padding: 0 !important;
font-family: inherit !important;
font-weight: inherit !important;
font-size: inherit !important;
.MuiOutlinedInput-notchedOutline {
visibility: hidden;
}
.create__comment {
@extend textarea;
color: var(--color-text) !important;
min-height: calc(var(--height-input) * 1.5) !important;
}
}
.MuiAutocomplete-paper {
@extend .card; @extend .card;
background-color: var(--color-card-background); background-color: var(--color-card-background);
max-height: 30vh;
overflow-y: scroll;
text-overflow: ellipsis;
box-shadow: var(--card-box-shadow); box-shadow: var(--card-box-shadow);
color: var(--color-text) !important;
.textareaSuggestions__group {
&:last-child hr {
display: none;
}
.textareaSuggestions__label {
@extend .wunderbar__label;
}
.Mui-focused {
background-color: var(--color-menu-background--active);
}
}
> .icon { > .icon {
top: 0; top: 0;
left: var(--spacing-m); left: var(--spacing-m);
height: 100%; height: 100%;
position: absolute;
z-index: 1; z-index: 1;
stroke: var(--color-input-placeholder); stroke: var(--color-input-placeholder);
} }
@ -18,61 +47,42 @@
@media (min-width: $breakpoint-small) { @media (min-width: $breakpoint-small) {
padding: 0; padding: 0;
} }
.textareaSuggestions__label:first-of-type {
margin-top: var(--spacing-xs);
}
} }
.textareaSuggestion { .MuiAutocomplete-option {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 var(--spacing-xxs); padding: 0 var(--spacing-xxs);
margin-left: var(--spacing-xxs); margin: 0 var(--spacing-xxs);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
border-radius: var(--border-radius);
.channel-thumbnail { .channel-thumbnail {
@include handleChannelGif(2.1rem); @include handleChannelGif(2.1rem);
position: absolute; margin-right: 0;
@media (min-width: $breakpoint-small) { @media (min-width: $breakpoint-small) {
@include handleChannelGif(2.1rem); @include handleChannelGif(2.1rem);
} }
} }
}
.textareaSuggestions__row { .textareaSuggestion__label {
&:last-child hr { @extend .wunderbar__suggestion-label;
display: none; margin-left: var(--spacing-m);
display: block;
position: relative;
.textareaSuggestion__title {
@extend .wunderbar__suggestion-title;
}
.textareaSuggestion__value {
@extend .wunderbar__suggestion-name;
}
} }
} }
.textareaSuggestions__topSeparator { .textareaSuggestions__topSeparator {
@extend .wunderbar__top-separator; @extend .wunderbar__top-separator;
} }
.textareaSuggestion__label {
@extend .wunderbar__suggestion-label;
margin-left: var(--spacing-m);
display: block;
position: relative;
}
.textareaSuggestions__label {
@extend .wunderbar__label;
}
.textareaSuggestions__topSeparator {
@extend .wunderbar__top-separator;
}
.textareaSuggestion__value {
@extend .wunderbar__suggestion-name;
margin-left: calc(var(--spacing-l) - var(--spacing-xxs));
}
.textareaSuggestion__title {
@extend .wunderbar__suggestion-title;
margin-left: calc(var(--spacing-l) - var(--spacing-xxs));
}