More comment create and textarea improvements

This commit is contained in:
Rafael 2022-02-02 09:49:02 -03:00 committed by Thomas Zarebczan
parent b3ed0027ff
commit eef6691557
16 changed files with 250 additions and 135 deletions

View file

@ -484,8 +484,8 @@ export function CommentCreate(props: Props) {
disabled={isFetchingChannels || disableInput}
isLivestream={isLivestream}
label={
<div className="commentCreate__labelWrapper">
<span className="commentCreate__label">
<div className="comment-create__label-wrapper">
<span className="comment-create__label">
{(isReply ? __('Replying as') : isLivestream ? __('Chat as') : __('Comment as')) + ' '}
</span>
<SelectChannel tiny />

View file

@ -253,7 +253,7 @@ function CommentMenuList(props: Props) {
</MenuItem>
)}
{isPinned && (
{isPinned && isLiveComment && (
<MenuItem className="comment__menu-option menu__link" onSelect={handleDismissPin}>
<Icon aria-hidden icon={ICONS.DISMISS_ALL} />
{__('Dismiss Pin')}

View file

@ -272,8 +272,8 @@ export class FormField extends React.PureComponent<Props> {
</React.Suspense>
)}
<div className="form-field__textarea-info">
{!noEmojis && openEmoteMenu && (
{!noEmojis && openEmoteMenu && (
<div className="form-field__textarea-info">
<Button
type="alt"
className="button--file-action"
@ -282,8 +282,8 @@ export class FormField extends React.PureComponent<Props> {
icon={ICONS.EMOJI}
iconSize={20}
/>
)}
</div>
</div>
)}
</fieldset-section>
);
default:

View file

@ -297,7 +297,6 @@ export default function LivestreamChatLayout(props: Props) {
key={pinnedComment.comment_id}
uri={uri}
pushMention={setMention}
handleDismissPin={() => setShowPinned(false)}
/>
<Button

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import LivestreamLayout from './view';
import SwipeableDrawer from './view';
import { selectTheme } from 'redux/selectors/settings';
import { selectMobilePlayerDimensions } from 'redux/selectors/app';
@ -8,4 +8,4 @@ const select = (state) => ({
mobilePlayerDimensions: selectMobilePlayerDimensions(state),
});
export default connect(select)(LivestreamLayout);
export default connect(select)(SwipeableDrawer);

View file

@ -42,29 +42,18 @@ export default function SwipeableDrawer(props: Props) {
}
}, [coverHeight, mobilePlayerDimensions, open]);
const DrawerGlobalStyles = () => (
<Global
styles={{
'.main-wrapper__inner--filepage': {
overflow: open ? 'hidden' : 'unset',
maxHeight: open ? '100vh' : 'unset',
},
'.MuiDrawer-root': {
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)`,
},
}}
/>
);
// Reset scroll position when opening: avoid broken position where
// the drawer is lower than the video
React.useEffect(() => {
if (open) {
const htmlEl = document.querySelector('html');
if (htmlEl) htmlEl.scrollTop = 0;
}
}, [open]);
return (
<>
<DrawerGlobalStyles />
<DrawerGlobalStyles open={open} videoHeight={videoHeight} />
<MUIDrawer
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 = {
theme: string,
};

View file

@ -1,9 +1,18 @@
import { connect } from 'react-redux';
import { selectClaimForUri } from 'redux/selectors/claims';
import TextareaSuggestionsItem from './view';
import { formatLbryChannelName } from 'util/url';
import { getClaimTitle } from 'util/claim';
const select = (state, props) => ({
claim: props.uri && selectClaimForUri(state, props.uri),
});
const select = (state, props) => {
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);

View file

@ -3,13 +3,14 @@ import ChannelThumbnail from 'component/channelThumbnail';
import React from 'react';
type Props = {
claim?: Claim,
claimLabel?: string,
claimTitle?: string,
emote?: any,
uri?: string,
};
export default function TextareaSuggestionsItem(props: Props) {
const { claim, emote, uri, ...autocompleteProps } = props;
const { claimLabel, claimTitle, emote, uri, ...autocompleteProps } = props;
if (emote) {
const { name: value, url, unicode } = emote;
@ -18,8 +19,8 @@ export default function TextareaSuggestionsItem(props: Props) {
<div {...autocompleteProps} dispatch={undefined}>
{unicode ? <div className="emote">{unicode}</div> : <img className="emote" src={url} />}
<div className="textareaSuggestion__label">
<span className="textareaSuggestion__title textareaSuggestion__value textareaSuggestion__value--emote">
<div className="textarea-suggestion__label">
<span className="textarea-suggestion__title textarea-suggestion__value textarea-suggestion__value--emote">
{value}
</span>
</div>
@ -27,16 +28,16 @@ export default function TextareaSuggestionsItem(props: Props) {
);
}
if (claim) {
const value = claim.canonical_url.replace('lbry://', '').replace('#', ':');
if (claimLabel) {
const value = claimLabel;
return (
<div {...autocompleteProps} dispatch={undefined}>
<ChannelThumbnail xsmall uri={uri} />
<div className="textareaSuggestion__label">
<span className="textareaSuggestion__title">{(claim.value && claim.value.title) || value}</span>
<span className="textareaSuggestion__value">{value}</span>
<div className="textarea-suggestion__label">
<span className="textarea-suggestion__title">{claimTitle || value}</span>
<span className="textarea-suggestion__value">{value}</span>
</div>
</div>
);

View file

@ -4,7 +4,6 @@ import { doSetMentionSearchResults } from 'redux/actions/search';
import { makeSelectWinningUriForQuery } from 'redux/selectors/search';
import { MAX_LIVESTREAM_COMMENTS } from 'constants/livestream';
import { selectChannelMentionData } from 'redux/selectors/comments';
import { selectShowMatureContent } from 'redux/selectors/settings';
import { withRouter } from 'react-router';
import TextareaWithSuggestions from './view';
@ -32,13 +31,12 @@ const select = (state, props) => {
commentorUris,
hasNewResolvedResults,
searchQuery: query,
showMature: selectShowMatureContent(state),
};
};
const perform = (dispatch) => ({
doResolveUris: (uris) => dispatch(doResolveUris(uris, true)),
doSetMentionSearchResults: (query, uris) => dispatch(doSetMentionSearchResults(query, uris)),
});
const perform = {
doResolveUris,
doSetMentionSearchResults,
};
export default withRouter(connect(select, perform)(TextareaWithSuggestions));

View 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;

View 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;

View 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;

View file

@ -2,22 +2,18 @@
import { EMOTES_48px as EMOTES } from 'constants/emotes';
import { matchSorter } from 'match-sorter';
import { SEARCH_OPTIONS } from 'constants/search';
import * as ICONS from 'constants/icons';
import * as KEYCODES from 'constants/keycodes';
import Autocomplete from '@mui/material/Autocomplete';
import BusyIndicator from 'component/common/busy-indicator';
import EMOJIS from 'emoji-dictionary';
import LbcSymbol from 'component/common/lbc-symbol';
import Popper from '@mui/material/Popper';
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 useThrottle from 'effects/use-throttle';
import { parseURI } from 'util/lbryURI';
import Button from 'component/button';
import { useIsMobile } from 'effects/use-screensize';
import TextareaSuggestionsOption from './render-option';
import TextareaSuggestionsInput from './render-input';
import TextareaSuggestionsGroup from './render-group';
const SUGGESTION_REGEX = new RegExp(
'((?:^| |\n)@[^\\s=&#$@%?:;/\\"<>%{}|^~[]*(?::[\\w]+)?)|((?:^| |\n):[\\w+-]*:?)',
@ -57,12 +53,11 @@ type Props = {
maxLength?: number,
placeholder?: string,
searchQuery?: string,
showMature: boolean,
type?: string,
uri?: string,
value: any,
doResolveUris: (Array<string>) => void,
doSetMentionSearchResults: (string, Array<string>) => void,
doResolveUris: (uris: Array<string>, cache: boolean) => void,
doSetMentionSearchResults: (query: string, uris: Array<string>) => void,
onBlur: (any) => any,
onChange: (any) => any,
onFocus: (any) => any,
@ -88,7 +83,6 @@ export default function TextareaWithSuggestions(props: Props) {
maxLength,
placeholder,
searchQuery,
showMature,
type,
value: messageValue,
doResolveUris,
@ -101,18 +95,15 @@ export default function TextareaWithSuggestions(props: Props) {
handleSubmit,
} = props;
const isMobile = useIsMobile();
const inputDefaultProps = { className, placeholder, maxLength, type, disabled };
const [suggestionValue, setSuggestionValue] = React.useState(undefined);
const [highlightedSuggestion, setHighlightedSuggestion] = React.useState('');
const [shouldClose, setClose] = React.useState();
const [debouncedTerm, setDebouncedTerm] = React.useState('');
// const [mostSupported, setMostSupported] = React.useState('');
const suggestionTerm = suggestionValue && suggestionValue.term;
const isEmote = suggestionValue && suggestionValue.isEmote;
const isEmote = Boolean(suggestionValue && suggestionValue.isEmote);
const isMention = suggestionValue && !suggestionValue.isEmote;
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 { 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 hasMinLength = suggestionTerm && isMention && suggestionTerm.length >= LIGHTHOUSE_MIN_CHARACTERS;
@ -180,7 +171,7 @@ export default function TextareaWithSuggestions(props: Props) {
let emoteLabel;
if (isEmote) {
// $FlowFixMe
emoteLabel = `:${replaceAll(option, ':', '')}:`;
emoteLabel = `:${option.replace(/:/g, '')}:`;
}
return {
@ -316,14 +307,14 @@ export default function TextareaWithSuggestions(props: Props) {
const arrayResults = JSON.parse(stringifiedResults);
if (debouncedTerm && arrayResults && arrayResults.length > 0) {
doResolveUris([debouncedTerm, ...arrayResults]);
doResolveUris([debouncedTerm, ...arrayResults], true);
doSetMentionSearchResults(debouncedTerm, arrayResults);
}
}, [debouncedTerm, doResolveUris, doSetMentionSearchResults, stringifiedResults, suggestionTerm]);
// Only resolve commentors on Livestreams when first trying to mention/looking for it
React.useEffect(() => {
if (isLivestream && commentorUris && suggestionTerm) doResolveUris(commentorUris);
if (isLivestream && commentorUris && suggestionTerm) doResolveUris(commentorUris, true);
}, [commentorUris, doResolveUris, isLivestream, suggestionTerm]);
// Allow selecting with TAB key
@ -371,58 +362,6 @@ export default function TextareaWithSuggestions(props: Props) {
/** 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 (
<Autocomplete
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) */
open={!!suggestionTerm && !shouldClose}
options={allOptionsGrouped}
renderGroup={({ group, children }) => renderGroup(group, children)}
renderInput={(params) => renderInput(params)}
renderOption={(optionProps, option) => renderOption(optionProps, option.label)}
renderGroup={({ group, children }) => (
<TextareaSuggestionsGroup groupName={group} suggestionTerm={suggestionTerm} searchQuery={searchQuery}>
{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) {
return <Popper {...props} placement="top" />;
}
const AutocompletePopper = (props: any) => <Popper {...props} placement="top" />;
function useSuggestionMatch(term: string, list: Array<string>) {
const throttledTerm = useThrottle(term);

View file

@ -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 {
margin-top: var(--spacing-m);
position: relative;
@ -41,7 +50,7 @@ $thumbnailWidthSmall: 1rem;
padding-bottom: 0;
}
.commentCreate__labelWrapper {
.comment-create__label-wrapper {
display: flex;
flex-direction: row;
justify-content: flex-start;
@ -49,14 +58,18 @@ $thumbnailWidthSmall: 1rem;
flex-wrap: wrap;
width: 100%;
.commentCreate__label {
.comment-create__label {
white-space: nowrap;
margin-right: var(--spacing-xs);
}
@media (min-width: $breakpoint-small) {
fieldset-section {
max-width: 10rem;
}
@media (max-width: $breakpoint-small) {
fieldset-section {
max-width: 10rem;
font-size: var(--font-xxsmall);
}
}
}

View file

@ -480,6 +480,12 @@ fieldset-group {
@media (min-width: $breakpoint-small) {
column-count: 2;
}
@media (max-width: $breakpoint-small) {
span {
font-size: var(--font-xxsmall);
}
}
}
.form-field__quick-action {

View file

@ -25,9 +25,19 @@
font-size: var(--font-xsmall) !important;
flex-wrap: nowrap !important;
color: var(--color-text) !important;
padding: 0px 9px !important;
textarea {
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 {
@ -52,12 +62,12 @@
box-shadow: var(--card-box-shadow);
color: var(--color-text) !important;
.textareaSuggestions__group {
.textarea-suggestions__group {
&:last-child hr {
display: none;
}
.textareaSuggestions__label {
.textarea-suggestions__group-label {
@extend .wunderbar__label;
}
@ -97,23 +107,23 @@
}
}
.textareaSuggestion__label {
.textarea-suggestion__label {
@extend .wunderbar__suggestion-label;
margin-left: var(--spacing-m);
display: block;
position: relative;
.textareaSuggestion__title {
.textarea-suggestion__title {
@extend .wunderbar__suggestion-title;
}
.textareaSuggestion__value {
.textarea-suggestion__value {
@extend .wunderbar__suggestion-name;
}
}
}
.textareaSuggestions__topSeparator {
.textarea-suggestions__separator {
@extend .wunderbar__top-separator;
}