More comment create and textarea improvements
This commit is contained in:
parent
b3ed0027ff
commit
eef6691557
16 changed files with 250 additions and 135 deletions
|
@ -484,8 +484,8 @@ export function CommentCreate(props: Props) {
|
||||||
disabled={isFetchingChannels || disableInput}
|
disabled={isFetchingChannels || disableInput}
|
||||||
isLivestream={isLivestream}
|
isLivestream={isLivestream}
|
||||||
label={
|
label={
|
||||||
<div className="commentCreate__labelWrapper">
|
<div className="comment-create__label-wrapper">
|
||||||
<span className="commentCreate__label">
|
<span className="comment-create__label">
|
||||||
{(isReply ? __('Replying as') : isLivestream ? __('Chat as') : __('Comment as')) + ' '}
|
{(isReply ? __('Replying as') : isLivestream ? __('Chat as') : __('Comment as')) + ' '}
|
||||||
</span>
|
</span>
|
||||||
<SelectChannel tiny />
|
<SelectChannel tiny />
|
||||||
|
|
|
@ -253,7 +253,7 @@ function CommentMenuList(props: Props) {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isPinned && (
|
{isPinned && isLiveComment && (
|
||||||
<MenuItem className="comment__menu-option menu__link" onSelect={handleDismissPin}>
|
<MenuItem className="comment__menu-option menu__link" onSelect={handleDismissPin}>
|
||||||
<Icon aria-hidden icon={ICONS.DISMISS_ALL} />
|
<Icon aria-hidden icon={ICONS.DISMISS_ALL} />
|
||||||
{__('Dismiss Pin')}
|
{__('Dismiss Pin')}
|
||||||
|
|
|
@ -272,8 +272,8 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="form-field__textarea-info">
|
{!noEmojis && openEmoteMenu && (
|
||||||
{!noEmojis && openEmoteMenu && (
|
<div className="form-field__textarea-info">
|
||||||
<Button
|
<Button
|
||||||
type="alt"
|
type="alt"
|
||||||
className="button--file-action"
|
className="button--file-action"
|
||||||
|
@ -282,8 +282,8 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
icon={ICONS.EMOJI}
|
icon={ICONS.EMOJI}
|
||||||
iconSize={20}
|
iconSize={20}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -297,7 +297,6 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
key={pinnedComment.comment_id}
|
key={pinnedComment.comment_id}
|
||||||
uri={uri}
|
uri={uri}
|
||||||
pushMention={setMention}
|
pushMention={setMention}
|
||||||
handleDismissPin={() => setShowPinned(false)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import LivestreamLayout from './view';
|
import SwipeableDrawer from './view';
|
||||||
import { selectTheme } from 'redux/selectors/settings';
|
import { selectTheme } from 'redux/selectors/settings';
|
||||||
import { selectMobilePlayerDimensions } from 'redux/selectors/app';
|
import { selectMobilePlayerDimensions } from 'redux/selectors/app';
|
||||||
|
|
||||||
|
@ -8,4 +8,4 @@ const select = (state) => ({
|
||||||
mobilePlayerDimensions: selectMobilePlayerDimensions(state),
|
mobilePlayerDimensions: selectMobilePlayerDimensions(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select)(LivestreamLayout);
|
export default connect(select)(SwipeableDrawer);
|
||||||
|
|
|
@ -42,29 +42,18 @@ export default function SwipeableDrawer(props: Props) {
|
||||||
}
|
}
|
||||||
}, [coverHeight, mobilePlayerDimensions, open]);
|
}, [coverHeight, mobilePlayerDimensions, open]);
|
||||||
|
|
||||||
const DrawerGlobalStyles = () => (
|
// Reset scroll position when opening: avoid broken position where
|
||||||
<Global
|
// the drawer is lower than the video
|
||||||
styles={{
|
React.useEffect(() => {
|
||||||
'.main-wrapper__inner--filepage': {
|
if (open) {
|
||||||
overflow: open ? 'hidden' : 'unset',
|
const htmlEl = document.querySelector('html');
|
||||||
maxHeight: open ? '100vh' : 'unset',
|
if (htmlEl) htmlEl.scrollTop = 0;
|
||||||
},
|
}
|
||||||
'.MuiDrawer-root': {
|
}, [open]);
|
||||||
top: `calc(${HEADER_HEIGHT_MOBILE}px + ${videoHeight}px) !important`,
|
|
||||||
},
|
|
||||||
'.MuiDrawer-root > .MuiPaper-root': {
|
|
||||||
overflow: 'visible',
|
|
||||||
color: 'var(--color-text)',
|
|
||||||
position: 'absolute',
|
|
||||||
height: `calc(100% - ${DRAWER_PULLER_HEIGHT}px)`,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DrawerGlobalStyles />
|
<DrawerGlobalStyles open={open} videoHeight={videoHeight} />
|
||||||
|
|
||||||
<MUIDrawer
|
<MUIDrawer
|
||||||
anchor="bottom"
|
anchor="bottom"
|
||||||
|
@ -90,6 +79,35 @@ export default function SwipeableDrawer(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GlobalStylesProps = {
|
||||||
|
open?: boolean,
|
||||||
|
videoHeight: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DrawerGlobalStyles = (globalStylesProps: GlobalStylesProps) => {
|
||||||
|
const { open, videoHeight } = globalStylesProps;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Global
|
||||||
|
styles={{
|
||||||
|
'.main-wrapper__inner--filepage': {
|
||||||
|
overflow: open ? 'hidden' : 'unset',
|
||||||
|
maxHeight: open ? '100vh' : 'unset',
|
||||||
|
},
|
||||||
|
'.main-wrapper .MuiDrawer-root': {
|
||||||
|
top: `calc(${HEADER_HEIGHT_MOBILE}px + ${videoHeight}px) !important`,
|
||||||
|
},
|
||||||
|
'.main-wrapper .MuiDrawer-root > .MuiPaper-root': {
|
||||||
|
overflow: 'visible',
|
||||||
|
color: 'var(--color-text)',
|
||||||
|
position: 'absolute',
|
||||||
|
height: `calc(100% - ${DRAWER_PULLER_HEIGHT}px)`,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
type PullerProps = {
|
type PullerProps = {
|
||||||
theme: string,
|
theme: string,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectClaimForUri } from 'redux/selectors/claims';
|
import { selectClaimForUri } from 'redux/selectors/claims';
|
||||||
import TextareaSuggestionsItem from './view';
|
import TextareaSuggestionsItem from './view';
|
||||||
|
import { formatLbryChannelName } from 'util/url';
|
||||||
|
import { getClaimTitle } from 'util/claim';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => {
|
||||||
claim: props.uri && selectClaimForUri(state, props.uri),
|
const { uri } = props;
|
||||||
});
|
|
||||||
|
const claim = uri && selectClaimForUri(state, uri);
|
||||||
|
|
||||||
|
return {
|
||||||
|
claimLabel: claim && formatLbryChannelName(claim.canonical_url),
|
||||||
|
claimTitle: claim && getClaimTitle(claim.canonical_url),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default connect(select)(TextareaSuggestionsItem);
|
export default connect(select)(TextareaSuggestionsItem);
|
||||||
|
|
|
@ -3,13 +3,14 @@ import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
claim?: Claim,
|
claimLabel?: string,
|
||||||
|
claimTitle?: string,
|
||||||
emote?: any,
|
emote?: any,
|
||||||
uri?: string,
|
uri?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TextareaSuggestionsItem(props: Props) {
|
export default function TextareaSuggestionsItem(props: Props) {
|
||||||
const { claim, emote, uri, ...autocompleteProps } = props;
|
const { claimLabel, claimTitle, emote, uri, ...autocompleteProps } = props;
|
||||||
|
|
||||||
if (emote) {
|
if (emote) {
|
||||||
const { name: value, url, unicode } = emote;
|
const { name: value, url, unicode } = emote;
|
||||||
|
@ -18,8 +19,8 @@ export default function TextareaSuggestionsItem(props: Props) {
|
||||||
<div {...autocompleteProps} dispatch={undefined}>
|
<div {...autocompleteProps} dispatch={undefined}>
|
||||||
{unicode ? <div className="emote">{unicode}</div> : <img className="emote" src={url} />}
|
{unicode ? <div className="emote">{unicode}</div> : <img className="emote" src={url} />}
|
||||||
|
|
||||||
<div className="textareaSuggestion__label">
|
<div className="textarea-suggestion__label">
|
||||||
<span className="textareaSuggestion__title textareaSuggestion__value textareaSuggestion__value--emote">
|
<span className="textarea-suggestion__title textarea-suggestion__value textarea-suggestion__value--emote">
|
||||||
{value}
|
{value}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,16 +28,16 @@ export default function TextareaSuggestionsItem(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (claim) {
|
if (claimLabel) {
|
||||||
const value = claim.canonical_url.replace('lbry://', '').replace('#', ':');
|
const value = claimLabel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...autocompleteProps} dispatch={undefined}>
|
<div {...autocompleteProps} dispatch={undefined}>
|
||||||
<ChannelThumbnail xsmall uri={uri} />
|
<ChannelThumbnail xsmall uri={uri} />
|
||||||
|
|
||||||
<div className="textareaSuggestion__label">
|
<div className="textarea-suggestion__label">
|
||||||
<span className="textareaSuggestion__title">{(claim.value && claim.value.title) || value}</span>
|
<span className="textarea-suggestion__title">{claimTitle || value}</span>
|
||||||
<span className="textareaSuggestion__value">{value}</span>
|
<span className="textarea-suggestion__value">{value}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { doSetMentionSearchResults } from 'redux/actions/search';
|
||||||
import { makeSelectWinningUriForQuery } from 'redux/selectors/search';
|
import { makeSelectWinningUriForQuery } from 'redux/selectors/search';
|
||||||
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 { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import TextareaWithSuggestions from './view';
|
import TextareaWithSuggestions from './view';
|
||||||
|
|
||||||
|
@ -32,13 +31,12 @@ const select = (state, props) => {
|
||||||
commentorUris,
|
commentorUris,
|
||||||
hasNewResolvedResults,
|
hasNewResolvedResults,
|
||||||
searchQuery: query,
|
searchQuery: query,
|
||||||
showMature: selectShowMatureContent(state),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = {
|
||||||
doResolveUris: (uris) => dispatch(doResolveUris(uris, true)),
|
doResolveUris,
|
||||||
doSetMentionSearchResults: (query, uris) => dispatch(doSetMentionSearchResults(query, uris)),
|
doSetMentionSearchResults,
|
||||||
});
|
};
|
||||||
|
|
||||||
export default withRouter(connect(select, perform)(TextareaWithSuggestions));
|
export default withRouter(connect(select, perform)(TextareaWithSuggestions));
|
||||||
|
|
33
ui/component/textareaWithSuggestions/render-group.jsx
Normal file
33
ui/component/textareaWithSuggestions/render-group.jsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// @flow
|
||||||
|
import LbcSymbol from 'component/common/lbc-symbol';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
groupName: string,
|
||||||
|
suggestionTerm?: ?string,
|
||||||
|
searchQuery?: string,
|
||||||
|
children: any,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextareaSuggestionsGroup = (props: Props) => {
|
||||||
|
const { groupName, suggestionTerm, searchQuery, children } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={groupName} className="textarea-suggestions__group">
|
||||||
|
<label className="textarea-suggestions__group-label">
|
||||||
|
{groupName === 'Top' ? (
|
||||||
|
<LbcSymbol prefix={__('Winning Search for %matching_term%', { matching_term: searchQuery })} />
|
||||||
|
) : suggestionTerm && suggestionTerm.length > 1 ? (
|
||||||
|
__('%group_name% matching %matching_term%', { group_name: groupName, matching_term: suggestionTerm })
|
||||||
|
) : (
|
||||||
|
groupName
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
<hr className="textarea-suggestions__separator" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TextareaSuggestionsGroup;
|
51
ui/component/textareaWithSuggestions/render-input.jsx
Normal file
51
ui/component/textareaWithSuggestions/render-input.jsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// @flow
|
||||||
|
import { useIsMobile } from 'effects/use-screensize';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import React from 'react';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import Zoom from '@mui/material/Zoom';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
params: any,
|
||||||
|
messageValue: string,
|
||||||
|
inputDefaultProps: any,
|
||||||
|
inputRef: any,
|
||||||
|
handleEmojis: () => any,
|
||||||
|
handleTip: (isLBC: boolean) => void,
|
||||||
|
handleSubmit: () => any,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextareaSuggestionsInput = (props: Props) => {
|
||||||
|
const { params, messageValue, inputRef, inputDefaultProps, handleEmojis, handleTip, handleSubmit } = props;
|
||||||
|
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
const { InputProps, disabled, fullWidth, id, inputProps: autocompleteInputProps } = params;
|
||||||
|
const inputProps = { ...autocompleteInputProps, ...inputDefaultProps };
|
||||||
|
const autocompleteProps = { InputProps, disabled, fullWidth, id, inputProps };
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
InputProps.startAdornment = <Button icon={ICONS.STICKER} onClick={handleEmojis} />;
|
||||||
|
InputProps.endAdornment = (
|
||||||
|
<>
|
||||||
|
<Button icon={ICONS.LBC} onClick={() => handleTip(true)} />
|
||||||
|
<Button icon={ICONS.FINANCE} onClick={() => handleTip(false)} />
|
||||||
|
|
||||||
|
<Zoom in={messageValue && messageValue.length > 0} mountOnEnter unmountOnExit>
|
||||||
|
<div>
|
||||||
|
<Button button="primary" icon={ICONS.SUBMIT} iconColor="red" onClick={() => handleSubmit()} />
|
||||||
|
</div>
|
||||||
|
</Zoom>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField inputRef={inputRef} variant="outlined" multiline minRows={1} select={false} {...autocompleteProps} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TextField inputRef={inputRef} multiline select={false} {...autocompleteProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TextareaSuggestionsInput;
|
24
ui/component/textareaWithSuggestions/render-option.jsx
Normal file
24
ui/component/textareaWithSuggestions/render-option.jsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// @flow
|
||||||
|
import { EMOTES_48px as EMOTES } from 'constants/emotes';
|
||||||
|
import EMOJIS from 'emoji-dictionary';
|
||||||
|
import React from 'react';
|
||||||
|
import TextareaSuggestionsItem from 'component/textareaSuggestionsItem';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string,
|
||||||
|
isEmote?: boolean,
|
||||||
|
optionProps: any,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextareaSuggestionsOption = (props: Props) => {
|
||||||
|
const { label, isEmote, optionProps } = props;
|
||||||
|
|
||||||
|
const emoteFound = isEmote && EMOTES.find(({ name }) => name === label);
|
||||||
|
const emoteValue = emoteFound ? { name: label, url: emoteFound.url } : undefined;
|
||||||
|
const emojiFound = isEmote && EMOJIS.getUnicode(label);
|
||||||
|
const emojiValue = emojiFound ? { name: label, unicode: emojiFound } : undefined;
|
||||||
|
|
||||||
|
return <TextareaSuggestionsItem key={label} uri={label} emote={emoteValue || emojiValue} {...optionProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TextareaSuggestionsOption;
|
|
@ -2,22 +2,18 @@
|
||||||
import { EMOTES_48px as EMOTES } from 'constants/emotes';
|
import { EMOTES_48px as EMOTES } from 'constants/emotes';
|
||||||
import { matchSorter } from 'match-sorter';
|
import { matchSorter } from 'match-sorter';
|
||||||
import { SEARCH_OPTIONS } from 'constants/search';
|
import { SEARCH_OPTIONS } from 'constants/search';
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
import * as KEYCODES from 'constants/keycodes';
|
import * as KEYCODES from 'constants/keycodes';
|
||||||
import Autocomplete from '@mui/material/Autocomplete';
|
import Autocomplete from '@mui/material/Autocomplete';
|
||||||
import BusyIndicator from 'component/common/busy-indicator';
|
import BusyIndicator from 'component/common/busy-indicator';
|
||||||
import EMOJIS from 'emoji-dictionary';
|
import EMOJIS from 'emoji-dictionary';
|
||||||
import LbcSymbol from 'component/common/lbc-symbol';
|
|
||||||
import Popper from '@mui/material/Popper';
|
import Popper from '@mui/material/Popper';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import replaceAll from 'core-js-pure/features/string/replace-all';
|
|
||||||
import TextareaSuggestionsItem from 'component/textareaSuggestionsItem';
|
|
||||||
import TextField from '@mui/material/TextField';
|
|
||||||
import useLighthouse from 'effects/use-lighthouse';
|
import useLighthouse from 'effects/use-lighthouse';
|
||||||
import useThrottle from 'effects/use-throttle';
|
import useThrottle from 'effects/use-throttle';
|
||||||
import { parseURI } from 'util/lbryURI';
|
import { parseURI } from 'util/lbryURI';
|
||||||
import Button from 'component/button';
|
import TextareaSuggestionsOption from './render-option';
|
||||||
import { useIsMobile } from 'effects/use-screensize';
|
import TextareaSuggestionsInput from './render-input';
|
||||||
|
import TextareaSuggestionsGroup from './render-group';
|
||||||
|
|
||||||
const SUGGESTION_REGEX = new RegExp(
|
const SUGGESTION_REGEX = new RegExp(
|
||||||
'((?:^| |\n)@[^\\s=&#$@%?:;/\\"<>%{}|^~[]*(?::[\\w]+)?)|((?:^| |\n):[\\w+-]*:?)',
|
'((?:^| |\n)@[^\\s=&#$@%?:;/\\"<>%{}|^~[]*(?::[\\w]+)?)|((?:^| |\n):[\\w+-]*:?)',
|
||||||
|
@ -57,12 +53,11 @@ type Props = {
|
||||||
maxLength?: number,
|
maxLength?: number,
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
searchQuery?: string,
|
searchQuery?: string,
|
||||||
showMature: boolean,
|
|
||||||
type?: string,
|
type?: string,
|
||||||
uri?: string,
|
uri?: string,
|
||||||
value: any,
|
value: any,
|
||||||
doResolveUris: (Array<string>) => void,
|
doResolveUris: (uris: Array<string>, cache: boolean) => void,
|
||||||
doSetMentionSearchResults: (string, Array<string>) => void,
|
doSetMentionSearchResults: (query: string, uris: Array<string>) => void,
|
||||||
onBlur: (any) => any,
|
onBlur: (any) => any,
|
||||||
onChange: (any) => any,
|
onChange: (any) => any,
|
||||||
onFocus: (any) => any,
|
onFocus: (any) => any,
|
||||||
|
@ -88,7 +83,6 @@ export default function TextareaWithSuggestions(props: Props) {
|
||||||
maxLength,
|
maxLength,
|
||||||
placeholder,
|
placeholder,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
showMature,
|
|
||||||
type,
|
type,
|
||||||
value: messageValue,
|
value: messageValue,
|
||||||
doResolveUris,
|
doResolveUris,
|
||||||
|
@ -101,18 +95,15 @@ export default function TextareaWithSuggestions(props: Props) {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const inputDefaultProps = { className, placeholder, maxLength, type, disabled };
|
const inputDefaultProps = { className, placeholder, maxLength, type, disabled };
|
||||||
|
|
||||||
const [suggestionValue, setSuggestionValue] = React.useState(undefined);
|
const [suggestionValue, setSuggestionValue] = React.useState(undefined);
|
||||||
const [highlightedSuggestion, setHighlightedSuggestion] = React.useState('');
|
const [highlightedSuggestion, setHighlightedSuggestion] = React.useState('');
|
||||||
const [shouldClose, setClose] = React.useState();
|
const [shouldClose, setClose] = React.useState();
|
||||||
const [debouncedTerm, setDebouncedTerm] = React.useState('');
|
const [debouncedTerm, setDebouncedTerm] = React.useState('');
|
||||||
// const [mostSupported, setMostSupported] = React.useState('');
|
|
||||||
|
|
||||||
const suggestionTerm = suggestionValue && suggestionValue.term;
|
const suggestionTerm = suggestionValue && suggestionValue.term;
|
||||||
const isEmote = suggestionValue && suggestionValue.isEmote;
|
const isEmote = Boolean(suggestionValue && suggestionValue.isEmote);
|
||||||
const isMention = suggestionValue && !suggestionValue.isEmote;
|
const isMention = suggestionValue && !suggestionValue.isEmote;
|
||||||
|
|
||||||
let invalidTerm = suggestionTerm && isMention && suggestionTerm.charAt(1) === ':';
|
let invalidTerm = suggestionTerm && isMention && suggestionTerm.charAt(1) === ':';
|
||||||
|
@ -125,7 +116,7 @@ export default function TextareaWithSuggestions(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const additionalOptions = { isBackgroundSearch: false, [SEARCH_OPTIONS.CLAIM_TYPE]: SEARCH_OPTIONS.INCLUDE_CHANNELS };
|
const additionalOptions = { isBackgroundSearch: false, [SEARCH_OPTIONS.CLAIM_TYPE]: SEARCH_OPTIONS.INCLUDE_CHANNELS };
|
||||||
const { results, loading } = useLighthouse(debouncedTerm, showMature, SEARCH_SIZE, additionalOptions, 0);
|
const { results, loading } = useLighthouse(debouncedTerm, false, SEARCH_SIZE, additionalOptions, 0);
|
||||||
const stringifiedResults = JSON.stringify(results);
|
const stringifiedResults = JSON.stringify(results);
|
||||||
|
|
||||||
const hasMinLength = suggestionTerm && isMention && suggestionTerm.length >= LIGHTHOUSE_MIN_CHARACTERS;
|
const hasMinLength = suggestionTerm && isMention && suggestionTerm.length >= LIGHTHOUSE_MIN_CHARACTERS;
|
||||||
|
@ -180,7 +171,7 @@ export default function TextareaWithSuggestions(props: Props) {
|
||||||
let emoteLabel;
|
let emoteLabel;
|
||||||
if (isEmote) {
|
if (isEmote) {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
emoteLabel = `:${replaceAll(option, ':', '')}:`;
|
emoteLabel = `:${option.replace(/:/g, '')}:`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -316,14 +307,14 @@ export default function TextareaWithSuggestions(props: Props) {
|
||||||
|
|
||||||
const arrayResults = JSON.parse(stringifiedResults);
|
const arrayResults = JSON.parse(stringifiedResults);
|
||||||
if (debouncedTerm && arrayResults && arrayResults.length > 0) {
|
if (debouncedTerm && arrayResults && arrayResults.length > 0) {
|
||||||
doResolveUris([debouncedTerm, ...arrayResults]);
|
doResolveUris([debouncedTerm, ...arrayResults], true);
|
||||||
doSetMentionSearchResults(debouncedTerm, arrayResults);
|
doSetMentionSearchResults(debouncedTerm, arrayResults);
|
||||||
}
|
}
|
||||||
}, [debouncedTerm, doResolveUris, doSetMentionSearchResults, stringifiedResults, suggestionTerm]);
|
}, [debouncedTerm, doResolveUris, doSetMentionSearchResults, stringifiedResults, suggestionTerm]);
|
||||||
|
|
||||||
// Only resolve commentors on Livestreams when first trying to mention/looking for it
|
// Only resolve commentors on Livestreams when first trying to mention/looking for it
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isLivestream && commentorUris && suggestionTerm) doResolveUris(commentorUris);
|
if (isLivestream && commentorUris && suggestionTerm) doResolveUris(commentorUris, true);
|
||||||
}, [commentorUris, doResolveUris, isLivestream, suggestionTerm]);
|
}, [commentorUris, doResolveUris, isLivestream, suggestionTerm]);
|
||||||
|
|
||||||
// Allow selecting with TAB key
|
// Allow selecting with TAB key
|
||||||
|
@ -371,58 +362,6 @@ export default function TextareaWithSuggestions(props: Props) {
|
||||||
/** Render **/
|
/** Render **/
|
||||||
/** ------ **/
|
/** ------ **/
|
||||||
|
|
||||||
const renderGroup = (groupName: string, children: any) => (
|
|
||||||
<div key={groupName} className="textareaSuggestions__group">
|
|
||||||
<label className="textareaSuggestions__label">
|
|
||||||
{groupName === 'Top' ? (
|
|
||||||
<LbcSymbol prefix={__('Winning Search for %matching_term%', { matching_term: searchQuery })} />
|
|
||||||
) : suggestionTerm && suggestionTerm.length > 1 ? (
|
|
||||||
__('%group_name% matching %matching_term%', { group_name: groupName, matching_term: suggestionTerm })
|
|
||||||
) : (
|
|
||||||
groupName
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
{children}
|
|
||||||
<hr className="textareaSuggestions__topSeparator" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderInput = (params: any) => {
|
|
||||||
const { InputProps, disabled, fullWidth, id, inputProps: autocompleteInputProps } = params;
|
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
InputProps.startAdornment = <Button icon={ICONS.STICKER} onClick={handleEmojis} />;
|
|
||||||
InputProps.endAdornment = (
|
|
||||||
<>
|
|
||||||
<Button icon={ICONS.LBC} onClick={() => handleTip(true)} />
|
|
||||||
<Button icon={ICONS.FINANCE} onClick={() => handleTip(false)} />
|
|
||||||
|
|
||||||
{messageValue && messageValue.length > 0 && (
|
|
||||||
<Button button="primary" icon={ICONS.SUBMIT} iconColor="red" onClick={() => handleSubmit()} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputProps = { ...autocompleteInputProps, ...inputDefaultProps };
|
|
||||||
const autocompleteProps = { InputProps, disabled, fullWidth, id, inputProps };
|
|
||||||
|
|
||||||
return !isMobile ? (
|
|
||||||
<TextField inputRef={inputRef} multiline select={false} {...autocompleteProps} />
|
|
||||||
) : (
|
|
||||||
<TextField inputRef={inputRef} variant="outlined" multiline minRows={1} select={false} {...autocompleteProps} />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderOption = (optionProps: any, label: string) => {
|
|
||||||
const emoteFound = isEmote && EMOTES.find(({ name }) => name === label);
|
|
||||||
const emoteValue = emoteFound ? { name: label, url: emoteFound.url } : undefined;
|
|
||||||
const emojiFound = isEmote && EMOJIS.getUnicode(label);
|
|
||||||
const emojiValue = emojiFound ? { name: label, unicode: emojiFound } : undefined;
|
|
||||||
|
|
||||||
return <TextareaSuggestionsItem key={label} uri={label} emote={emoteValue || emojiValue} {...optionProps} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
PopperComponent={AutocompletePopper}
|
PopperComponent={AutocompletePopper}
|
||||||
|
@ -450,16 +389,30 @@ export default function TextareaWithSuggestions(props: Props) {
|
||||||
or else it will be displayed all the time as empty (no options) */
|
or else it will be displayed all the time as empty (no options) */
|
||||||
open={!!suggestionTerm && !shouldClose}
|
open={!!suggestionTerm && !shouldClose}
|
||||||
options={allOptionsGrouped}
|
options={allOptionsGrouped}
|
||||||
renderGroup={({ group, children }) => renderGroup(group, children)}
|
renderGroup={({ group, children }) => (
|
||||||
renderInput={(params) => renderInput(params)}
|
<TextareaSuggestionsGroup groupName={group} suggestionTerm={suggestionTerm} searchQuery={searchQuery}>
|
||||||
renderOption={(optionProps, option) => renderOption(optionProps, option.label)}
|
{children}
|
||||||
|
</TextareaSuggestionsGroup>
|
||||||
|
)}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextareaSuggestionsInput
|
||||||
|
params={params}
|
||||||
|
messageValue={messageValue}
|
||||||
|
inputRef={inputRef}
|
||||||
|
inputDefaultProps={inputDefaultProps}
|
||||||
|
handleEmojis={handleEmojis}
|
||||||
|
handleTip={handleTip}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderOption={(optionProps, option) => (
|
||||||
|
<TextareaSuggestionsOption label={option.label} isEmote={isEmote} optionProps={optionProps} />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AutocompletePopper(props: any) {
|
const AutocompletePopper = (props: any) => <Popper {...props} placement="top" />;
|
||||||
return <Popper {...props} placement="top" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function useSuggestionMatch(term: string, list: Array<string>) {
|
function useSuggestionMatch(term: string, list: Array<string>) {
|
||||||
const throttledTerm = useThrottle(term);
|
const throttledTerm = useThrottle(term);
|
||||||
|
|
|
@ -23,6 +23,15 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
.commentCreate + .empty__wrap {
|
||||||
|
p {
|
||||||
|
font-size: var(--font-small);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.commentCreate--reply {
|
.commentCreate--reply {
|
||||||
margin-top: var(--spacing-m);
|
margin-top: var(--spacing-m);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -41,7 +50,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate__labelWrapper {
|
.comment-create__label-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
@ -49,14 +58,18 @@ $thumbnailWidthSmall: 1rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.commentCreate__label {
|
.comment-create__label {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin-right: var(--spacing-xs);
|
margin-right: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
fieldset-section {
|
||||||
|
max-width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
fieldset-section {
|
fieldset-section {
|
||||||
max-width: 10rem;
|
font-size: var(--font-xxsmall);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -480,6 +480,12 @@ fieldset-group {
|
||||||
@media (min-width: $breakpoint-small) {
|
@media (min-width: $breakpoint-small) {
|
||||||
column-count: 2;
|
column-count: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
span {
|
||||||
|
font-size: var(--font-xxsmall);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field__quick-action {
|
.form-field__quick-action {
|
||||||
|
|
|
@ -25,9 +25,19 @@
|
||||||
font-size: var(--font-xsmall) !important;
|
font-size: var(--font-xsmall) !important;
|
||||||
flex-wrap: nowrap !important;
|
flex-wrap: nowrap !important;
|
||||||
color: var(--color-text) !important;
|
color: var(--color-text) !important;
|
||||||
|
padding: 0px 9px !important;
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
border: none;
|
border: none;
|
||||||
|
margin: 9px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:not(:first-of-type):not(:last-of-type) {
|
||||||
|
margin: 0px var(--spacing-xxs);
|
||||||
|
}
|
||||||
|
|
||||||
|
button + div {
|
||||||
|
margin-left: var(--spacing-xxs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--primary {
|
.button--primary {
|
||||||
|
@ -52,12 +62,12 @@
|
||||||
box-shadow: var(--card-box-shadow);
|
box-shadow: var(--card-box-shadow);
|
||||||
color: var(--color-text) !important;
|
color: var(--color-text) !important;
|
||||||
|
|
||||||
.textareaSuggestions__group {
|
.textarea-suggestions__group {
|
||||||
&:last-child hr {
|
&:last-child hr {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textareaSuggestions__label {
|
.textarea-suggestions__group-label {
|
||||||
@extend .wunderbar__label;
|
@extend .wunderbar__label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,23 +107,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.textareaSuggestion__label {
|
.textarea-suggestion__label {
|
||||||
@extend .wunderbar__suggestion-label;
|
@extend .wunderbar__suggestion-label;
|
||||||
margin-left: var(--spacing-m);
|
margin-left: var(--spacing-m);
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.textareaSuggestion__title {
|
.textarea-suggestion__title {
|
||||||
@extend .wunderbar__suggestion-title;
|
@extend .wunderbar__suggestion-title;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textareaSuggestion__value {
|
.textarea-suggestion__value {
|
||||||
@extend .wunderbar__suggestion-name;
|
@extend .wunderbar__suggestion-name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.textareaSuggestions__topSeparator {
|
.textarea-suggestions__separator {
|
||||||
@extend .wunderbar__top-separator;
|
@extend .wunderbar__top-separator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue