From fed036be08cebdf13a730b3f2682180b596708ed Mon Sep 17 00:00:00 2001 From: mgabdev <> Date: Wed, 22 Apr 2020 01:00:11 -0400 Subject: [PATCH] Progress --- Gemfile | 2 + Gemfile.lock | 4 + app/controllers/api/v1/statuses_controller.rb | 19 +- app/javascript/gabsocial/actions/compose.js | 12 +- .../gabsocial/actions/importer/index.js | 5 + .../gabsocial/actions/importer/normalizer.js | 3 +- app/javascript/gabsocial/actions/statuses.js | 48 +- .../gabsocial/components/account.js | 7 +- .../components/autosuggest_textbox.js | 6 +- .../gabsocial/components/composer.js | 30 +- .../intersection_observer_article.js | 10 +- app/javascript/gabsocial/components/list.js | 5 +- .../gabsocial/components/modal/modal_root.js | 4 +- .../components/panel/panel_layout.js | 4 +- .../popover/status_options_popover.js | 170 ++++-- .../gabsocial/components/scrollable_list.js | 2 +- .../components/sidebar_section_item.js | 6 +- app/javascript/gabsocial/components/status.js | 384 ++++++++------ .../gabsocial/components/status_action_bar.js | 124 +---- .../gabsocial/components/status_content.js | 11 +- .../gabsocial/components/status_header.js | 74 +-- .../gabsocial/components/status_list.js | 40 +- .../gabsocial/components/status_prepend.js | 14 +- .../gabsocial/containers/media_container.js | 6 +- .../gabsocial/containers/status_container.js | 103 +++- .../components/compose_form/compose_form.js | 24 +- .../features/compose/components/poll_form.js | 4 +- .../containers/compose_form_container.js | 4 +- .../gabsocial/features/lists_directory.js | 9 +- .../containers/detailed_status_container.js | 162 ------ .../gabsocial/features/status/index.js | 1 - .../gabsocial/features/status/status.js | 483 ------------------ .../features/ui/util/async_components.js | 2 +- .../gabsocial/features/ui/util/page_title.js | 4 +- app/javascript/gabsocial/reducers/compose.js | 3 +- app/javascript/gabsocial/reducers/contexts.js | 17 +- app/javascript/styles/global.css | 41 +- app/lib/formatter.rb | 119 ++++- app/models/status.rb | 1 + app/serializers/rest/status_serializer.rb | 5 + app/services/post_status_service.rb | 3 + config/routes.rb | 1 + .../20200420223346_add_markdown_to_status.rb | 5 + db/schema.rb | 3 +- package.json | 2 + yarn.lock | 31 +- 46 files changed, 883 insertions(+), 1134 deletions(-) delete mode 100644 app/javascript/gabsocial/features/status/containers/detailed_status_container.js delete mode 100644 app/javascript/gabsocial/features/status/index.js delete mode 100644 app/javascript/gabsocial/features/status/status.js create mode 100644 db/migrate/20200420223346_add_markdown_to_status.rb diff --git a/Gemfile b/Gemfile index 00f9e451..fd6fca4b 100644 --- a/Gemfile +++ b/Gemfile @@ -56,6 +56,7 @@ gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', r gem 'httplog', '~> 1.3' gem 'idn-ruby', require: 'idn' gem 'kaminari', '~> 1.1' +gem 'kramdown', '~> 2.1.0' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.2', require: 'mime/types/columnar' gem 'nokogiri', '~> 1.10' @@ -89,6 +90,7 @@ gem 'twitter-text', '~> 1.14' gem 'tzinfo-data', '~> 1.2019' gem 'webpacker', '~> 4.0' gem 'webpush' +gem 'redcarpet', '~> 3.5.0' gem 'json-ld', '~> 3.0' gem 'json-ld-preloaded', '~> 3.0' diff --git a/Gemfile.lock b/Gemfile.lock index a5c48dfd..4efcac86 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -311,6 +311,7 @@ GEM activerecord kaminari-core (= 1.1.1) kaminari-core (1.1.1) + kramdown (2.1.0) launchy (2.4.3) addressable (~> 2.3) letter_opener (1.7.0) @@ -478,6 +479,7 @@ GEM link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.3.3) rdf (>= 2.2, < 4.0) + redcarpet (3.5.0) redis (4.1.2) redis-actionpack (5.0.2) actionpack (>= 4.0, < 6) @@ -701,6 +703,7 @@ DEPENDENCIES json-ld (~> 3.0) json-ld-preloaded (~> 3.0) kaminari (~> 1.1) + kramdown (~> 2.1.0) letter_opener (~> 1.7) letter_opener_web (~> 1.3) link_header (~> 0.0) @@ -739,6 +742,7 @@ DEPENDENCIES rails-i18n (~> 5.1) rails-settings-cached (~> 0.6) rdf-normalize (~> 0.3) + redcarpet (~> 3.5.0) redis (~> 4.1) redis-namespace (~> 1.5) redis-rails (~> 5.0) diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 1a0dab99..43059ebf 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -5,8 +5,8 @@ class Api::V1::StatusesController < Api::BaseController before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy] before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy] - before_action :require_user!, except: [:show, :context, :card] - before_action :set_status, only: [:show, :context, :card, :update, :revisions] + before_action :require_user!, except: [:show, :comments, :context, :card] + before_action :set_status, only: [:show, :comments, :context, :card, :update, :revisions] respond_to :json @@ -14,13 +14,24 @@ class Api::V1::StatusesController < Api::BaseController # breaking backwards-compatibility. Arbitrarily high number to cover most # conversations as quasi-unlimited, it would be too much work to render more # than this anyway - CONTEXT_LIMIT = 4_096 + # : TODO : + CONTEXT_LIMIT = 4_096 def show @status = cache_collection([@status], Status).first render json: @status, serializer: REST::StatusSerializer end + def comments + descendants_results = @status.descendants(CONTEXT_LIMIT, current_account) + loaded_descendants = cache_collection(descendants_results, Status) + + @context = Context.new(descendants: loaded_descendants) + statuses = [@status] + @context.descendants + + render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) + end + def context ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(CONTEXT_LIMIT, current_account) descendants_results = @status.descendants(CONTEXT_LIMIT, current_account) @@ -42,6 +53,7 @@ class Api::V1::StatusesController < Api::BaseController def create @status = PostStatusService.new.call(current_user.account, text: status_params[:status], + markdown: status_params[: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], @@ -93,6 +105,7 @@ class Api::V1::StatusesController < Api::BaseController def status_params params.permit( :status, + :markdown, :in_reply_to_id, :quote_of_id, :sensitive, diff --git a/app/javascript/gabsocial/actions/compose.js b/app/javascript/gabsocial/actions/compose.js index 2a54b1d3..dddda92f 100644 --- a/app/javascript/gabsocial/actions/compose.js +++ b/app/javascript/gabsocial/actions/compose.js @@ -77,10 +77,12 @@ export const ensureComposeIsVisible = (getState, routerHistory) => { } }; -export function changeCompose(text) { +export function changeCompose(text, markdown) { + console.log("changeCompose:", markdown) return { type: COMPOSE_CHANGE, text: text, + markdown: markdown, }; }; @@ -173,7 +175,7 @@ export function submitCompose(routerHistory, group) { if (!me) return; let status = getState().getIn(['compose', 'text'], ''); - const statusMarkdown = getState().getIn(['compose', 'text_markdown'], ''); + const markdown = getState().getIn(['compose', 'markdown'], ''); const media = getState().getIn(['compose', 'media_attachments']); // : hack : @@ -182,11 +184,13 @@ export function submitCompose(routerHistory, group) { const hasProtocol = match.startsWith('https://') || match.startsWith('http://') return hasProtocol ? match : `http://${match}` }) - // statusMarkdown = statusMarkdown.replace(urlRegex, (match) =>{ + // markdown = statusMarkdown.replace(urlRegex, (match) =>{ // const hasProtocol = match.startsWith('https://') || match.startsWith('http://') // return hasProtocol ? match : `http://${match}` // }) + console.log("markdown:", markdown) + dispatch(submitComposeRequest()); dispatch(closeModal()); @@ -202,7 +206,7 @@ export function submitCompose(routerHistory, group) { api(getState)[method](endpoint, { status, - // statusMarkdown, + markdown, scheduled_at, in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), quote_of_id: getState().getIn(['compose', 'quote_of_id'], null), diff --git a/app/javascript/gabsocial/actions/importer/index.js b/app/javascript/gabsocial/actions/importer/index.js index 768535ad..9953a74e 100644 --- a/app/javascript/gabsocial/actions/importer/index.js +++ b/app/javascript/gabsocial/actions/importer/index.js @@ -1,4 +1,5 @@ import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer'; +import { fetchContext } from '../statuses' export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; @@ -78,6 +79,10 @@ export function importFetchedStatuses(statuses) { if (status.poll && status.poll.id) { pushUnique(polls, normalizePoll(status.poll)); } + + // if (status.replies_count > 0) { + // dispatch(fetchComments(status.id)); + // } } statuses.forEach(processStatus); diff --git a/app/javascript/gabsocial/actions/importer/normalizer.js b/app/javascript/gabsocial/actions/importer/normalizer.js index bfc4bc4b..c542523f 100644 --- a/app/javascript/gabsocial/actions/importer/normalizer.js +++ b/app/javascript/gabsocial/actions/importer/normalizer.js @@ -62,9 +62,10 @@ export function normalizeStatus(status, normalOldStatus) { const spoilerText = normalStatus.spoiler_text || ''; const searchContent = [spoilerText, status.content].join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); const emojiMap = makeEmojiMap(normalStatus); + const theContent = !!normalStatus.rich_content ? normalStatus.rich_content : normalStatus.content; normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; - normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); + normalStatus.contentHtml = emojify(theContent, emojiMap); normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive; } diff --git a/app/javascript/gabsocial/actions/statuses.js b/app/javascript/gabsocial/actions/statuses.js index c993c640..83d59512 100644 --- a/app/javascript/gabsocial/actions/statuses.js +++ b/app/javascript/gabsocial/actions/statuses.js @@ -18,6 +18,10 @@ export const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST'; export const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS'; export const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL'; +export const COMMENTS_FETCH_REQUEST = 'COMMENTS_FETCH_REQUEST'; +export const COMMENTS_FETCH_SUCCESS = 'COMMENTS_FETCH_SUCCESS'; +export const COMMENTS_FETCH_FAIL = 'COMMENTS_FETCH_FAIL'; + export const STATUS_MUTE_REQUEST = 'STATUS_MUTE_REQUEST'; export const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS'; export const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL'; @@ -85,8 +89,6 @@ export function fetchStatus(id) { return (dispatch, getState) => { const skipLoading = getState().getIn(['statuses', id], null) !== null; - dispatch(fetchContext(id)); - if (skipLoading) { return; } @@ -205,6 +207,24 @@ export function fetchContext(id) { }; }; +export function fetchComments(id) { + return (dispatch, getState) => { + dispatch(fetchCommentsRequest(id)); + + api(getState).get(`/api/v1/statuses/${id}/comments`).then(response => { + dispatch(importFetchedStatuses(response.data.descendants)); + dispatch(fetchCommentsSuccess(id, response.data.descendants)); + + }).catch(error => { + if (error.response && error.response.status === 404) { + dispatch(deleteFromTimelines(id)); + } + + dispatch(fetchCommentsFail(id, error)); + }); + }; +}; + export function fetchContextRequest(id) { return { type: CONTEXT_FETCH_REQUEST, @@ -231,6 +251,30 @@ export function fetchContextFail(id, error) { }; }; +export function fetchCommentsRequest(id) { + return { + type: COMMENTS_FETCH_REQUEST, + id, + }; +}; + +export function fetchCommentsSuccess(id, descendants) { + return { + type: COMMENTS_FETCH_SUCCESS, + id, + descendants, + }; +}; + +export function fetchCommentsFail(id, error) { + return { + type: COMMENTS_FETCH_FAIL, + id, + error, + skipAlert: true, + }; +}; + export function muteStatus(id) { return (dispatch, getState) => { if (!me) return; diff --git a/app/javascript/gabsocial/components/account.js b/app/javascript/gabsocial/components/account.js index 0c2421fb..a6df881e 100644 --- a/app/javascript/gabsocial/components/account.js +++ b/app/javascript/gabsocial/components/account.js @@ -92,7 +92,7 @@ class Account extends ImmutablePureComponent { onMute: PropTypes.func.isRequired, onMuteNotifications: PropTypes.func, intl: PropTypes.object.isRequired, - hidden: PropTypes.bool, + isHidden: PropTypes.bool, actionIcon: PropTypes.string, actionTitle: PropTypes.string, onActionClick: PropTypes.func, @@ -134,7 +134,7 @@ class Account extends ImmutablePureComponent { const { account, intl, - hidden, + isHidden, onActionClick, actionIcon, actionTitle, @@ -146,7 +146,7 @@ class Account extends ImmutablePureComponent { if (!account) return null - if (hidden) { + if (isHidden) { return ( {account.get('display_name')} @@ -207,7 +207,6 @@ class Account extends ImmutablePureComponent { const dismissBtn = ( 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 6ffbc182..36016553 100644 --- a/app/javascript/gabsocial/features/compose/containers/compose_form_container.js +++ b/app/javascript/gabsocial/features/compose/containers/compose_form_container.js @@ -46,8 +46,8 @@ const mapStateToProps = (state, { replyToId }) => { const mapDispatchToProps = (dispatch) => ({ - onChange(text) { - dispatch(changeCompose(text)) + onChange(text, markdown) { + dispatch(changeCompose(text, markdown)) }, onSubmit(router, group) { diff --git a/app/javascript/gabsocial/features/lists_directory.js b/app/javascript/gabsocial/features/lists_directory.js index a5c6aa3f..7559927f 100644 --- a/app/javascript/gabsocial/features/lists_directory.js +++ b/app/javascript/gabsocial/features/lists_directory.js @@ -33,16 +33,19 @@ export default class ListsDirectory extends ImmutablePureComponent { static propTypes = { - params: PropTypes.object.isRequired, - onFetchLists: PropTypes.func.isRequired, - lists: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, + lists: ImmutablePropTypes.list, + onFetchLists: PropTypes.func.isRequired, } state = { fetched: false, } + updateOnProps = [ + 'lists' + ] + componentWillMount() { this.props.onFetchLists() .then(() => this.setState({ fetched: true })) diff --git a/app/javascript/gabsocial/features/status/containers/detailed_status_container.js b/app/javascript/gabsocial/features/status/containers/detailed_status_container.js deleted file mode 100644 index a9f207bc..00000000 --- a/app/javascript/gabsocial/features/status/containers/detailed_status_container.js +++ /dev/null @@ -1,162 +0,0 @@ -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { - replyCompose, - mentionCompose, -} from '../../../actions/compose'; -import { - reblog, - favorite, - unreblog, - unfavorite, - pin, - unpin, -} from '../../../actions/interactions'; -import { blockAccount } from '../../../actions/accounts'; -import { - muteStatus, - unmuteStatus, - deleteStatus, - hideStatus, - revealStatus, -} from '../../../actions/statuses'; -import { initMuteModal } from '../../../actions/mutes'; -import { initReport } from '../../../actions/reports'; -import { openModal } from '../../../actions/modal'; -// import { showAlertForError } from '../../../actions/alerts'; -import { boostModal, deleteModal } from '../../../initial_state'; -import { makeGetStatus } from '../../../selectors'; -import DetailedStatus from '../components/detailed_status'; - -const messages = defineMessages({ - deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, - deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, - replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, - replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, - blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, -}); - -const makeMapStateToProps = () => { - const getStatus = makeGetStatus(); - - const mapStateToProps = (state, props) => ({ - status: getStatus(state, props), - domain: state.getIn(['meta', 'domain']), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { intl }) => ({ - - onReply (status, router) { - dispatch((_, getState) => { - const state = getState(); - if (state.getIn(['compose', 'text']).trim().length !== 0) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onConfirm: () => dispatch(replyCompose(status, router)), - })); - } else { - dispatch(replyCompose(status, router)); - } - }); - }, - - onModalRepost (status) { - dispatch(repost(status)); - }, - - onRepost (status, e) { - if (status.get('reblogged')) { - dispatch(unrepost(status)); - } else { - if (e.shiftKey || !boostModal) { - this.onModalRepost(status); - } else { - dispatch(openModal('BOOST', { status, onRepost: this.onModalRepost })); - } - } - }, - - onFavorite (status) { - if (status.get('favourited')) { - dispatch(unfavorite(status)); - } else { - dispatch(favorite(status)); - } - }, - - onPin (status) { - if (status.get('pinned')) { - dispatch(unpin(status)); - } else { - dispatch(pin(status)); - } - }, - - onEmbed (status) { - // dispatch(openModal('EMBED', { - // url: status.get('url'), - // onError: error => dispatch(showAlertForError(error)), - // })); - }, - - onDelete (status, history) { - if (!deleteModal) { - dispatch(deleteStatus(status.get('id'), history)); - } else { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.deleteMessage), - confirm: intl.formatMessage(messages.deleteConfirm), - onConfirm: () => dispatch(deleteStatus(status.get('id'), history)), - })); - } - }, - - onMention (account, router) { - dispatch(mentionCompose(account, router)); - }, - - onOpenMedia (media, index) { - dispatch(openModal('MEDIA', { media, index })); - }, - - onOpenVideo (media, time) { - dispatch(openModal('VIDEO', { media, time })); - }, - - onBlock (status) { - const account = status.get('account') - dispatch(openModal('BLOCK_ACCOUNT', { - accountId: account.get('id'), - })) - }, - - onReport (status) { - dispatch(initReport(status.get('account'), status)); - }, - - onMute (account) { - dispatch(initMuteModal(account)); - }, - - onMuteConversation (status) { - if (status.get('muted')) { - dispatch(unmuteStatus(status.get('id'))); - } else { - dispatch(muteStatus(status.get('id'))); - } - }, - - onToggleHidden (status) { - if (status.get('hidden')) { - dispatch(revealStatus(status.get('id'))); - } else { - dispatch(hideStatus(status.get('id'))); - } - }, - -}); - -export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(DetailedStatus)); diff --git a/app/javascript/gabsocial/features/status/index.js b/app/javascript/gabsocial/features/status/index.js deleted file mode 100644 index 7c8b359d..00000000 --- a/app/javascript/gabsocial/features/status/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './status' \ No newline at end of file diff --git a/app/javascript/gabsocial/features/status/status.js b/app/javascript/gabsocial/features/status/status.js deleted file mode 100644 index 94ff2ca3..00000000 --- a/app/javascript/gabsocial/features/status/status.js +++ /dev/null @@ -1,483 +0,0 @@ -import Immutable from 'immutable' -import ImmutablePropTypes from 'react-immutable-proptypes' -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl' -import ImmutablePureComponent from 'react-immutable-pure-component' -import { HotKeys } from 'react-hotkeys' -import { fetchStatus } from '../../actions/statuses' -import { - favorite, - unfavorite, - repost, - unrepost, - pin, - unpin, -} from '../../actions/interactions' -import { - replyCompose, - mentionCompose, -} from '../../actions/compose' -import { blockAccount } from '../../actions/accounts' -import { - muteStatus, - unmuteStatus, - deleteStatus, - hideStatus, - revealStatus, -} from '../../actions/statuses' -import { initMuteModal } from '../../actions/mutes' -import { initReport } from '../../actions/reports' -import { openModal } from '../../actions/modal' -import { boostModal, deleteModal, me } from '../../initial_state' -import { makeGetStatus } from '../../selectors' -import StatusContainer from '../../containers/status_container' -import { textForScreenReader, defaultMediaVisibility } from '../../components/status' -import ColumnIndicator from '../../components/column_indicator' -import Block from '../../components/block' -import CommentList from '../../components/comment_list' - -const messages = defineMessages({ - deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, - deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, - redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, - redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.' }, - revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' }, - hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' }, - detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, - replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, - replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, - blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, -}); - -const makeMapStateToProps = () => { - const getStatus = makeGetStatus() - - const mapStateToProps = (state, props) => { - const statusId = props.id || props.params.statusId - const username = props.params ? props.params.username : undefined - - const status = getStatus(state, { - id: statusId, - username: username, - }) - - // : todo : if is comment (i.e. if any ancestorsIds) use comment not status - - let ancestorsIds = Immutable.List() - let descendantsIds = Immutable.List() - - if (status) { - // ancestorsIds = ancestorsIds.withMutations(mutable => { - // let id = status.get('in_reply_to_id'); - - // while (id) { - // mutable.unshift(id); - // id = state.getIn(['contexts', 'inReplyTos', id]); - // } - // }); - - // // ONLY Direct descendants - // descendantsIds = state.getIn(['contexts', 'replies', status.get('id')]) - - let indent = -1 - descendantsIds = descendantsIds.withMutations(mutable => { - const ids = [status.get('id')] - - while (ids.length > 0) { - let id = ids.shift(); - const replies = state.getIn(['contexts', 'replies', id]) - - if (status.get('id') !== id) { - mutable.push(Immutable.Map({ - statusId: id, - indent: indent, - })) - } - - if (replies) { - replies.reverse().forEach(reply => { - ids.unshift(reply) - }); - indent++ - indent = Math.min(2, indent) - } - } - }) - } - - // console.log("descendantsIds:", descendantsIds) - - return { - status, - ancestorsIds, - descendantsIds, - askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0, - }; - }; - - return mapStateToProps; -}; - -export default -@injectIntl -@connect(makeMapStateToProps) -class Status extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - status: ImmutablePropTypes.map, - ancestorsIds: ImmutablePropTypes.list, - descendantsIds: ImmutablePropTypes.list, - intl: PropTypes.object.isRequired, - askReplyConfirmation: PropTypes.bool, - contextType: PropTypes.string, - }; - - state = { - fullscreen: false, - showMedia: defaultMediaVisibility(this.props.status), - loadedStatusId: undefined, - }; - - componentWillMount() { - const statusId = this.props.id || this.props.params.statusId - // console.log("statusId:", statusId) - this.props.dispatch(fetchStatus(statusId)); - } - - componentWillReceiveProps(nextProps) { - const statusId = this.props.id || this.props.params.statusId - const nextStatusId = nextProps.id || nextProps.params.statusId - - if (nextStatusId !== statusId && nextStatusId) { - this._scrolledIntoView = false; - this.props.dispatch(fetchStatus(nextStatusId)); - } - - if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) { - this.setState({ showMedia: defaultMediaVisibility(nextProps.status), loadedStatusId: nextProps.status.get('id') }); - } - } - - handleToggleMediaVisibility = () => { - this.setState({ showMedia: !this.state.showMedia }); - } - - handleFavoriteClick = (status) => { - if (status.get('favourited')) { - this.props.dispatch(unfavorite(status)); - } else { - this.props.dispatch(favorite(status)); - } - } - - handlePin = (status) => { - if (status.get('pinned')) { - this.props.dispatch(unpin(status)); - } else { - this.props.dispatch(pin(status)); - } - } - - handleReplyClick = (status) => { - let { askReplyConfirmation, dispatch, intl } = this.props; - if (askReplyConfirmation) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onConfirm: () => dispatch(replyCompose(status, this.context.router.history)), - })); - } else { - dispatch(replyCompose(status, this.context.router.history)); - } - } - - handleModalRepost = (status) => { - this.props.dispatch(repost(status)); - } - - handleRepostClick = (status, e) => { - if (status.get('reblogged')) { - this.props.dispatch(unrepost(status)); - } else { - if ((e && e.shiftKey) || !boostModal) { - this.handleModalRepost(status); - } else { - this.props.dispatch(openModal('BOOST', { status, onRepost: this.handleModalRepost })); - } - } - } - - handleDeleteClick = (status, history, withRedraft = false) => { - const { dispatch, intl } = this.props; - - if (!deleteModal) { - dispatch(deleteStatus(status.get('id'), history, withRedraft)); - } else { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), - confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), - onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), - })); - } - } - - handleMentionClick = (account, router) => { - this.props.dispatch(mentionCompose(account, router)); - } - - handleOpenMedia = (media, index) => { - this.props.dispatch(openModal('MEDIA', { media, index })); - } - - handleOpenVideo = (media, time) => { - this.props.dispatch(openModal('VIDEO', { media, time })); - } - - handleMuteClick = (account) => { - this.props.dispatch(initMuteModal(account)); - } - - handleConversationMuteClick = (status) => { - if (status.get('muted')) { - this.props.dispatch(unmuteStatus(status.get('id'))); - } else { - this.props.dispatch(muteStatus(status.get('id'))); - } - } - - handleToggleHidden = (status) => { - if (status.get('hidden')) { - this.props.dispatch(revealStatus(status.get('id'))); - } else { - this.props.dispatch(hideStatus(status.get('id'))); - } - } - - handleToggleAll = () => { - const { status, ancestorsIds, descendantsIds } = this.props; - const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS()); - - if (status.get('hidden')) { - this.props.dispatch(revealStatus(statusIds)); - } else { - this.props.dispatch(hideStatus(statusIds)); - } - } - - handleBlockClick = (status) => { - const { dispatch } = this.props - const account = status.get('account') - - dispatch(openModal('BLOCK_ACCOUNT', { - accountId: account.get('id'), - })) - } - - handleReport = (status) => { - this.props.dispatch(initReport(status.get('account'), status)); - } - - handleEmbed = (status) => { - this.props.dispatch(openModal('EMBED', { url: status.get('url') })); - } - - handleHotkeyMoveUp = () => { - this.handleMoveUp(this.props.status.get('id')); - } - - handleHotkeyMoveDown = () => { - this.handleMoveDown(this.props.status.get('id')); - } - - handleHotkeyReply = e => { - e.preventDefault(); - this.handleReplyClick(this.props.status); - } - - handleHotkeyFavorite = () => { - this.handleFavoriteClick(this.props.status); - } - - handleHotkeyBoost = () => { - this.handleRepostClick(this.props.status); - } - - handleHotkeyMention = e => { - e.preventDefault(); - this.handleMentionClick(this.props.status.get('account')); - } - - handleHotkeyOpenProfile = () => { - this.context.router.history.push(`/${this.props.status.getIn(['account', 'acct'])}`); - } - - handleHotkeyToggleHidden = () => { - this.handleToggleHidden(this.props.status); - } - - handleHotkeyToggleSensitive = () => { - this.handleToggleMediaVisibility(); - } - - handleMoveUp = id => { - const { status, ancestorsIds, descendantsIds } = this.props; - - if (id === status.get('id')) { - this._selectChild(ancestorsIds.size - 1, true); - } else { - let index = ancestorsIds.indexOf(id); - - if (index === -1) { - index = descendantsIds.indexOf(id); - this._selectChild(ancestorsIds.size + index, true); - } else { - this._selectChild(index - 1, true); - } - } - } - - handleMoveDown = id => { - const { status, ancestorsIds, descendantsIds } = this.props; - - if (id === status.get('id')) { - this._selectChild(ancestorsIds.size + 1, false); - } else { - let index = ancestorsIds.indexOf(id); - - if (index === -1) { - index = descendantsIds.indexOf(id); - this._selectChild(ancestorsIds.size + index + 2, false); - } else { - this._selectChild(index + 1, false); - } - } - } - - _selectChild(index, align_top) { - const container = this.node; - const element = container.querySelectorAll('.focusable')[index]; - - if (element) { - if (align_top && container.scrollTop > element.offsetTop) { - element.scrollIntoView(true); - } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) { - element.scrollIntoView(false); - } - element.focus(); - } - } - - renderChildren(list) { - // console.log("list:", list) - return null - // : todo : comments - return list.map(id => ( - - )) - } - - setRef = c => { - this.node = c; - } - - componentDidUpdate() { - if (this._scrolledIntoView) return - - const { status, ancestorsIds } = this.props - - if (status && ancestorsIds && ancestorsIds.size > 0) { - const element = this.node.querySelectorAll('.focusable')[ancestorsIds.size - 1]; - - window.requestAnimationFrame(() => { - element.scrollIntoView(true); - }); - this._scrolledIntoView = true; - } - } - - render() { - const { - status, - ancestorsIds, - descendantsIds, - intl, - contextType, - commentsLimited, - } = this.props - - let ancestors, descendants - - if (status === null) { - return - } - - // if (ancestorsIds && ancestorsIds.size > 0) { - // ancestors = this.renderChildren(ancestorsIds) - // } - - if (descendantsIds && descendantsIds.size > 0) { - descendants = this.renderChildren(descendantsIds) - } - - const handlers = { - moveUp: this.handleHotkeyMoveUp, - moveDown: this.handleHotkeyMoveDown, - reply: this.handleHotkeyReply, - favorite: this.handleHotkeyFavorite, - boost: this.handleHotkeyBoost, - mention: this.handleHotkeyMention, - openProfile: this.handleHotkeyOpenProfile, - toggleHidden: this.handleHotkeyToggleHidden, - toggleSensitive: this.handleHotkeyToggleSensitive, - }; - - console.log("descendantsIds.size > 0:", descendantsIds.size > 0) - - return ( -

- - { - /* : todo : ancestors if is comment */ - } - - -
- - - -
-
- - { - descendantsIds && descendantsIds.size > 0 && -
- } - - - -
- ) - } - -} diff --git a/app/javascript/gabsocial/features/ui/util/async_components.js b/app/javascript/gabsocial/features/ui/util/async_components.js index f340a34c..5d4b4f72 100644 --- a/app/javascript/gabsocial/features/ui/util/async_components.js +++ b/app/javascript/gabsocial/features/ui/util/async_components.js @@ -29,6 +29,6 @@ export function Notifications() { return import(/* webpackChunkName: "features/n export function Reposts() { return import(/* webpackChunkName: "features/reposts" */'../../reposts') } export function ReportModal() { return import(/* webpackChunkName: "modals/report_modal" */'../../../components/modal/report_modal') } export function Search() { return import(/*webpackChunkName: "features/search" */'../../search') } -export function Status() { return import(/* webpackChunkName: "features/status" */'../../status') } +export function Status() { return import(/* webpackChunkName: "components/status" */'../../../components/status') } export function StatusRevisionsModal() { return import(/* webpackChunkName: "modals/status_revisions_modal" */'../../../components/modal/status_revisions_modal') } export function Video() { return import(/* webpackChunkName: "components/video" */'../../../components/video') } diff --git a/app/javascript/gabsocial/features/ui/util/page_title.js b/app/javascript/gabsocial/features/ui/util/page_title.js index f7502b60..1eb9a0ba 100644 --- a/app/javascript/gabsocial/features/ui/util/page_title.js +++ b/app/javascript/gabsocial/features/ui/util/page_title.js @@ -2,11 +2,11 @@ import isEqual from 'lodash.isequal' export default class PageTitle extends PureComponent { static propTypes = { - badge: PropTypes.oneOf([ + badge: PropTypes.oneOfType([ PropTypes.number, PropTypes.string, ]), - path: PropTypes.oneOf([ + path: PropTypes.oneOfType([ PropTypes.sting, PropTypes.array, ]), diff --git a/app/javascript/gabsocial/reducers/compose.js b/app/javascript/gabsocial/reducers/compose.js index 73fa7787..94395264 100644 --- a/app/javascript/gabsocial/reducers/compose.js +++ b/app/javascript/gabsocial/reducers/compose.js @@ -54,7 +54,7 @@ const initialState = ImmutableMap({ spoiler_text: '', privacy: null, text: '', - markdown_text: '', + markdown: null, focusDate: null, caretPosition: null, preselectDate: null, @@ -248,6 +248,7 @@ export default function compose(state = initialState, action) { case COMPOSE_CHANGE: return state .set('text', action.text) + .set('markdown', action.markdown) .set('idempotencyKey', uuid()); case COMPOSE_COMPOSING_CHANGE: return state.set('is_composing', action.value); diff --git a/app/javascript/gabsocial/reducers/contexts.js b/app/javascript/gabsocial/reducers/contexts.js index bcc42ea4..139effcc 100644 --- a/app/javascript/gabsocial/reducers/contexts.js +++ b/app/javascript/gabsocial/reducers/contexts.js @@ -1,16 +1,19 @@ import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS, -} from '../actions/accounts'; -import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; -import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import compareId from '../utils/compare_id'; +} from '../actions/accounts' +import { + COMMENTS_FETCH_SUCCESS, + CONTEXT_FETCH_SUCCESS, +} from '../actions/statuses' +import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines' +import { Map as ImmutableMap, List as ImmutableList } from 'immutable' +import compareId from '../utils/compare_id' const initialState = ImmutableMap({ inReplyTos: ImmutableMap(), replies: ImmutableMap(), -}); +}) const normalizeContext = (immutableState, id, ancestors, descendants) => immutableState.withMutations(state => { state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => { @@ -96,6 +99,8 @@ export default function replies(state = initialState, action) { return filterContexts(state, action.relationship, action.statuses); case CONTEXT_FETCH_SUCCESS: return normalizeContext(state, action.id, action.ancestors, action.descendants); + case COMMENTS_FETCH_SUCCESS: + return normalizeContext(state, action.id, ImmutableList(), action.descendants); case TIMELINE_DELETE: return deleteFromContexts(state, [action.id]); case TIMELINE_UPDATE: diff --git a/app/javascript/styles/global.css b/app/javascript/styles/global.css index 5007aaf5..959ed041 100644 --- a/app/javascript/styles/global.css +++ b/app/javascript/styles/global.css @@ -24,6 +24,37 @@ body { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif; } +.statusContent p { + min-height: 18px; +} + +.statusContent em { + font-style: italic; +} + +.statusContent strong { + font-weight: 700; +} + +.statusContent strike, +.statusContent del { + text-decoration: line-through; +} + +.statusContent h1 { + font-size: 1.5rem; + font-weight: 700; + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.statusContent ul, +.statusContent ol { + list-style-type: disc; + padding-left: 40px; + margin: 0; +} + .dangerousContent, .dangerousContent * { margin-top: 0; @@ -144,6 +175,10 @@ body { overflow-y: hidden; } +.overflowYHidden { + overflow-y: hidden; +} + .textOverflowEllipsis { max-width: 100%; overflow-x: hidden; @@ -1079,10 +1114,14 @@ body { display: none; } +/* :global(.public-DraftEditorPlaceholder-inner) { + font-weight: 400; + font-size: 16px; +} */ + :global(.RichEditor-blockquote) { border-left: 5px solid #eee; color: #666; - font-family: 'Hoefler Text', 'Georgia', serif; font-style: italic; margin: 16px 0; padding: 10px 20px; diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 1a38d2f4..78f6c3a9 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -2,6 +2,7 @@ require 'singleton' require_relative './sanitize_config' +require 'redcarpet' class Formatter include Singleton @@ -9,12 +10,24 @@ class Formatter include ActionView::Helpers::TextHelper + class CustomRender < Redcarpet::Render::HTML + def paragraph(text) + %(

#{text}

) + end + end + def format(status, **options) - raw_content = status.text - raw_content = ActionController::Base.helpers.strip_tags(raw_content) if status.id <= 11063737261633602 # #TODO: Migration fix + if options[:use_markdown] + raw_content = status.markdown + return '' if raw_content.blank? + else + raw_content = status.text + end + + raw_content = ActionController::Base.helpers.strip_tags(raw_content) if status.id <= 11063737261633602 # #TODO: Migration fix if status.reblog? - status = status.proper + status = status.proper end if options[:inline_poll_options] && status.preloadable_poll @@ -33,9 +46,33 @@ class Formatter linkable_accounts << status.account html = raw_content + + # puts "BOLLI 1: " + html + html = encode_and_link_urls(html, linkable_accounts) + + # puts "BOLLI 2: " + html + + if options[:use_markdown] + html = convert_headers(html) + html = convert_strong(html) + html = convert_italic(html) + html = convert_strikethrough(html) + html = convert_code(html) + html = convert_codeblock(html) + html = convert_links(html) + html = convert_lists(html) + html = convert_ordered_lists(html) + # puts "BOLLI 3: " + html + end + html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify] + # puts "BOLLI 4: " + html + html = simple_format(html, {}, sanitize: false) + + # puts "BOLLI 5: " + html + html = html.delete("\n") html.html_safe # rubocop:disable Rails/OutputSafety @@ -296,4 +333,80 @@ class Formatter def mention_html(account) "@#{encode(account.acct)}" end + + def convert_headers(html) + html.gsub(/^\#{1,6}.*$/) do |header| + weight = 0 + header.split('').each do |char| + break unless char == '#' + weight += 1 + end + content = header.sub(/^\#{1,6}/, '') + "#{content}" + end + end + + def convert_strong(html) + html.gsub(/\*{2}.*\*{2}|_{2}.*_{2}/) do |strong| + content = strong.gsub(/\*{2}|_{2}/, '') + "#{content}" + end + end + + def convert_italic(html) + html.gsub(/\*{1}(\w|\s)+\*{1}|_{1}(\w|\s)+_{1}/) do |italic| + content = italic.gsub(/\*{1}|_{1}/, '') + "#{content}" + end + end + + def convert_strikethrough(html) + html.gsub(/~~(\w|\s)+~~/) do |strike| + content = strike.gsub(/~~/, '') + "#{content}" + end + end + + def convert_code(html) + html.gsub(/`(\w|\s)+`/) do |code| + content = code.gsub(/`/, '') + "#{content}" + end + end + + def convert_codeblock(html) + html.gsub(/```\w*(.*(\r\n|\r|\n))+```/) do |code| + lang = code.match(/```\w+/)[0].gsub(/`/, '') + content = code.gsub(/```\w+/, '```').gsub(/`/, '') + "
#{content}
" + end + end + + def convert_links(html) + html.gsub(/\[(\w|\s)+\]\((\w|\W)+\)/) do |anchor| + link_text = anchor.match(/\[(\w|\s)+\]/)[0].gsub(/[\[\]]/, '') + href = anchor.match(/\((\w|\W)+\)/)[0].gsub(/\(|\)/, '') + "#{link_text}" + end + end + + def convert_lists(html) + html.gsub(/(\-.+(\r|\n|\r\n))+/) do |list| + items = "
    \n" + list.gsub(/\-.+/) do |li| + items << "
  • #{li.sub(/^\-/, '').strip}
  • \n" + end + items << "
\n" + end + end + + def convert_ordered_lists(html) + html.gsub(/(\d\..+(\r|\n|\r\n))+/) do |list| + items = "
    \n" + list.gsub(/\d.+/) do |li| + items << "
  1. #{li.sub(/^\d\./, '').strip}
  2. \n" + end + items << "
\n" + end + end end diff --git a/app/models/status.rb b/app/models/status.rb index c46d7bb1..208aa9af 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -6,6 +6,7 @@ # id :bigint(8) not null, primary key # uri :string # text :text default(""), not null +# markdown :text # created_at :datetime not null # updated_at :datetime not null # in_reply_to_id :bigint(8) diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index a875c718..8685b1a2 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -12,6 +12,7 @@ class REST::StatusSerializer < ActiveModel::Serializer attribute :pinned, if: :pinnable? attribute :content, unless: :source_requested? + attribute :rich_content, unless: :source_requested? attribute :text, if: :source_requested? belongs_to :reblog, serializer: REST::StatusSerializer @@ -71,6 +72,10 @@ class REST::StatusSerializer < ActiveModel::Serializer Formatter.instance.format(object).strip end + def rich_content + Formatter.instance.format(object, use_markdown: true).strip + end + def url TagManager.instance.url_for(object) end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 3a7cfea8..b50b2f00 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -9,6 +9,7 @@ class PostStatusService < BaseService # @param [Account] account Account from which to post # @param [Hash] options # @option [String] :text Message + # @option [String] :markdown Optional message in markdown # @option [Status] :thread Optional status to reply to # @option [Boolean] :sensitive # @option [String] :visibility @@ -25,6 +26,7 @@ class PostStatusService < BaseService @account = account @options = options @text = @options[:text] || '' + @markdown = @options[:markdown] @in_reply_to = @options[:thread] return idempotency_duplicate if idempotency_given? && idempotency_duplicate? @@ -171,6 +173,7 @@ class PostStatusService < BaseService def status_attributes { text: @text, + markdown: @markdown, group_id: @options[:group_id], quote_of_id: @options[:quote_of_id], media_attachments: @media || [], diff --git a/config/routes.rb b/config/routes.rb index d1b88c98..d92c5fce 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -311,6 +311,7 @@ Rails.application.routes.draw do end member do + get :comments get :context get :card get :revisions diff --git a/db/migrate/20200420223346_add_markdown_to_status.rb b/db/migrate/20200420223346_add_markdown_to_status.rb new file mode 100644 index 00000000..79dca79a --- /dev/null +++ b/db/migrate/20200420223346_add_markdown_to_status.rb @@ -0,0 +1,5 @@ +class AddMarkdownToStatus < ActiveRecord::Migration[5.2] + def change + add_column :statuses, :markdown, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index 2570abba..776f381f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_03_10_224203) do +ActiveRecord::Schema.define(version: 2020_04_20_223346) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -711,6 +711,7 @@ ActiveRecord::Schema.define(version: 2020_03_10_224203) do t.integer "group_id" t.bigint "quote_of_id" t.datetime "revised_at" + t.text "markdown" t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20180106", order: { id: :desc } t.index ["group_id"], name: "index_statuses_on_group_id" t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id" diff --git a/package.json b/package.json index be587178..b889e170 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "detect-passive-events": "^1.0.2", "dotenv": "^8.0.0", "draft-js": "^0.11.4", + "draftjs-to-markdown": "^0.6.0", "emoji-mart": "^3.0.0", "es6-symbol": "^3.1.1", "escape-html": "^1.0.3", @@ -113,6 +114,7 @@ "lodash.throttle": "^4.1.1", "lodash.unescape": "^4.0.1", "mark-loader": "^0.1.6", + "markdown-draft-js": "^2.2.0", "mini-css-extract-plugin": "^0.9.0", "mkdirp": "^0.5.1", "npmlog": "^4.1.2", diff --git a/yarn.lock b/yarn.lock index 4078aad2..cdf1e070 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1472,7 +1472,7 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" -argparse@^1.0.7: +argparse@^1.0.10, argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== @@ -1640,6 +1640,13 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +autolinker@^3.11.0: + version "3.14.1" + resolved "https://registry.yarnpkg.com/autolinker/-/autolinker-3.14.1.tgz#6ae4b812b6eaf42d4d68138b9e67757cbf2bc1e4" + integrity sha512-yvsRHIaY51EYDml6MGlbqyJGfl4n7zezGYf+R7gvM8c5LNpRGc4SISkvgAswSS8SWxk/OrGCylKV9mJyVstz7w== + dependencies: + tslib "^1.9.3" + autoprefixer@^9.5.1: version "9.7.6" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.6.tgz#63ac5bbc0ce7934e6997207d5bb00d68fa8293a4" @@ -3260,6 +3267,11 @@ draft-js@^0.11.4: immutable "~3.7.4" object-assign "^4.1.1" +draftjs-to-markdown@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/draftjs-to-markdown/-/draftjs-to-markdown-0.6.0.tgz#78ec1850c909a8d97132c211dc4d52345c79cd90" + integrity sha512-nmze89CrGOQCzPLd16i2hYpmVyQhZnZuIuuLK7SCuGCRCYTPSnf6PE2f9ONcvjT357G+yEdEQh3K3Ry5cNVMcQ== + duplexer@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" @@ -6086,6 +6098,13 @@ mark-loader@^0.1.6: resolved "https://registry.yarnpkg.com/mark-loader/-/mark-loader-0.1.6.tgz#0abb477dca7421d70e20128ff6489f5cae8676d5" integrity sha1-CrtHfcp0IdcOIBKP9kifXK6GdtU= +markdown-draft-js@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/markdown-draft-js/-/markdown-draft-js-2.2.0.tgz#1a4796c4d7907761ae417e2176ab858dfaa1166a" + integrity sha512-OW53nd5m7KrnuXjH8jNKB3SQ5KidKD/+pHS+3daVKTsyjsiozghjUxzNH+5JLxzaBECHmJ7itE39RzMgYm5EFQ== + dependencies: + remarkable "2.0.0" + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -8205,6 +8224,14 @@ regjsparser@^0.6.4: dependencies: jsesc "~0.5.0" +remarkable@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/remarkable/-/remarkable-2.0.0.tgz#795f965bede8300362ce51a716edc322d9e7a4ca" + integrity sha512-3gvKFAgL4xmmVRKAMNm6UzDo/rO2gPVkZrWagp6AXEA4JvCcMcRx9aapYbb7AJAmLLvi/u06+EhzqoS7ha9qOg== + dependencies: + argparse "^1.0.10" + autolinker "^3.11.0" + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -9415,7 +9442,7 @@ tryer@^1.0.1: resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== -tslib@^1.9.0: +tslib@^1.9.0, tslib@^1.9.3: version "1.11.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==