lbry-desktop/ui/component/common/form-components/form-field.jsx
jessopb 0b41fc041a
Bringing in emotes, stickers, and refactors from ody (#7435)
* [New Feature] Comment Emotes (#125)

* Refactor form-field

* Create new Emote Menu

* Add Emotes

* Add Emote Selector and Emote Comment creation ability

* Fix and Split CSS

* [New Feature] Stickers (#131)

* Refactor filePrice

* Refactor Wallet Tip Components

* Add backend sticker support for comments

* Add stickers

* Refactor commentCreate

* Add Sticker Selector and sticker comment creation

* Add stickers display to comments and hyperchats

* Fix wrong checks for total Super Chats

* Stickers/emojis fall out / improvements (#220)

* Fix error logs

* Improve LBC sticker flow/clarity

* Show inline error if custom sticker amount below min

* Sort emojis alphabetically

* Improve loading of Images

* Improve quality and display of emojis and fix CSS

* Display both USD and LBC prices

* Default to LBC tip if creator can't receive USD

* Don't clear text-field after sticker is sent

* Refactor notification component

* Handle notifications

* Don't show profile pic on sticker livestream comments

* Change Sticker icon

* Fix wording and number rounding

* Fix blurring emojis

* Disable non functional emote buttons

* new Stickers! (#248)

* Add new stickers (#347)

* Fix cancel sending sticker (#447)

* Refactor scrollbar CSS for portal components outside of main

Refactor channelMention suggestions into new textareaSuggestions component

Install @mui/material packages

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

Add support for suggesting Emotes while typing ':'

Improve label to display matching term

Add back and improved support for searching while mentioning

Add support for suggesting emojis

Fix non concatenated strings

Add key to groups and options

Fix dispatch props

Fix Popper positioning to be consistent

Fix and Improve searching

Add back support for Winning Uri

Filter default emojis with the same name as emotes

Remove unused topSuggestion component

Fix text color on darkmode

Fix livestream updating state from both websocket and reducer and causing double of the same comments to appear

Fix blur and focus commentCreate events

Fix no name after @ error

* desktop tweaks

Co-authored-by: saltrafael <76502841+saltrafael@users.noreply.github.com>
Co-authored-by: Thomas Zarebczan <tzarebczan@users.noreply.github.com>
Co-authored-by: Rafael <rafael.saes@odysee.com>
2022-01-24 11:07:09 -05:00

309 lines
10 KiB
JavaScript

// @flow
import 'easymde/dist/easymde.min.css';
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
import * as ICONS from 'constants/icons';
import Button from 'component/button';
import MarkdownPreview from 'component/common/markdown-preview';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import SimpleMDE from 'react-simplemde-editor';
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
import type { ElementRef, Node } from 'react';
type Props = {
affixClass?: string, // class applied to prefix/postfix label
autoFocus?: boolean,
blockWrap: boolean,
charCount?: number,
children?: React$Node,
defaultValue?: string | number,
disabled?: boolean,
error?: string | boolean,
helper?: string | React$Node,
hideSuggestions?: boolean,
inputButton?: React$Node,
isLivestream?: boolean,
label?: string | Node,
labelOnLeft: boolean,
max?: number,
min?: number,
name: string,
noEmojis?: boolean,
placeholder?: string | number,
postfix?: string,
prefix?: string,
quickActionLabel?: string,
range?: number,
readOnly?: boolean,
stretch?: boolean,
textAreaMaxLength?: number,
type?: string,
value?: string | number,
onChange?: (any) => any,
openEmoteMenu?: () => void,
quickActionHandler?: (any) => any,
render?: () => React$Node,
};
export class FormField extends React.PureComponent<Props> {
static defaultProps = { labelOnLeft: false, blockWrap: true };
input: { current: ElementRef<any> };
constructor(props: Props) {
super(props);
this.input = React.createRef();
}
componentDidMount() {
const { autoFocus } = this.props;
const input = this.input.current;
if (input && autoFocus) input.focus();
}
render() {
const {
affixClass,
autoFocus,
blockWrap,
charCount,
children,
error,
helper,
hideSuggestions,
inputButton,
isLivestream,
label,
labelOnLeft,
name,
noEmojis,
postfix,
prefix,
quickActionLabel,
stretch,
textAreaMaxLength,
type,
openEmoteMenu,
quickActionHandler,
render,
...inputProps
} = this.props;
const errorMessage = typeof error === 'object' ? error.message : error;
// Ideally, the character count should (and can) be appended to the
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
// to pass the current value to it's callback, nor query the current
// text length from the callback. So, we'll use our own widget.
const hasCharCount = charCount !== undefined && charCount >= 0;
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
);
const Wrapper = blockWrap
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
const quickAction =
quickActionLabel && quickActionHandler ? (
<div className="form-field__quick-action">
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} />
</div>
) : null;
const inputSimple = (type: string) => (
<>
<input id={name} type={type} {...inputProps} />
<label htmlFor={name}>{label}</label>
</>
);
const inputSelect = (selectClass: string) => (
<fieldset-section class={selectClass}>
{(label || errorMessage) && (
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
)}
<select id={name} {...inputProps}>
{children}
</select>
</fieldset-section>
);
const input = () => {
switch (type) {
case 'radio':
return <Wrapper>{inputSimple('radio')}</Wrapper>;
case 'checkbox':
return <div className="checkbox">{inputSimple('checkbox')}</div>;
case 'range':
return <div>{inputSimple('range')}</div>;
case 'select':
return inputSelect('');
case 'select-tiny':
return inputSelect('select--slim');
case 'markdown':
const handleEvents = { contextmenu: openEditorMenu };
const getInstance = (editor) => {
// SimpleMDE max char check
editor.codemirror.on('beforeChange', (instance, changes) => {
if (textAreaMaxLength && changes.update) {
var str = changes.text.join('\n');
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
if (delta <= 0) return;
delta = instance.getValue().length + delta - textAreaMaxLength;
if (delta > 0) {
str = str.substr(0, str.length - delta);
changes.update(changes.from, changes.to, str.split('\n'));
}
}
});
// "Create Link (Ctrl-K)": highlight URL instead of label:
editor.codemirror.on('changes', (instance, changes) => {
try {
// Grab the last change from the buffered list. I assume the
// buffered one ('changes', instead of 'change') is more efficient,
// and that "Create Link" will always end up last in the list.
const lastChange = changes[changes.length - 1];
if (lastChange.origin === '+input') {
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
const EASYMDE_URL_PLACEHOLDER = '(https://)';
// The URL placeholder is always placed last, so just look at the
// last text in the array to also cover the multi-line case:
const urlLineText = lastChange.text[lastChange.text.length - 1];
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
const from = lastChange.from;
const to = lastChange.to;
const isSelectionMultiline = lastChange.text.length > 1;
const baseIndex = isSelectionMultiline ? 0 : from.ch;
// Everything works fine for the [Ctrl-K] case, but for the
// [Button] case, this handler happens before the original
// code, thus our change got wiped out.
// Add a small delay to handle that case.
setTimeout(() => {
instance.setSelection(
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
);
}, 25);
}
}
} catch (e) {} // Do nothing (revert to original behavior)
});
};
return (
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
<fieldset-section>
<div className="form-field__two-column">
<div>
<label htmlFor={name}>{label}</label>
</div>
{quickAction}
</div>
<SimpleMDE
{...inputProps}
id={name}
type="textarea"
events={handleEvents}
getMdeInstance={getInstance}
options={{
spellChecker: true,
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
previewRender(plainText) {
const preview = <MarkdownPreview content={plainText} noDataStore />;
return ReactDOMServer.renderToString(preview);
},
}}
/>
{countInfo}
</fieldset-section>
</div>
);
case 'textarea':
return (
<fieldset-section>
{(label || quickAction) && (
<div className="form-field__two-column">
<label htmlFor={name}>{label}</label>
{quickAction}
</div>
)}
{hideSuggestions ? (
<textarea
type={type}
id={name}
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
ref={this.input}
{...inputProps}
/>
) : (
<TextareaWithSuggestions
type={type}
id={name}
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
inputRef={this.input}
isLivestream={isLivestream}
{...inputProps}
/>
)}
<div className="form-field__textarea-info">
{!noEmojis && openEmoteMenu && (
<Button
type="alt"
className="button--file-action"
title="Emotes"
onClick={openEmoteMenu}
icon={ICONS.EMOJI}
iconSize={20}
/>
)}
{countInfo}
</div>
</fieldset-section>
);
default:
const inputElement = <input type={type} id={name} {...inputProps} ref={this.input} />;
const inner = inputButton ? (
<input-submit>
{inputElement}
{inputButton}
</input-submit>
) : (
inputElement
);
return (
<fieldset-section>
{(label || errorMessage) && (
<label htmlFor={name}>
{errorMessage ? <span className="error__text">{errorMessage}</span> : label}
</label>
)}
{prefix && <label htmlFor={name}>{prefix}</label>}
{inner}
</fieldset-section>
);
}
};
return (
<>
{type && input()}
{helper && <div className="form-field__help">{helper}</div>}
</>
);
}
}
export default FormField;