Rich Text Editor (WIP) x3

This commit is contained in:
mgabdev 2020-06-17 13:25:10 -04:00
parent 861ae55aec
commit 1a506327db
13 changed files with 113 additions and 110 deletions

View File

@ -52,9 +52,10 @@ class Api::V1::StatusesController < Api::BaseController
end end
def create def create
markdown = status_params[:markdown] unless status_params[:markdown] === status_params[:status]
@status = PostStatusService.new.call(current_user.account, @status = PostStatusService.new.call(current_user.account,
text: status_params[:status], 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]), thread: status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]),
media_ids: status_params[:media_ids], media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive], sensitive: status_params[:sensitive],
@ -72,9 +73,10 @@ class Api::V1::StatusesController < Api::BaseController
def update def update
authorize @status, :update? authorize @status, :update?
markdown = status_params[:markdown] unless status_params[:markdown] === status_params[:status]
@status = EditStatusService.new.call(@status, @status = EditStatusService.new.call(@status,
text: status_params[:status], text: status_params[:status],
markdown: markdown,
media_ids: status_params[:media_ids], media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive], sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text], spoiler_text: status_params[:spoiler_text],

View File

@ -268,7 +268,7 @@ export function submitCompose(group, replyToId = null, router, isStandalone) {
// : hack : // : hack :
//Prepend http:// to urls in status that don't have protocol //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://') const hasProtocol = match.startsWith('https://') || match.startsWith('http://')
//Make sure not a remote mention like @someone@somewhere.com //Make sure not a remote mention like @someone@somewhere.com
if (!hasProtocol) { if (!hasProtocol) {
@ -276,18 +276,20 @@ export function submitCompose(group, replyToId = null, router, isStandalone) {
} }
return hasProtocol ? match : `http://${match}` return hasProtocol ? match : `http://${match}`
}) })
markdown = markdown.replace(urlRegex, (match) =>{ markdown = !!markdown ? markdown.replace(urlRegex, (match) =>{
const hasProtocol = match.startsWith('https://') || match.startsWith('http://') const hasProtocol = match.startsWith('https://') || match.startsWith('http://')
if (!hasProtocol) { if (!hasProtocol) {
if (status.indexOf(`@${match}`) > -1) return match if (status.indexOf(`@${match}`) > -1) return match
} }
return hasProtocol ? match : `http://${match}` return hasProtocol ? match : `http://${match}`
}) }) : undefined
if (status === markdown) {
markdown = undefined
}
const inReplyToId = getState().getIn(['compose', 'in_reply_to'], null) || replyToId const inReplyToId = getState().getIn(['compose', 'in_reply_to'], null) || replyToId
// console.log("markdown:", markdown)
dispatch(submitComposeRequest()); dispatch(submitComposeRequest());
dispatch(closeModal()); dispatch(closeModal());
@ -709,9 +711,9 @@ export function changeScheduledAt(date) {
}; };
}; };
export function changeRichTextEditorControlsVisibility(status) { export function changeRichTextEditorControlsVisibility(open) {
return { return {
type: COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY, type: COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY,
status: status, open,
} }
} }

View File

@ -109,7 +109,6 @@ export function fetchStatus(id) {
}).then(() => { }).then(() => {
dispatch(fetchStatusSuccess(skipLoading)); dispatch(fetchStatusSuccess(skipLoading));
}, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => { }, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => {
console.log("response.data:", response.data)
dispatch(importFetchedStatus(response.data)); dispatch(importFetchedStatus(response.data));
dispatch(fetchStatusSuccess(skipLoading)); dispatch(fetchStatusSuccess(skipLoading));
})).catch(error => { })).catch(error => {

View File

@ -201,7 +201,7 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
} }
setTextbox = (c) => { setTextbox = (c) => {
this.textbox = c; this.textbox = c
} }
render() { render() {

View File

@ -4,6 +4,7 @@ import {
CompositeDecorator, CompositeDecorator,
RichUtils, RichUtils,
convertToRaw, convertToRaw,
ContentState,
} from 'draft-js' } from 'draft-js'
import draftToMarkdown from '../features/ui/util/draft-to-markdown' import draftToMarkdown from '../features/ui/util/draft-to-markdown'
import { urlRegex } from '../features/ui/util/url_regex' import { urlRegex } from '../features/ui/util/url_regex'
@ -70,22 +71,15 @@ const compositeDecorator = new CompositeDecorator([
} }
]) ])
const HANDLE_REGEX = /\@[\w]+/g; const HANDLE_REGEX = /\@[\w]+/g
const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g; const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g
const mapDispatchToProps = (dispatch) => ({ export default class Composer extends PureComponent {
})
export default
@connect(null, mapDispatchToProps)
class Composer extends PureComponent {
static propTypes = { static propTypes = {
inputRef: PropTypes.func, inputRef: PropTypes.func,
disabled: PropTypes.bool, disabled: PropTypes.bool,
placeholder: PropTypes.string, placeholder: PropTypes.string,
autoFocus: PropTypes.bool,
value: PropTypes.string, value: PropTypes.string,
onChange: PropTypes.func, onChange: PropTypes.func,
onKeyDown: PropTypes.func, onKeyDown: PropTypes.func,
@ -97,40 +91,35 @@ class Composer extends PureComponent {
} }
state = { state = {
markdownText: '',
plainText: '',
editorState: EditorState.createEmpty(compositeDecorator), editorState: EditorState.createEmpty(compositeDecorator),
plainText: this.props.value,
} }
static getDerivedStateFromProps(nextProps, prevState) { componentDidUpdate() {
// if (!nextProps.isHidden && nextProps.isIntersecting && !prevState.fetched) { if (this.state.plainText !== this.props.value) {
// return { let editorState
// fetched: true if (!this.props.value) {
// } editorState = EditorState.createEmpty(compositeDecorator)
// } } else {
editorState = EditorState.push(this.state.editorState, ContentState.createFromText(this.props.value))
return null
} }
this.setState({
componentDidUpdate (prevProps) { editorState,
if (prevProps.value !== this.props.value) { plainText: this.props.value,
// const editorState = EditorState.push(this.state.editorState, ContentState.createFromText(this.props.value)); })
// this.setState({ editorState })
} }
} }
// EditorState.createWithContent(ContentState.createFromText('Hello')) onChange = (editorState) => {
const content = editorState.getCurrentContent()
const plainText = content.getPlainText('\u0001')
onChange = (editorState, b, c, d) => { this.setState({ editorState, plainText })
this.setState({ editorState })
const content = editorState.getCurrentContent();
const text = content.getPlainText('\u0001')
const selectionState = editorState.getSelection() const selectionState = editorState.getSelection()
const selectionStart = selectionState.getStartOffset() const selectionStart = selectionState.getStartOffset()
const rawObject = convertToRaw(content); const rawObject = convertToRaw(content)
const markdownString = draftToMarkdown(rawObject, { const markdownString = draftToMarkdown(rawObject, {
escapeMarkdownCharacters: false, escapeMarkdownCharacters: false,
preserveNewlines: false, preserveNewlines: false,
@ -143,24 +132,11 @@ class Composer extends PureComponent {
inline: ['del', 'ins'], inline: ['del', 'ins'],
} }
} }
}); })
console.log("text:", markdownString) this.props.onChange(null, plainText, markdownString, selectionStart)
// console.log("html:", html)
this.props.onChange(null, text, markdownString, selectionStart)
} }
// **bold**
// *italic*
// __underline__
// ~~strike~~
// # header
// > quote
// ```
// code
// ```
focus = () => { focus = () => {
this.textbox.editor.focus() this.textbox.editor.focus()
} }
@ -177,27 +153,24 @@ class Composer extends PureComponent {
return false return false
} }
handleOnTogglePopoutEditor = () => {
//
}
onTab = (e) => { onTab = (e) => {
const maxDepth = 4 const maxDepth = 4
this.onChange(RichUtils.onTab(e, this.state.editorState, maxDepth)) this.onChange(RichUtils.onTab(e, this.state.editorState, maxDepth))
} }
setRef = (n) => { setRef = (n) => {
try {
this.textbox = n this.textbox = n
this.props.inputRef(n)
} catch (error) {
//
}
} }
render() { render() {
const { const {
inputRef,
disabled, disabled,
placeholder, placeholder,
autoFocus,
value,
onChange,
onKeyDown, onKeyDown,
onKeyUp, onKeyUp,
onFocus, onFocus,
@ -217,8 +190,6 @@ class Composer extends PureComponent {
pt15: !small, pt15: !small,
px15: !small, px15: !small,
px10: small, px10: small,
pt5: small,
pb5: small,
pb10: !small, pb10: !small,
}) })

View File

@ -42,6 +42,7 @@ class ProUpgradeModal extends ImmutablePureComponent {
<Text> Larger Video and Image Uploads</Text> <Text> Larger Video and Image Uploads</Text>
<Text> Receive the PRO Badge</Text> <Text> Receive the PRO Badge</Text>
<Text> Remove in-feed promotions</Text> <Text> Remove in-feed promotions</Text>
<Text> Compose Rich Text posts (Bold, Italic, Underline and more)</Text>
</div> </div>
<Button <Button

View File

@ -38,35 +38,35 @@ const RTE_ITEMS = [
// icon: 'circle', // icon: 'circle',
// }, // },
{ {
label: 'H1', label: 'Title',
style: 'header-one', style: 'header-one',
type: 'block', type: 'block',
icon: 'text-size', icon: 'text-size',
}, },
{ // {
label: 'Blockquote', // label: 'Blockquote',
style: 'blockquote', // style: 'blockquote',
type: 'block', // type: 'block',
icon: 'blockquote', // icon: 'blockquote',
}, // },
{ // {
label: 'Code Block', // label: 'Code Block',
style: 'code-block', // style: 'code-block',
type: 'block', // type: 'block',
icon: 'code', // icon: 'code',
}, // },
{ // {
label: 'UL', // label: 'UL',
style: 'unordered-list-item', // style: 'unordered-list-item',
type: 'block', // type: 'block',
icon: 'ul-list', // icon: 'ul-list',
}, // },
{ // {
label: 'OL', // label: 'OL',
style: 'ordered-list-item', // style: 'ordered-list-item',
type: 'block', // type: 'block',
icon: 'ol-list', // icon: 'ol-list',
}, // },
] ]
const mapStateToProps = (state) => { const mapStateToProps = (state) => {

View File

@ -46,7 +46,7 @@ class TimelineComposeBlock extends ImmutablePureComponent {
return ( return (
<section className={_s.default}> <section className={_s.default}>
<div className={[_s.default, _s.flexRow].join(' ')}> <div className={[_s.default, _s.flexRow].join(' ')}>
<ComposeFormContainer {...rest} /> <ComposeFormContainer {...rest} modal={modal} />
</div> </div>
</section> </section>
) )

View File

@ -370,7 +370,7 @@ class ComposeForm extends ImmutablePureComponent {
> >
{ {
!!reduxReplyToId && isModalOpen && !!reduxReplyToId && isModalOpen && isMatch &&
<div className={[_s.default, _s.px15, _s.py10, _s.mt5].join(' ')}> <div className={[_s.default, _s.px15, _s.py10, _s.mt5].join(' ')}>
<StatusContainer <StatusContainer
contextType='compose' contextType='compose'
@ -443,7 +443,7 @@ class ComposeForm extends ImmutablePureComponent {
} }
{ {
!!quoteOfId && isModalOpen && !!quoteOfId && isModalOpen && isMatch &&
<div className={[_s.default, _s.px15, _s.py10, _s.mt5].join(' ')}> <div className={[_s.default, _s.px15, _s.py10, _s.mt5].join(' ')}>
<StatusContainer <StatusContainer
contextType='compose' contextType='compose'

View File

@ -1,5 +1,7 @@
import { injectIntl, defineMessages } from 'react-intl' import { injectIntl, defineMessages } from 'react-intl'
import { changeRichTextEditorControlsVisibility } from '../../../actions/compose' import { changeRichTextEditorControlsVisibility } from '../../../actions/compose'
import { openModal } from '../../../actions/modal'
import { me } from '../../../initial_state'
import ComposeExtraButton from './compose_extra_button' import ComposeExtraButton from './compose_extra_button'
const messages = defineMessages({ const messages = defineMessages({
@ -10,14 +12,18 @@ const messages = defineMessages({
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
active: state.getIn(['compose', 'rte_controls_visible']), active: state.getIn(['compose', 'rte_controls_visible']),
isPro: state.getIn(['accounts', me, 'is_pro']),
}) })
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
onClick (status) { onChangeRichTextEditorControlsVisibility() {
dispatch(changeRichTextEditorControlsVisibility(status)) dispatch(changeRichTextEditorControlsVisibility())
}, },
onOpenProUpgradeModal() {
dispatch(openModal('PRO_UPGRADE'))
},
}) })
export default export default
@ -29,11 +35,18 @@ class RichTextEditorButton extends PureComponent {
active: PropTypes.bool, active: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
small: PropTypes.bool, small: PropTypes.bool,
isPro: PropTypes.bool,
onOpenProUpgradeModal: PropTypes.func.isRequired,
onChangeRichTextEditorControlsVisibility: PropTypes.func.isRequired,
} }
handleClick = (e) => { handleClick = (e) => {
e.preventDefault() e.preventDefault()
this.props.onClick() if (!this.props.isPro) {
this.props.onOpenProUpgradeModal()
} else {
this.props.onChangeRichTextEditorControlsVisibility()
}
} }
render() { render() {

View File

@ -12,7 +12,13 @@ import {
} from '../../../actions/compose' } from '../../../actions/compose'
import { me } from '../../../initial_state' 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 reduxReplyToId = state.getIn(['compose', 'in_reply_to'])
const isModalOpen = state.getIn(['modal', 'modalType']) === 'COMPOSE' || isStandalone const isModalOpen = state.getIn(['modal', 'modalType']) === 'COMPOSE' || isStandalone
@ -27,6 +33,8 @@ const mapStateToProps = (state, { replyToId, isStandalone }) => {
} }
if (isModalOpen) isMatch = true 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("isMatch:", isMatch, reduxReplyToId, replyToId, state.getIn(['compose', 'text']))
// console.log("reduxReplyToId:", reduxReplyToId, isModalOpen, isStandalone) // console.log("reduxReplyToId:", reduxReplyToId, isModalOpen, isStandalone)

View File

@ -100,6 +100,7 @@ function clearAll(state) {
return state.withMutations(map => { return state.withMutations(map => {
map.set('id', null); map.set('id', null);
map.set('text', ''); map.set('text', '');
map.set('markdown', null);
map.set('spoiler', false); map.set('spoiler', false);
map.set('spoiler_text', ''); map.set('spoiler_text', '');
map.set('is_submitting', false); map.set('is_submitting', false);
@ -112,6 +113,8 @@ function clearAll(state) {
map.set('poll', null); map.set('poll', null);
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
map.set('scheduled_at', null); 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('idempotencyKey', uuid());
map.set('spoiler', false); map.set('spoiler', false);
map.set('spoiler_text', ''); map.set('spoiler_text', '');
map.set('rte_controls_visible', false);
if (action.text) { if (action.text) {
map.set('text', `${statusToTextMentions(state, action.status)}${action.text}`); map.set('text', `${statusToTextMentions(state, action.status)}${action.text}`);
} else { } else {
@ -289,6 +293,7 @@ export default function compose(state = initialState, action) {
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
map.set('spoiler', false); map.set('spoiler', false);
map.set('spoiler_text', ''); map.set('spoiler_text', '');
map.set('rte_controls_visible', '');
}); });
case COMPOSE_REPLY_CANCEL: case COMPOSE_REPLY_CANCEL:
case COMPOSE_RESET: case COMPOSE_RESET:
@ -371,6 +376,7 @@ export default function compose(state = initialState, action) {
map.set('focusDate', new Date()); map.set('focusDate', new Date());
map.set('caretPosition', null); map.set('caretPosition', null);
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
map.set('rte_controls_visible', false);
if (action.status.get('spoiler_text').length > 0) { if (action.status.get('spoiler_text').length > 0) {
map.set('spoiler', true); map.set('spoiler', true);
@ -396,7 +402,7 @@ export default function compose(state = initialState, action) {
return state.set('scheduled_at', action.date); return state.set('scheduled_at', action.date);
case COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY: case COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY:
return state.withMutations(map => { 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: default:
return state; return state;

View File

@ -188,9 +188,10 @@ pre {
} }
.statusContent code { .statusContent code {
background-color: rgba(0,0,0,.05); background-color: var(--border_color_secondary);
padding-left: 0.5em; color: var(--text_color_secondary) !important;
padding-right: 0.5em; font-size: var(--fs_n) !important;
padding: 0.25em 0.5em;
} }
.dangerousContent, .dangerousContent,