From c98443fd8a5d9f74ac47f46080194c8ffc1ec1e6 Mon Sep 17 00:00:00 2001 From: zeppi Date: Fri, 1 Jul 2022 17:08:32 -0400 Subject: [PATCH] separate out advanced textarea, fix comment channel selector width, add advanced text icon --- static/app-strings.json | 2 + ui/component/collectionEdit/view.jsx | 4 +- ui/component/comment/view.jsx | 4 +- .../commentCreate/comment-create-header.jsx | 32 +++ ui/component/commentCreate/view.jsx | 47 ++-- .../form-field-area-advanced.jsx | 241 ++++++++++++++++++ .../common/form-components/form-field.jsx | 156 +----------- ui/component/common/form.jsx | 1 + ui/component/common/icon-custom.jsx | 11 + ui/component/postEditor/view.jsx | 4 +- ui/component/publishDescription/view.jsx | 4 +- ui/constants/icons.js | 2 + ui/page/report/view.jsx | 4 +- ui/scss/component/_card.scss | 2 +- ui/scss/component/_comment-create.scss | 51 ++-- ui/scss/component/_form-field.scss | 8 +- ui/scss/component/_notification.scss | 10 +- ui/scss/component/_textarea-suggestions.scss | 6 +- 18 files changed, 383 insertions(+), 206 deletions(-) create mode 100644 ui/component/commentCreate/comment-create-header.jsx create mode 100644 ui/component/common/form-components/form-field-area-advanced.jsx diff --git a/static/app-strings.json b/static/app-strings.json index 756121275..e014df315 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -2319,5 +2319,7 @@ "Your hub has blocked this content because it subscribes to the following blocking channel:": "Your hub has blocked this content because it subscribes to the following blocking channel:", "Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.": "Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.", "Autoplay Next is on.": "Autoplay Next is on.", + "This will be visible in a few minutes after you submit this form.": "This will be visible in a few minutes after you submit this form.", + "Anon --[used in <%anonymous% Reposted>]--": "Anon", "--end--": "--end--" } diff --git a/ui/component/collectionEdit/view.jsx b/ui/component/collectionEdit/view.jsx index 8976fa97f..088e67225 100644 --- a/ui/component/collectionEdit/view.jsx +++ b/ui/component/collectionEdit/view.jsx @@ -17,7 +17,7 @@ import { useHistory } from 'react-router-dom'; import { isNameValid, regexInvalidURI } from 'util/lbryURI'; import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses'; import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs'; -import { FormField } from 'component/common/form'; +import { FormField, FormFieldAreaAdvanced } from 'component/common/form'; import { handleBidChange } from 'util/publish'; import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field'; import { INVALID_NAME_ERROR } from 'constants/claim'; @@ -371,7 +371,7 @@ function CollectionForm(props: Props) { usePublishFormMode /> - {isEditing ? (
- void, + advanced: boolean, +}; + +export default function CommentCreateHeader(props: Props) { + const { isReply, advancedHandler, advanced } = props; + + return ( +
+
+ {(isReply ? __('Replying as') : __('Comment as')) + ' '} + +
+
+
+
+ ); +} diff --git a/ui/component/commentCreate/view.jsx b/ui/component/commentCreate/view.jsx index 8512fa3b1..afbeac4b6 100644 --- a/ui/component/commentCreate/view.jsx +++ b/ui/component/commentCreate/view.jsx @@ -4,7 +4,7 @@ import 'scss/component/_comment-create.scss'; import { buildValidSticker } from 'util/comments'; import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field'; -import { FormField, Form } from 'component/common/form'; +import { FormFieldAreaAdvanced, Form } from 'component/common/form'; import { getChannelIdFromClaim } from 'util/claim'; import { Lbryio } from 'lbryinc'; import { useHistory } from 'react-router'; @@ -22,8 +22,8 @@ import I18nMessage from 'component/i18nMessage'; import Icon from 'component/common/icon'; import OptimizedImage from 'component/optimizedImage'; import React from 'react'; -import SelectChannel from 'component/selectChannel'; import StickerSelector from './sticker-selector'; +import CommentCreateHeader from './comment-create-header'; import type { ElementRef } from 'react'; import UriIndicator from 'component/uriIndicator'; import usePersistedState from 'effects/use-persisted-state'; @@ -409,7 +409,11 @@ export function CommentCreate(props: Props) { push(pathPlusRedirect); }} > - +
@@ -420,22 +424,22 @@ export function CommentCreate(props: Props) { return ( {}} - className={classnames('commentCreate', { - 'commentCreate--reply': isReply, - 'commentCreate--nestedReply': isNested, - 'commentCreate--bottom': bottom, + className={classnames('comment-create', { + 'comment-create--reply': isReply, + 'comment-create--nestedReply': isNested, + 'comment-create--bottom': bottom, })} > {/* Input Box/Preview Box */} {stickerSelector ? ( handleSelectSticker(sticker)} claimIsMine={claimIsMine} /> ) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? ( -
-
+
+
-
+
{/* figure out lbc sticker prices */} @@ -447,15 +451,15 @@ export function CommentCreate(props: Props) { )}
) : isReviewingSupportComment && activeChannelClaim ? ( -
+
-
+
{commentValue}
@@ -470,23 +474,22 @@ export function CommentCreate(props: Props) { /> )} - - {(isReply ? __('Replying as') : __('Comment as')) + ' '} - -
+ header={ + setAdvancedEditor(!advancedEditor)} + /> } name={isReply ? 'content_reply' : 'content_description'} - quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')} ref={formFieldRef} onChange={handleCommentChange} openEmoteMenu={() => setShowEmotes(!showEmotes)} - quickActionHandler={() => setAdvancedEditor(!advancedEditor)} onFocus={onTextareaFocus} onBlur={onTextareaBlur} placeholder={__('Say something about this...')} @@ -654,7 +657,7 @@ export function CommentCreate(props: Props) { {/* Help Text */} {deletedComment &&
{__('This comment has been deleted.')}
} {!!minAmount && ( -
+
}}> {minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''} diff --git a/ui/component/common/form-components/form-field-area-advanced.jsx b/ui/component/common/form-components/form-field-area-advanced.jsx new file mode 100644 index 000000000..04d21a1c4 --- /dev/null +++ b/ui/component/common/form-components/form-field-area-advanced.jsx @@ -0,0 +1,241 @@ +// @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 = { + autoFocus?: boolean, + blockWrap: boolean, + charCount?: number, + children?: React$Node, + disabled?: boolean, + helper?: string | React$Node, + hideSuggestions?: boolean, + isLivestream?: boolean, + label?: string | Node, + labelOnLeft: boolean, + name: string, + noEmojis?: boolean, + placeholder?: string | number, + quickActionLabel?: string, + textAreaMaxLength?: number, + type?: string, + value?: string | number, + onChange?: (any) => any, + openEmoteMenu?: () => void, + quickActionHandler?: (any) => any, + render?: () => React$Node, + header?: React$Node, +}; + +export class FormFieldAreaAdvanced extends React.PureComponent { + static defaultProps = { labelOnLeft: false, blockWrap: true }; + + input: { current: ElementRef }; + + 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 { + autoFocus, + blockWrap, + charCount, + children, + helper, + hideSuggestions, + isLivestream, + label, + header, + labelOnLeft, + name, + noEmojis, + quickActionLabel, + textAreaMaxLength, + type, + openEmoteMenu, + quickActionHandler, + render, + ...inputProps + } = this.props; + + // 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 quickAction = + quickActionLabel && quickActionHandler ? ( +
+
+ ) : null; + + const input = () => { + switch (type) { + 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.substring(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 ( +
+ + {!header && ( +
+
+ +
+ {quickAction} +
+ )} + {!!header &&
{header}
} + ; + return ReactDOMServer.renderToString(preview); + }, + }} + /> + {countInfo} +
+
+ ); + case 'textarea': + return ( + + {!header && (label || quickAction) && ( +
+ + {quickAction} +
+ )} + {!!header &&
{header}
} + {hideSuggestions ? ( +