Commit f7e0528d authored by mgabdev's avatar mgabdev

Merge branch 'feature/rich_text_editor' of https://code.gab.com/gab/social/gab-social into develop

parents 9d616596 13eb1764
......@@ -94,6 +94,8 @@ gem 'json-ld', '~> 3.0'
gem 'json-ld-preloaded', '~> 3.0'
gem 'rdf-normalize', '~> 0.3'
gem 'redcarpet', '~> 3.4'
group :development, :test do
gem 'fabrication', '~> 2.20'
gem 'fuubar', '~> 2.3'
......
......@@ -479,6 +479,7 @@ GEM
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.3)
rdf (>= 2.2, < 4.0)
redcarpet (3.4.0)
redis (4.1.2)
redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6)
......@@ -740,6 +741,7 @@ DEPENDENCIES
rails-i18n (~> 5.1)
rails-settings-cached (~> 0.6)
rdf-normalize (~> 0.3)
redcarpet (~> 3.4)
redis (~> 4.1)
redis-namespace (~> 1.5)
redis-rails (~> 5.0)
......
......@@ -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],
......
......@@ -263,12 +263,12 @@ export function submitCompose(group, replyToId = null, router, isStandalone) {
if (!me) return;
let status = getState().getIn(['compose', 'text'], '');
const markdown = getState().getIn(['compose', 'markdown'], '');
let markdown = getState().getIn(['compose', 'markdown'], '');
const media = getState().getIn(['compose', 'media_attachments']);
// : 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,15 +276,20 @@ export function submitCompose(group, replyToId = null, router, isStandalone) {
}
return hasProtocol ? match : `http://${match}`
})
// markdown = statusMarkdown.replace(urlRegex, (match) =>{
// const hasProtocol = match.startsWith('https://') || match.startsWith('http://')
// return hasProtocol ? match : `http://${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());
......@@ -706,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
import escapeTextContentForBrowser from 'escape-html';
import emojify from '../../components/emoji/emoji';
import { unescapeHTML } from '../../utils/html';
import { expandSpoilers } from '../../initial_state';
import escapeTextContentForBrowser from 'escape-html'
import emojify from '../../components/emoji/emoji'
import { unescapeHTML } from '../../utils/html'
import { expandSpoilers } from '../../initial_state'
const domParser = new DOMParser();
const domParser = new DOMParser()
const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji;
......@@ -86,3 +86,21 @@ export function normalizePoll(poll) {
return normalPoll;
}
// <p><h1>attention!</h1></p>
// <p>#test @bob #nice https://bob.com http://techcrunch.com <del>strike it</del></p>
// <p><del>https://twitter.com</del></p>
// <p><em>@bobitalic</em></p>
// <p><pre><code>jonincode</code></pre></p>
// # attention!
// #test @bob #nice https://bob.com http://techcrunch.com ~~strike it~~
// ~~https://twitter.com~~
// _@bobitalic_
// ```
// jonincode
// ```
\ No newline at end of file
......@@ -15,6 +15,7 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
static propTypes = {
value: PropTypes.string,
valueMarkdown: PropTypes.string,
suggestions: ImmutablePropTypes.list,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
......@@ -45,11 +46,12 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
tokenStart: 0,
}
onChange = (e, value, selectionStart, markdown) => {
onChange = (e, value, markdown, selectionStart) => {
if (!isObject(e)) {
e = {
target: {
value,
markdown,
selectionStart,
},
}
......@@ -65,7 +67,7 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
this.props.onSuggestionsClearRequested();
}
this.props.onChange(e, markdown);
this.props.onChange(e);
}
onKeyDown = (e) => {
......@@ -191,7 +193,7 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
}
setTextbox = (c) => {
this.textbox = c;
this.textbox = c
}
render() {
......@@ -203,6 +205,7 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
placeholder,
onKeyUp,
children,
valueMarkdown,
id,
} = this.props
......@@ -246,29 +249,14 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
<label htmlFor={id} className={_s.visiblyHidden}>
{placeholder}
</label>
<Textarea
id={id}
inputRef={this.setTextbox}
className={textareaClasses}
disabled={disabled}
placeholder={placeholder}
autoFocus={false}
value={value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onKeyUp={onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
onPaste={this.onPaste}
aria-autocomplete='list'
/>
{/*<Composer
<Composer
inputRef={this.setTextbox}
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}
value={value}
valueMarkdown={valueMarkdown}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onKeyUp={onKeyUp}
......@@ -276,7 +264,7 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
onBlur={this.onBlur}
onPaste={this.onPaste}
small={small}
/>*/}
/>
{children}
</div>
......
......@@ -7,8 +7,8 @@ import {
convertFromRaw,
ContentState,
} from 'draft-js'
import { draftToMarkdown } from 'markdown-draft-js'
// import draftToMarkdown from 'draftjs-to-markdown'
import draftToMarkdown from '../features/ui/util/draft-to-markdown'
import markdownToDraft from '../features/ui/util/markdown-to-draft'
import { urlRegex } from '../features/ui/util/url_regex'
import classNames from 'classnames/bind'
import RichTextEditorBar from './rich_text_editor_bar'
......@@ -30,11 +30,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)
}
......@@ -73,24 +73,17 @@ 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,
valueMarkdown: PropTypes.string,
onChange: PropTypes.func,
onKeyDown: PropTypes.func,
onKeyUp: PropTypes.func,
......@@ -101,59 +94,69 @@ 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) {
// console.log("this.props.value:", this.props.value)
if (prevProps.value !== this.props.value) {
// const editorState = EditorState.push(this.state.editorState, ContentState.createFromText(this.props.value));
// this.setState({ editorState })
componentDidMount() {
if (this.props.valueMarkdown) {
const rawData = markdownToDraft(this.props.valueMarkdown)
const contentState = convertFromRaw(rawData)
const editorState = EditorState.createWithContent(contentState)
this.setState({
editorState,
plainText: this.props.value,
})
} else if (this.props.value) {
editorState = EditorState.push(this.state.editorState, ContentState.createFromText(this.props.value))
this.setState({
editorState,
plainText: this.props.value,
})
}
}
// EditorState.createWithContent(ContentState.createFromText('Hello'))
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,
})
}
}
onChange = (editorState) => {
this.setState({ editorState })
const content = this.state.editorState.getCurrentContent();
const text = content.getPlainText('\u0001')
// const selectionState = editorState.getSelection()
// const selectionStart = selectionState.getStartOffset()
// const rawObject = convertToRaw(content);
// const markdownString = draftToMarkdown(rawObject);
// const markdownString = draftToMarkdown(rawObject, {
// trigger: '#',
// separator: ' ',
// });
// console.log("text:", text, this.props.value)
this.props.onChange(null, text, selectionStart, markdownString)
}
const content = editorState.getCurrentContent()
const plainText = content.getPlainText('\u0001')
this.setState({ editorState, plainText })
const selectionState = editorState.getSelection()
const selectionStart = selectionState.getStartOffset()
const rawObject = convertToRaw(content)
const markdownString = draftToMarkdown(rawObject, {
escapeMarkdownCharacters: false,
preserveNewlines: false,
remarkablePreset: 'commonmark',
remarkableOptions: {
disable: {
block: ['table']
},
enable: {
inline: ['del', 'ins'],
}
}
})
// **bold**
// *italic*
// __underline__
// ~strikethrough~
// # title
// > quote
// `code`
// ```code```
this.props.onChange(null, plainText, markdownString, selectionStart)
}
focus = () => {
this.textbox.editor.focus()
......@@ -171,27 +174,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,
......@@ -211,15 +211,13 @@ class Composer extends PureComponent {
pt15: !small,
px15: !small,
px10: small,
pt5: small,
pb5: small,
pb10: !small,
})
return (
<div className={_s.default}>
{ /** : todo : */
{
!small &&
<RichTextEditorBar
editorState={editorState}
......@@ -241,6 +239,9 @@ class Composer extends PureComponent {
placeholder={placeholder}
ref={this.setRef}
readOnly={disabled}
onBlur={onBlur}
onFocus={onFocus}
stripPastedStyles
/>
</div>
</div>
......
......@@ -42,6 +42,7 @@ class ProUpgradeModal extends ImmutablePureComponent {
<Text> Larger Video and Image Uploads</Text>
<Text> Receive the PRO Badge</Text>
<Text> Remove in-feed promotions</Text>
<Text> Compose Rich Text posts (Bold, Italic, Underline and more)</Text>
</div>
<Button
......
import { RichUtils } from 'draft-js'
import { defineMessages, injectIntl } from 'react-intl'
import classNames from 'classnames/bind'
import { me } from '../initial_state'
import { makeGetAccount } from '../selectors'
......@@ -39,41 +38,37 @@ const RTE_ITEMS = [
// icon: 'circle',
// },
{
label: 'H1',
label: 'Title',
style: 'header-one',
type: 'block',
icon: 'text-size',
},
{
label: 'Blockquote',
style: 'blockquote',
type: 'block',
icon: 'blockquote',
},
{
label: 'Code Block',
style: 'code-block',
type: 'block',
icon: 'code',
},
{
label: 'UL',
style: 'unordered-list-item',
type: 'block',
icon: 'ul-list',
},
{
label: 'OL',
style: 'ordered-list-item',
type: 'block',
icon: 'ol-list',
},
// {
// label: 'Blockquote',
// style: 'blockquote',
// type: 'block',
// icon: 'blockquote',
// },
// {
// label: 'Code Block',
// style: 'code-block',
// type: 'block',
// icon: 'code',
// },
// {
// label: 'UL',
// style: 'unordered-list-item',
// type: 'block',
// icon: 'ul-list',
// },
// {
// label: 'OL',
// style: 'ordered-list-item',
// type: 'block',
// icon: 'ol-list',
// },
]
const messages = defineMessages({
follow: { id: 'follow', defaultMessage: 'Follow' },
})
const mapStateToProps = (state) => {
const getAccount = makeGetAccount()
const account = getAccount(state, me)
......@@ -86,13 +81,11 @@ const mapStateToProps = (state) => {
}
export default
@injectIntl
@connect(mapStateToProps)
class RichTextEditorBar extends PureComponent {
static propTypes = {
editorState: PropTypes.object.isRequired,
intl: PropTypes.object.isRequired,
isPro: PropTypes.bool.isRequired,
rteControlsVisible: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
......@@ -127,7 +120,7 @@ class RichTextEditorBar extends PureComponent {
/>
))
}
<Button
{/*<Button
backgroundColor='none'
color='secondary'
onClick={this.handleOnTogglePopoutEditor}
......@@ -137,7 +130,7 @@ class RichTextEditorBar extends PureComponent {
iconClassName={_s.inheritFill}
iconSize='12px'
radiusSmall
/>
/>*/}
</div>
)
}
......
......@@ -46,7 +46,7 @@ class TimelineComposeBlock extends ImmutablePureComponent {
return (
<section className={_s.default}>
<div className={[_s.default, _s.flexRow].join(' ')}>
<ComposeFormContainer {...rest} />
<ComposeFormContainer {...rest} modal={modal} />
</div>
</section>
)
......
......@@ -21,6 +21,7 @@ import PollButton from './poll_button'
import PollForm from './poll_form'
import SchedulePostButton from './schedule_post_button'
import SpoilerButton from './spoiler_button'
import RichTextEditorButton from './rich_text_editor_button'
import StatusContainer from '../../../containers/status_container'
import StatusVisibilityButton from './status_visibility_button'
import UploadButton from './media_upload_button'
......@@ -44,7 +45,7 @@ class ComposeForm extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
}
state = {
composeFocused: false,
}
......@@ -54,6 +55,7 @@ class ComposeForm extends ImmutablePureComponent {
edit: PropTypes.bool,
isMatch: PropTypes.bool,
text: PropTypes.string.isRequired,
markdown: PropTypes.string,
suggestions: ImmutablePropTypes.list,
account: ImmutablePropTypes.map.isRequired,
status: ImmutablePropTypes.map,
......@@ -92,14 +94,8 @@ class ComposeForm extends ImmutablePureComponent {
showSearch: false,
};
handleChange = (e, markdown) => {
let position = null
try {
position = this.autosuggestTextarea.textbox.selectionStart
} catch (error) {
//
}
this.props.onChange(e.target.value, markdown, this.props.replyToId, position)
handleChange = (e, selectionStart) => {
this.props.onChange(e.target.value, e.target.markdown, this.props.replyToId, selectionStart)
}
handleComposeFocus = () => {
......@@ -137,11 +133,11 @@ class ComposeForm extends ImmutablePureComponent {
}
handleSubmit = () => {
if (this.props.text !== this.autosuggestTextarea.textbox.value) {
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
// Update the state to match the current text
this.props.onChange(this.autosuggestTextarea.textbox.value);
}
// if (this.props.text !== this.autosuggestTextarea.textbox.value) {
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
// Update the state to match the current text
// this.props.onChange(this.autosuggestTextarea.textbox.value);
// }
// Submit disabled:
const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props;
......@@ -218,6 +214,7 @@ class ComposeForm extends ImmutablePureComponent {
}
handleEmojiPick = (data) => {
// : todo : with rich text
const { text } = this.props
const position = this.autosuggestTextarea.textbox.selectionStart
const needsSpace = data.custom && position > 0 && !ALLOWED_AROUND_SHORT_CODE.includes(text[position - 1])
......@@ -248,6 +245,7 @@ class ComposeForm extends ImmutablePureComponent {
isSubmitting,
selectedGifSrc,
} = this.props
const disabled = isSubmitting
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
const disabledButton = disabled || isUploading || isChangingUpload || length(text) > MAX_POST_CHARACTER_COUNT || (length(text) !== 0 && length(text.trim()) === 0 && !anyMedia);
......@@ -330,9 +328,9 @@ class ComposeForm extends ImmutablePureComponent {
<div className={actionsContainerClasses}>
<div className={[_s.default, _s.flexRow, _s.mrAuto].join(' ')}>
{ /* <EmojiPickerButton small={shouldCondense} isMatch={isMatch} /> */ }
{ /* <EmojiPickerButton small={shouldCondense} isMatch={isMatch} /> */}
{ /* <UploadButton small={shouldCondense} /> */ }
{ /* <UploadButton small={shouldCondense} /> */}
<div className={commentPublishBtnClasses}>
<Button
......@@ -375,7 +373,7 @@ class ComposeForm extends ImmutablePureComponent {
>
{
!!reduxReplyToId && isModalOpen &&