From 1a506327db88a96568b2a5daff528b1b1050c6fc Mon Sep 17 00:00:00 2001 From: mgabdev <> Date: Wed, 17 Jun 2020 13:25:10 -0400 Subject: [PATCH] Rich Text Editor (WIP) x3 --- app/controllers/api/v1/statuses_controller.rb | 6 +- app/javascript/gabsocial/actions/compose.js | 16 ++-- app/javascript/gabsocial/actions/statuses.js | 1 - .../components/autosuggest_textbox.js | 2 +- .../gabsocial/components/composer.js | 93 +++++++------------ .../components/modal/pro_upgrade_modal.js | 1 + .../components/rich_text_editor_bar.js | 50 +++++----- .../components/timeline_compose_block.js | 2 +- .../compose/components/compose_form.js | 4 +- .../components/rich_text_editor_button.js | 21 ++++- .../containers/compose_form_container.js | 12 ++- app/javascript/gabsocial/reducers/compose.js | 8 +- app/javascript/styles/global.css | 7 +- 13 files changed, 113 insertions(+), 110 deletions(-) diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index e800bff4..cdee103e 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -52,9 +52,10 @@ class Api::V1::StatusesController < Api::BaseController end def create + markdown = status_params[:markdown] unless status_params[:markdown] === status_params[:status] @status = PostStatusService.new.call(current_user.account, text: status_params[:status], - markdown: status_params[:markdown], + markdown: markdown, thread: status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]), media_ids: status_params[:media_ids], sensitive: status_params[:sensitive], @@ -72,9 +73,10 @@ class Api::V1::StatusesController < Api::BaseController def update authorize @status, :update? - + markdown = status_params[:markdown] unless status_params[:markdown] === status_params[:status] @status = EditStatusService.new.call(@status, text: status_params[:status], + markdown: markdown, media_ids: status_params[:media_ids], sensitive: status_params[:sensitive], spoiler_text: status_params[:spoiler_text], diff --git a/app/javascript/gabsocial/actions/compose.js b/app/javascript/gabsocial/actions/compose.js index 0aecfcaa..9745469f 100644 --- a/app/javascript/gabsocial/actions/compose.js +++ b/app/javascript/gabsocial/actions/compose.js @@ -268,7 +268,7 @@ export function submitCompose(group, replyToId = null, router, isStandalone) { // : hack : //Prepend http:// to urls in status that don't have protocol - status = status.replace(urlRegex, (match, a, b, c) =>{ + status = `${status}`.replace(urlRegex, (match, a, b, c) =>{ const hasProtocol = match.startsWith('https://') || match.startsWith('http://') //Make sure not a remote mention like @someone@somewhere.com if (!hasProtocol) { @@ -276,18 +276,20 @@ export function submitCompose(group, replyToId = null, router, isStandalone) { } return hasProtocol ? match : `http://${match}` }) - markdown = markdown.replace(urlRegex, (match) =>{ + markdown = !!markdown ? markdown.replace(urlRegex, (match) =>{ const hasProtocol = match.startsWith('https://') || match.startsWith('http://') if (!hasProtocol) { if (status.indexOf(`@${match}`) > -1) return match } return hasProtocol ? match : `http://${match}` - }) + }) : undefined + + if (status === markdown) { + markdown = undefined + } const inReplyToId = getState().getIn(['compose', 'in_reply_to'], null) || replyToId - // console.log("markdown:", markdown) - dispatch(submitComposeRequest()); dispatch(closeModal()); @@ -709,9 +711,9 @@ export function changeScheduledAt(date) { }; }; -export function changeRichTextEditorControlsVisibility(status) { +export function changeRichTextEditorControlsVisibility(open) { return { type: COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY, - status: status, + open, } } \ No newline at end of file diff --git a/app/javascript/gabsocial/actions/statuses.js b/app/javascript/gabsocial/actions/statuses.js index 60af0990..d22b6df1 100644 --- a/app/javascript/gabsocial/actions/statuses.js +++ b/app/javascript/gabsocial/actions/statuses.js @@ -109,7 +109,6 @@ export function fetchStatus(id) { }).then(() => { dispatch(fetchStatusSuccess(skipLoading)); }, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => { - console.log("response.data:", response.data) dispatch(importFetchedStatus(response.data)); dispatch(fetchStatusSuccess(skipLoading)); })).catch(error => { diff --git a/app/javascript/gabsocial/components/autosuggest_textbox.js b/app/javascript/gabsocial/components/autosuggest_textbox.js index 40e945a6..9bbc2ca0 100644 --- a/app/javascript/gabsocial/components/autosuggest_textbox.js +++ b/app/javascript/gabsocial/components/autosuggest_textbox.js @@ -201,7 +201,7 @@ export default class AutosuggestTextbox extends ImmutablePureComponent { } setTextbox = (c) => { - this.textbox = c; + this.textbox = c } render() { diff --git a/app/javascript/gabsocial/components/composer.js b/app/javascript/gabsocial/components/composer.js index 7be61d27..90691bac 100644 --- a/app/javascript/gabsocial/components/composer.js +++ b/app/javascript/gabsocial/components/composer.js @@ -4,6 +4,7 @@ import { CompositeDecorator, RichUtils, convertToRaw, + ContentState, } from 'draft-js' import draftToMarkdown from '../features/ui/util/draft-to-markdown' import { urlRegex } from '../features/ui/util/url_regex' @@ -27,11 +28,11 @@ function handleStrategy(contentBlock, callback, contentState) { findWithRegex(HANDLE_REGEX, contentBlock, callback) } -function hashtagStrategy (contentBlock, callback, contentState) { +function hashtagStrategy(contentBlock, callback, contentState) { findWithRegex(HASHTAG_REGEX, contentBlock, callback) } -function urlStrategy (contentBlock, callback, contentState) { +function urlStrategy(contentBlock, callback, contentState) { findWithRegex(urlRegex, contentBlock, callback) } @@ -70,22 +71,15 @@ const compositeDecorator = new CompositeDecorator([ } ]) -const HANDLE_REGEX = /\@[\w]+/g; -const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g; +const HANDLE_REGEX = /\@[\w]+/g +const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g -const mapDispatchToProps = (dispatch) => ({ - -}) - -export default -@connect(null, mapDispatchToProps) -class Composer extends PureComponent { +export default class Composer extends PureComponent { static propTypes = { inputRef: PropTypes.func, disabled: PropTypes.bool, placeholder: PropTypes.string, - autoFocus: PropTypes.bool, value: PropTypes.string, onChange: PropTypes.func, onKeyDown: PropTypes.func, @@ -97,40 +91,35 @@ class Composer extends PureComponent { } state = { - markdownText: '', - plainText: '', editorState: EditorState.createEmpty(compositeDecorator), - } - - static getDerivedStateFromProps(nextProps, prevState) { - // if (!nextProps.isHidden && nextProps.isIntersecting && !prevState.fetched) { - // return { - // fetched: true - // } - // } - - return null + plainText: this.props.value, } - componentDidUpdate (prevProps) { - if (prevProps.value !== this.props.value) { - // const editorState = EditorState.push(this.state.editorState, ContentState.createFromText(this.props.value)); - // this.setState({ editorState }) + componentDidUpdate() { + if (this.state.plainText !== this.props.value) { + let editorState + if (!this.props.value) { + editorState = EditorState.createEmpty(compositeDecorator) + } else { + editorState = EditorState.push(this.state.editorState, ContentState.createFromText(this.props.value)) + } + this.setState({ + editorState, + plainText: this.props.value, + }) } } - // EditorState.createWithContent(ContentState.createFromText('Hello')) + onChange = (editorState) => { + const content = editorState.getCurrentContent() + const plainText = content.getPlainText('\u0001') - onChange = (editorState, b, c, d) => { - this.setState({ editorState }) + this.setState({ editorState, plainText }) - const content = editorState.getCurrentContent(); - const text = content.getPlainText('\u0001') - const selectionState = editorState.getSelection() const selectionStart = selectionState.getStartOffset() - const rawObject = convertToRaw(content); + const rawObject = convertToRaw(content) const markdownString = draftToMarkdown(rawObject, { escapeMarkdownCharacters: false, preserveNewlines: false, @@ -143,24 +132,11 @@ class Composer extends PureComponent { inline: ['del', 'ins'], } } - }); + }) - console.log("text:", markdownString) - // console.log("html:", html) - - this.props.onChange(null, text, markdownString, selectionStart) + this.props.onChange(null, plainText, markdownString, selectionStart) } - // **bold** - // *italic* - // __underline__ - // ~~strike~~ - // # header - // > quote - // ``` - // code - // ``` - focus = () => { this.textbox.editor.focus() } @@ -177,27 +153,24 @@ class Composer extends PureComponent { return false } - handleOnTogglePopoutEditor = () => { - // - } - onTab = (e) => { const maxDepth = 4 this.onChange(RichUtils.onTab(e, this.state.editorState, maxDepth)) } setRef = (n) => { - this.textbox = n + try { + this.textbox = n + this.props.inputRef(n) + } catch (error) { + // + } } render() { const { - inputRef, disabled, placeholder, - autoFocus, - value, - onChange, onKeyDown, onKeyUp, onFocus, @@ -217,8 +190,6 @@ class Composer extends PureComponent { pt15: !small, px15: !small, px10: small, - pt5: small, - pb5: small, pb10: !small, }) diff --git a/app/javascript/gabsocial/components/modal/pro_upgrade_modal.js b/app/javascript/gabsocial/components/modal/pro_upgrade_modal.js index 235545c4..a130cb05 100644 --- a/app/javascript/gabsocial/components/modal/pro_upgrade_modal.js +++ b/app/javascript/gabsocial/components/modal/pro_upgrade_modal.js @@ -42,6 +42,7 @@ class ProUpgradeModal extends ImmutablePureComponent { • Larger Video and Image Uploads • Receive the PRO Badge • Remove in-feed promotions + • Compose Rich Text posts (Bold, Italic, Underline and more) { diff --git a/app/javascript/gabsocial/components/timeline_compose_block.js b/app/javascript/gabsocial/components/timeline_compose_block.js index c938713b..53d26f44 100644 --- a/app/javascript/gabsocial/components/timeline_compose_block.js +++ b/app/javascript/gabsocial/components/timeline_compose_block.js @@ -46,7 +46,7 @@ class TimelineComposeBlock extends ImmutablePureComponent { return ( - + ) diff --git a/app/javascript/gabsocial/features/compose/components/compose_form.js b/app/javascript/gabsocial/features/compose/components/compose_form.js index 7d495095..02ed2bde 100644 --- a/app/javascript/gabsocial/features/compose/components/compose_form.js +++ b/app/javascript/gabsocial/features/compose/components/compose_form.js @@ -370,7 +370,7 @@ class ComposeForm extends ImmutablePureComponent { > { - !!reduxReplyToId && isModalOpen && + !!reduxReplyToId && isModalOpen && isMatch && ({ active: state.getIn(['compose', 'rte_controls_visible']), + isPro: state.getIn(['accounts', me, 'is_pro']), }) const mapDispatchToProps = (dispatch) => ({ - onClick (status) { - dispatch(changeRichTextEditorControlsVisibility(status)) + onChangeRichTextEditorControlsVisibility() { + dispatch(changeRichTextEditorControlsVisibility()) }, + onOpenProUpgradeModal() { + dispatch(openModal('PRO_UPGRADE')) + }, }) export default @@ -29,14 +35,21 @@ class RichTextEditorButton extends PureComponent { active: PropTypes.bool, intl: PropTypes.object.isRequired, small: PropTypes.bool, + isPro: PropTypes.bool, + onOpenProUpgradeModal: PropTypes.func.isRequired, + onChangeRichTextEditorControlsVisibility: PropTypes.func.isRequired, } handleClick = (e) => { e.preventDefault() - this.props.onClick() + if (!this.props.isPro) { + this.props.onOpenProUpgradeModal() + } else { + this.props.onChangeRichTextEditorControlsVisibility() + } } - render () { + render() { const { active, intl, small } = this.props return ( diff --git a/app/javascript/gabsocial/features/compose/containers/compose_form_container.js b/app/javascript/gabsocial/features/compose/containers/compose_form_container.js index 35ea2652..12493738 100644 --- a/app/javascript/gabsocial/features/compose/containers/compose_form_container.js +++ b/app/javascript/gabsocial/features/compose/containers/compose_form_container.js @@ -12,7 +12,13 @@ import { } from '../../../actions/compose' import { me } from '../../../initial_state' -const mapStateToProps = (state, { replyToId, isStandalone }) => { +const mapStateToProps = (state, props) => { + const { + replyToId, + isStandalone, + shouldCondense, + modal, + } = props const reduxReplyToId = state.getIn(['compose', 'in_reply_to']) const isModalOpen = state.getIn(['modal', 'modalType']) === 'COMPOSE' || isStandalone @@ -27,7 +33,9 @@ const mapStateToProps = (state, { replyToId, isStandalone }) => { } if (isModalOpen) isMatch = true - + if (isModalOpen && shouldCondense) isMatch = false + if (isModalOpen && !modal) isMatch = false + // console.log("isMatch:", isMatch, reduxReplyToId, replyToId, state.getIn(['compose', 'text'])) // console.log("reduxReplyToId:", reduxReplyToId, isModalOpen, isStandalone) diff --git a/app/javascript/gabsocial/reducers/compose.js b/app/javascript/gabsocial/reducers/compose.js index f7b05035..59c488ba 100644 --- a/app/javascript/gabsocial/reducers/compose.js +++ b/app/javascript/gabsocial/reducers/compose.js @@ -100,6 +100,7 @@ function clearAll(state) { return state.withMutations(map => { map.set('id', null); map.set('text', ''); + map.set('markdown', null); map.set('spoiler', false); map.set('spoiler_text', ''); map.set('is_submitting', false); @@ -112,6 +113,8 @@ function clearAll(state) { map.set('poll', null); map.set('idempotencyKey', uuid()); map.set('scheduled_at', null); + map.set('rte_controls_visible', false); + map.set('gif', false); }); }; @@ -271,6 +274,7 @@ export default function compose(state = initialState, action) { map.set('idempotencyKey', uuid()); map.set('spoiler', false); map.set('spoiler_text', ''); + map.set('rte_controls_visible', false); if (action.text) { map.set('text', `${statusToTextMentions(state, action.status)}${action.text}`); } else { @@ -289,6 +293,7 @@ export default function compose(state = initialState, action) { map.set('idempotencyKey', uuid()); map.set('spoiler', false); map.set('spoiler_text', ''); + map.set('rte_controls_visible', ''); }); case COMPOSE_REPLY_CANCEL: case COMPOSE_RESET: @@ -371,6 +376,7 @@ export default function compose(state = initialState, action) { map.set('focusDate', new Date()); map.set('caretPosition', null); map.set('idempotencyKey', uuid()); + map.set('rte_controls_visible', false); if (action.status.get('spoiler_text').length > 0) { map.set('spoiler', true); @@ -396,7 +402,7 @@ export default function compose(state = initialState, action) { return state.set('scheduled_at', action.date); case COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY: return state.withMutations(map => { - map.set('rte_controls_visible', !state.get('rte_controls_visible')); + map.set('rte_controls_visible', action.open || !state.get('rte_controls_visible')); }); default: return state; diff --git a/app/javascript/styles/global.css b/app/javascript/styles/global.css index 49879372..5ed7def2 100644 --- a/app/javascript/styles/global.css +++ b/app/javascript/styles/global.css @@ -188,9 +188,10 @@ pre { } .statusContent code { - background-color: rgba(0,0,0,.05); - padding-left: 0.5em; - padding-right: 0.5em; + background-color: var(--border_color_secondary); + color: var(--text_color_secondary) !important; + font-size: var(--fs_n) !important; + padding: 0.25em 0.5em; } .dangerousContent,