diff options
-rw-r--r-- | app/controllers/api/web/embeds_controller.rb | 17 | ||||
-rw-r--r-- | app/javascript/mastodon/features/status/components/action_bar.js | 8 | ||||
-rw-r--r-- | app/javascript/mastodon/features/status/index.js | 5 | ||||
-rw-r--r-- | app/javascript/mastodon/features/ui/components/embed_modal.js | 84 | ||||
-rw-r--r-- | app/javascript/mastodon/features/ui/components/modal_root.js | 2 | ||||
-rw-r--r-- | app/javascript/mastodon/features/ui/util/async-components.js | 4 | ||||
-rw-r--r-- | app/javascript/packs/public.js | 4 | ||||
-rw-r--r-- | app/javascript/styles/components.scss | 61 | ||||
-rw-r--r-- | app/serializers/oembed_serializer.rb | 2 | ||||
-rw-r--r-- | config/routes.rb | 1 |
10 files changed, 186 insertions, 2 deletions
diff --git a/app/controllers/api/web/embeds_controller.rb b/app/controllers/api/web/embeds_controller.rb new file mode 100644 index 00000000000..2ed5161612b --- /dev/null +++ b/app/controllers/api/web/embeds_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Api::Web::EmbedsController < Api::BaseController + respond_to :json + + before_action :require_user! + + def create + status = StatusFinder.new(params[:url]).status + render json: status, serializer: OEmbedSerializer, width: 400 + rescue ActiveRecord::RecordNotFound + oembed = OEmbed::Providers.get(params[:url]) + render json: Oj.dump(oembed.fields) + rescue OEmbed::NotFound + render json: {}, status: :not_found + end +end diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index c4a61467765..9431b11c135 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -16,6 +16,7 @@ const messages = defineMessages({ share: { id: 'status.share', defaultMessage: 'Share' }, pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, + embed: { id: 'status.embed', defaultMessage: 'Embed' }, }); @injectIntl @@ -34,6 +35,7 @@ export default class ActionBar extends React.PureComponent { onMention: PropTypes.func.isRequired, onReport: PropTypes.func, onPin: PropTypes.func, + onEmbed: PropTypes.func, me: PropTypes.number.isRequired, intl: PropTypes.object.isRequired, }; @@ -73,11 +75,17 @@ export default class ActionBar extends React.PureComponent { }); } + handleEmbed = () => { + this.props.onEmbed(this.props.status); + } + render () { const { status, me, intl } = this.props; let menu = []; + menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); + if (me === status.getIn(['account', 'id'])) { if (['public', 'unlisted'].indexOf(status.get('visibility')) !== -1) { menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 84e717a1217..c614f6acb79 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -147,6 +147,10 @@ export default class Status extends ImmutablePureComponent { this.props.dispatch(initReport(status.get('account'), status)); } + handleEmbed = (status) => { + this.props.dispatch(openModal('EMBED', { url: status.get('url') })); + } + renderChildren (list) { return list.map(id => <StatusContainer key={id} id={id} />); } @@ -198,6 +202,7 @@ export default class Status extends ImmutablePureComponent { onMention={this.handleMentionClick} onReport={this.handleReport} onPin={this.handlePin} + onEmbed={this.handleEmbed} /> {descendants} diff --git a/app/javascript/mastodon/features/ui/components/embed_modal.js b/app/javascript/mastodon/features/ui/components/embed_modal.js new file mode 100644 index 00000000000..992aed8a3fa --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/embed_modal.js @@ -0,0 +1,84 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { FormattedMessage, injectIntl } from 'react-intl'; +import axios from 'axios'; + +@injectIntl +export default class EmbedModal extends ImmutablePureComponent { + + static propTypes = { + url: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + } + + state = { + loading: false, + oembed: null, + }; + + componentDidMount () { + const { url } = this.props; + + this.setState({ loading: true }); + + axios.post('/api/web/embed', { url }).then(res => { + this.setState({ loading: false, oembed: res.data }); + + const iframeDocument = this.iframe.contentWindow.document; + + iframeDocument.open(); + iframeDocument.write(res.data.html); + iframeDocument.close(); + + iframeDocument.body.style.margin = 0; + this.iframe.height = iframeDocument.body.scrollHeight + 'px'; + }); + } + + setIframeRef = c => { + this.iframe = c; + } + + handleTextareaClick = (e) => { + e.target.select(); + } + + render () { + const { oembed } = this.state; + + return ( + <div className='modal-root__modal embed-modal'> + <h4><FormattedMessage id='status.embed' defaultMessage='Embed' /></h4> + + <div className='embed-modal__container'> + <p className='hint'> + <FormattedMessage id='embed.instructions' defaultMessage='Embed this status on your website by copying the code below.' /> + </p> + + <input + type='text' + className='embed-modal__html' + readOnly + value={oembed && oembed.html || ''} + onClick={this.handleTextareaClick} + /> + + <p className='hint'> + <FormattedMessage id='embed.preview' defaultMessage='Here is what it will look like:' /> + </p> + + <iframe + className='embed-modal__iframe' + scrolling='no' + frameBorder='0' + ref={this.setIframeRef} + title='preview' + /> + </div> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 5b598bddf3f..5420ba2bd24 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -13,6 +13,7 @@ import { BoostModal, ConfirmationModal, ReportModal, + EmbedModal, } from '../../../features/ui/util/async-components'; const MODAL_COMPONENTS = { @@ -23,6 +24,7 @@ const MODAL_COMPONENTS = { 'CONFIRM': ConfirmationModal, 'REPORT': ReportModal, 'ACTIONS': () => Promise.resolve({ default: ActionsModal }), + 'EMBED': EmbedModal, }; export default class ModalRoot extends React.PureComponent { diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 7f6090d3ac0..0882beb7fe2 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -109,3 +109,7 @@ export function MediaGallery () { export function VideoPlayer () { return import(/* webpackChunkName: "status/video_player" */'../../../components/video_player'); } + +export function EmbedModal () { + return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal'); +} diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index ce12041e66e..7b90aa2b8fe 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -45,6 +45,10 @@ function main() { window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes'); }); }); + + if (window.parent) { + window.parent.postMessage(['setHeight', document.getElementsByTagName('html')[0].scrollHeight], '*'); + } }); delegate(document, '.video-player video', 'click', ({ target }) => { diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 3a6672b9f18..8b932e77c70 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -3099,7 +3099,8 @@ button.icon-button.active i.fa-retweet { } .onboarding-modal, -.error-modal { +.error-modal, +.embed-modal { background: $ui-secondary-color; color: $ui-base-color; border-radius: 8px; @@ -3951,3 +3952,61 @@ noscript { } } } + +.embed-modal__html { + color: $ui-secondary-color; + outline: 0; + box-sizing: border-box; + display: block; + width: 100%; + border: none; + padding: 10px; + font-family: 'mastodon-font-monospace', monospace; + background: $ui-base-color; + color: $ui-primary-color; + font-size: 14px; + margin: 0; + margin-bottom: 15px; + + &::-moz-focus-inner { + border: 0; + } + + &::-moz-focus-inner, + &:focus, + &:active { + outline: 0 !important; + } + + &:focus { + background: lighten($ui-base-color, 4%); + } + + @media screen and (max-width: 600px) { + font-size: 16px; + } +} + +.embed-modal { + h4 { + padding: 30px; + font-weight: 500; + font-size: 16px; + text-align: center; + } + + .hint { + margin-bottom: 15px; + } +} + +.embed-modal__container { + padding: 10px; +} + +.embed-modal__iframe { + width: 100%; + min-width: 400px; + overflow: hidden; + border: 0; +} diff --git a/app/serializers/oembed_serializer.rb b/app/serializers/oembed_serializer.rb index 3b53509537b..4f929304339 100644 --- a/app/serializers/oembed_serializer.rb +++ b/app/serializers/oembed_serializer.rb @@ -39,7 +39,7 @@ class OEmbedSerializer < ActiveModel::Serializer def html attributes = { src: embed_short_account_status_url(object.account, object), - style: 'width: 100%; overflow: hidden', + class: 'mastodon-embed', frameborder: '0', scrolling: 'no', width: width, diff --git a/config/routes.rb b/config/routes.rb index 7f77460688d..63e94fb495b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -237,6 +237,7 @@ Rails.application.routes.draw do namespace :web do resource :settings, only: [:update] + resource :embed, only: [:create] resources :push_subscriptions, only: [:create] do member do put :update |