From 5c4cfdd4d89671621a1b8273d981dccf6f8f9d75 Mon Sep 17 00:00:00 2001 From: saltrafael <76502841+saltrafael@users.noreply.github.com> Date: Wed, 27 Oct 2021 15:20:47 -0300 Subject: [PATCH] [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 --- static/app-strings.json | 80 ++ ui/component/commentCreate/emote-selector.jsx | 68 ++ ui/component/commentCreate/view.jsx | 21 +- .../common/form-components/form-field.jsx | 398 +++++----- ui/component/common/icon-custom.jsx | 8 + ui/component/common/markdown-preview.jsx | 56 +- ui/component/optimizedImage/view.jsx | 9 +- ui/constants/emotes.js | 96 +++ ui/constants/icons.js | 1 + ui/scss/component/_button.scss | 6 - ui/scss/component/_comment-create.scss | 82 ++ ui/scss/component/_comments.scss | 70 +- ui/scss/component/_emote-selector.scss | 40 + ui/scss/component/_form-field.scss | 8 - ui/util/remark-emote.js | 125 +++ ui/util/remark-lbry.js | 2 +- .../themes/odysee/component/_form-field.scss | 711 ++++++++++++++++++ 17 files changed, 1458 insertions(+), 323 deletions(-) create mode 100644 ui/component/commentCreate/emote-selector.jsx create mode 100644 ui/constants/emotes.js create mode 100644 ui/scss/component/_comment-create.scss create mode 100644 ui/scss/component/_emote-selector.scss create mode 100644 ui/util/remark-emote.js create mode 100644 web/scss/themes/odysee/component/_form-field.scss diff --git a/static/app-strings.json b/static/app-strings.json index 99b65f1ec..600ca762d 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -2239,5 +2239,85 @@ "A channel is required to repost on LBRY": "A channel is required to repost on LBRY", "Failed to view lbry://@gatogalactico#9/gato-galactico-e-as-estrelas-ninja-dos#1, please try again. If this problem persists, visit https://lbry.com/faq/support for support.": "Failed to view lbry://@gatogalactico#9/gato-galactico-e-as-estrelas-ninja-dos#1, please try again. If this problem persists, visit https://lbry.com/faq/support for support.", "Admin": "Admin", + ":ALIEN:": ":ALIEN:", + ":ANGRY_2:": ":ANGRY_2:", + ":ANGRY_3:": ":ANGRY_3:", + ":ANGRY_4:": ":ANGRY_4:", + ":ANGRY_1:": ":ANGRY_1:", + ":BLIND:": ":BLIND:", + ":BLOCK:": ":BLOCK:", + ":BOMB:": ":BOMB:", + ":BRAIN_CHIP:": ":BRAIN_CHIP:", + ":CONFIRM:": ":CONFIRM:", + ":CONFUSED_1:": ":CONFUSED_1:", + ":CONFUSED_2:": ":CONFUSED_2:", + ":COOKING_SOMETHING_NICE:": ":COOKING_SOMETHING_NICE:", + ":CRY_2:": ":CRY_2:", + ":CRY_3:": ":CRY_3:", + ":CRY_4:": ":CRY_4:", + ":CRY_5:": ":CRY_5:", + ":CRY_1:": ":CRY_1:", + ":SPACE_DOGE:": ":SPACE_DOGE:", + ":DONUT:": ":DONUT:", + ":EGGPLANT_WITH_CONDOM:": ":EGGPLANT_WITH_CONDOM:", + ":EGGPLANT:": ":EGGPLANT:", + ":FIRE_UP:": ":FIRE_UP:", + ":FLAT_EARTH:": ":FLAT_EARTH:", + ":FLYING_SAUCER:": ":FLYING_SAUCER:", + ":HEART_CHOPPER:": ":HEART_CHOPPER:", + ":HYPER_TROLL:": ":HYPER_TROLL:", + ":ICE_CREAM:": ":ICE_CREAM:", + ":IDK:": ":IDK:", + ":ILLUMINATI_1:": ":ILLUMINATI_1:", + ":ILLUMINATI_2:": ":ILLUMINATI_2:", + ":KISS_2:": ":KISS_2:", + ":KISS_1:": ":KISS_1:", + ":LASER_GUN:": ":LASER_GUN:", + ":LAUGHING_2:": ":LAUGHING_2:", + ":LAUGHING_1:": ":LAUGHING_1:", + ":LOLLIPOP:": ":LOLLIPOP:", + ":LOVE_2:": ":LOVE_2:", + ":LOVE_1:": ":LOVE_1:", + ":MONSTER:": ":MONSTER:", + ":MUSHROOM:": ":MUSHROOM:", + ":NAIL_IT:": ":NAIL_IT:", + ":NO:": ":NO:", + ":OUCH:": ":OUCH:", + ":PREACE:": ":PREACE:", + ":PIZZA:": ":PIZZA:", + ":RABBIT_HOLE:": ":RABBIT_HOLE:", + ":RAINBOW_PUKE_1:": ":RAINBOW_PUKE_1:", + ":RAINBOW_PUKE_2:": ":RAINBOW_PUKE_2:", + ":SPACE_RESITAS:": ":SPACE_RESITAS:", + ":ROCK:": ":ROCK:", + ":SAD:": ":SAD:", + ":SALTY:": ":SALTY:", + ":SCARY:": ":SCARY:", + ":SLEEP:": ":SLEEP:", + ":SLIME_DOWN:": ":SLIME_DOWN:", + ":SMELLY_SOCKS:": ":SMELLY_SOCKS:", + ":SMILE_2:": ":SMILE_2:", + ":SMILE_1:": ":SMILE_1:", + ":SPACE_CHAD:": ":SPACE_CHAD:", + ":SPACE_JULIAN:": ":SPACE_JULIAN:", + ":SPACE_TOM:": ":SPACE_TOM:", + ":SPACE_WOJAK_1:": ":SPACE_WOJAK_1:", + ":SPOCK:": ":SPOCK:", + ":STAR:": ":STAR:", + ":SUNNY_DAY:": ":SUNNY_DAY:", + ":SUPRISED:": ":SUPRISED:", + ":SWEET:": ":SWEET:", + ":THINKING_1:": ":THINKING_1:", + ":THINKING_2:": ":THINKING_2:", + ":THUMB_DOWN:": ":THUMB_DOWN:", + ":THUMB_UP_1:": ":THUMB_UP_1:", + ":THUMB_UP_2:": ":THUMB_UP_2:", + ":TINFOIL_HAT:": ":TINFOIL_HAT:", + ":TROLL_KING:": ":TROLL_KING:", + ":UFO:": ":UFO:", + ":WAITING:": ":WAITING:", + ":WHAT:": ":WHAT:", + ":WOODOO_DOLL:": ":WOODOO_DOLL:", + "Global Emotes": "Global Emotes", "--end--": "--end--" } diff --git a/ui/component/commentCreate/emote-selector.jsx b/ui/component/commentCreate/emote-selector.jsx new file mode 100644 index 000000000..1dc1a1bd6 --- /dev/null +++ b/ui/component/commentCreate/emote-selector.jsx @@ -0,0 +1,68 @@ +// @flow +import 'scss/component/_emote-selector.scss'; +import { EMOTES_24px as EMOTES } from 'constants/emotes'; +import * as ICONS from 'constants/icons'; +import Button from 'component/button'; +import EMOJIS from 'emoji-dictionary'; +import OptimizedImage from 'component/optimizedImage'; +import React from 'react'; + +const OLD_QUICK_EMOJIS = [ + EMOJIS.getUnicode('rocket'), + EMOJIS.getUnicode('jeans'), + EMOJIS.getUnicode('fire'), + EMOJIS.getUnicode('heart'), + EMOJIS.getUnicode('open_mouth'), +]; + +type Props = { commentValue: string, setCommentValue: (string) => void, closeSelector: () => void }; + +export default function EmoteSelector(props: Props) { + const { commentValue, setCommentValue, closeSelector } = props; + + function addEmoteToComment(emote: string) { + setCommentValue( + commentValue + (commentValue && commentValue.charAt(commentValue.length - 1) !== ' ' ? ` ${emote} ` : `${emote} `) + ); + } + + return ( +
+ + ); + })} +
+ + + + ); +} diff --git a/ui/component/commentCreate/view.jsx b/ui/component/commentCreate/view.jsx index f03b0ec66..216a15d79 100644 --- a/ui/component/commentCreate/view.jsx +++ b/ui/component/commentCreate/view.jsx @@ -1,5 +1,6 @@ // @flow import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field'; +import 'scss/component/_comment-create.scss'; import { FormField, Form } from 'component/common/form'; import { getChannelIdFromClaim } from 'util/claim'; import { Lbryio } from 'lbryinc'; @@ -12,6 +13,7 @@ import ChannelMentionSuggestions from 'component/channelMentionSuggestions'; import ChannelThumbnail from 'component/channelThumbnail'; import classnames from 'classnames'; import CreditAmount from 'component/common/credit-amount'; +import EmoteSelector from './emote-selector'; import Empty from 'component/common/empty'; import I18nMessage from 'component/i18nMessage'; import Icon from 'component/common/icon'; @@ -102,6 +104,7 @@ export function CommentCreate(props: Props) { const [deletedComment, setDeletedComment] = React.useState(false); const [pauseQuickSend, setPauseQuickSend] = React.useState(false); const [shouldDisableReviewButton, setShouldDisableReviewButton] = React.useState(); + const [showEmotes, setShowEmotes] = React.useState(false); const selectedMentionIndex = commentValue.indexOf('@', selectionIndex) === selectionIndex @@ -199,12 +202,6 @@ export function CommentCreate(props: Props) { window.removeEventListener('keydown', altEnterListener); } - function handleSubmit() { - if (activeChannelClaim && commentValue.length) { - handleCreateComment(); - } - } - function handleSupportComment() { if (!activeChannelClaim) { return; @@ -362,6 +359,7 @@ export function CommentCreate(props: Props) { * @param {string} [environment] Optional environment for Stripe (test|live) */ function handleCreateComment(txid, payment_intent_id, environment) { + setShowEmotes(false); setIsSubmitting(true); createComment(commentValue, claimId, parentId, txid, payment_intent_id, environment) @@ -494,13 +492,20 @@ export function CommentCreate(props: Props) { return (
+ {showEmotes && ( + setShowEmotes(false)} + /> + )} + {!advancedEditor && ( setAdvancedEditor(!advancedEditor)} + openEmoteMenu={() => setShowEmotes(!showEmotes)} onFocus={onTextareaFocus} onBlur={onTextareaBlur} placeholder={__('Say something about this...')} @@ -579,6 +585,7 @@ export function CommentCreate(props: Props) { ? __('Commenting...') : __('Comment --[button to submit something]--') } + onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()} /> )} {!supportDisabled && !claimIsMine && ( diff --git a/ui/component/common/form-components/form-field.jsx b/ui/component/common/form-components/form-field.jsx index 83e01f85d..3453fdbd8 100644 --- a/ui/component/common/form-components/form-field.jsx +++ b/ui/component/common/form-components/form-field.jsx @@ -1,33 +1,23 @@ // @flow -import type { ElementRef, Node } from 'react'; +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 MarkdownPreview from 'component/common/markdown-preview'; -import { openEditorMenu, stopContextMenu } from 'util/context-menu'; -import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field'; -import 'easymde/dist/easymde.min.css'; -import Button from 'component/button'; -import emoji from 'emoji-dictionary'; - -const QUICK_EMOJIS = [ - emoji.getUnicode('rocket'), - emoji.getUnicode('jeans'), - emoji.getUnicode('fire'), - emoji.getUnicode('heart'), - emoji.getUnicode('open_mouth'), -]; +import type { ElementRef, Node } from 'react'; type Props = { name: string, label?: string | Node, - render?: () => React$Node, prefix?: string, postfix?: string, error?: string | boolean, helper?: string | React$Node, type?: string, - onChange?: (any) => any, defaultValue?: string | number, placeholder?: string | number, children?: React$Node, @@ -43,18 +33,17 @@ type Props = { min?: number, max?: number, quickActionLabel?: string, - quickActionHandler?: (any) => any, disabled?: boolean, - onChange: (any) => void, value?: string | number, noEmojis?: boolean, + render?: () => React$Node, + onChange?: (any) => any, + quickActionHandler?: (any) => any, + openEmoteMenu?: () => void, }; export class FormField extends React.PureComponent { - static defaultProps = { - labelOnLeft: false, - blockWrap: true, - }; + static defaultProps = { labelOnLeft: false, blockWrap: true }; input: { current: ElementRef }; @@ -67,14 +56,11 @@ export class FormField extends React.PureComponent { const { autoFocus } = this.props; const input = this.input.current; - if (input && autoFocus) { - input.focus(); - } + if (input && autoFocus) input.focus(); } render() { const { - render, label, prefix, postfix, @@ -92,11 +78,24 @@ export class FormField extends React.PureComponent { charCount, textAreaMaxLength, quickActionLabel, - quickActionHandler, noEmojis, + render, + quickActionHandler, + openEmoteMenu, ...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 && ( + {`${charCount || '0'}/${textAreaMaxLength}`} + ); + const Wrapper = blockWrap ? ({ children: innerChildren }) => {innerChildren} : ({ children: innerChildren }) => {innerChildren}; @@ -108,207 +107,164 @@ export class FormField extends React.PureComponent { ) : null; - let input; - if (type) { - if (type === 'radio') { - input = ( - - - - - ); - } else if (type === 'checkbox') { - input = ( -
- - -
- ); - } else if (type === 'range') { - input = ( -
- - -
- ); - } else if (type === 'select') { - input = ( - - {(label || errorMessage) && ( - - )} - - - ); - } else if (type === 'select-tiny') { - input = ( - - {(label || errorMessage) && ( - - )} - - - ); - } else if (type === 'markdown') { - const handleEvents = { - contextmenu: openEditorMenu, - }; + const inputSimple = (type: string) => ( + <> + + + + ); - 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')); - } - } - }); + const inputSelect = (selectClass: string) => ( + + {(label || errorMessage) && ( + + )} + + + ); - // "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://)'; + const input = () => { + switch (type) { + case 'radio': + return {inputSimple('radio')}; + case 'checkbox': + return
{inputSimple('checkbox')}
; + case 'range': + return
{inputSimple('range')}
; + case 'select': + return inputSelect(''); + case 'select-tiny': + return inputSelect('select--slim'); + case 'markdown': + const handleEvents = { contextmenu: openEditorMenu }; - // 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]; + 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 (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; + if (delta <= 0) return; - // 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); + 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')); } } - } catch (err) { - // Do nothing (revert to original behavior) - } - }); - }; + }); - // 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 && ( - {`${charCount || '0'}/${textAreaMaxLength}`} - ); + // "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://)'; - input = ( -
+ // 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 ( +
+ +
+
+ +
+ {quickAction} +
+ ; + return ReactDOMServer.renderToString(preview); + }, + }} + /> + {countInfo} +
+
+ ); + case 'textarea': + return ( -
-
+ {(label || quickAction) && ( +
-
- {quickAction} -
- ; - return ReactDOMServer.renderToString(preview); - }, - }} - /> - {countInfo} - -
- ); - } else if (type === 'textarea') { - const hasCharCount = charCount !== undefined && charCount >= 0; - const countInfo = hasCharCount && textAreaMaxLength !== undefined && ( - {`${charCount || '0'}/${textAreaMaxLength}`} - ); - input = ( - - {(label || quickAction) && ( -
-
- -
- {quickAction} -
- )} -