Conflicts: app/javascript/mastodon/components/status_list.js app/javascript/mastodon/features/notifications/index.js app/javascript/mastodon/features/ui/components/modal_root.js app/javascript/mastodon/features/ui/components/onboarding_modal.js app/javascript/mastodon/features/ui/index.js app/javascript/styles/about.scss app/javascript/styles/accounts.scss app/javascript/styles/components.scss app/presenters/instance_presenter.rb app/services/post_status_service.rb app/services/reblog_service.rb app/views/about/more.html.haml app/views/about/show.html.haml app/views/accounts/_header.html.haml config/webpack/loaders/babel.js spec/controllers/api/v1/accounts/credentials_controller_spec.rbmaster
commit
b9f7bc149b
@ -0,0 +1,15 @@ |
||||
# CODEOWNERS for tootsuite/mastodon |
||||
|
||||
# Translators |
||||
# To add translator, copy these lines, replace `fr` with appropriate language code and replace `@żelipapą` with user's GitHub nickname preceded by `@` sign or e-mail address. |
||||
# /app/javascript/mastodon/locales/fr.json @żelipapą |
||||
# /app/views/user_mailer/*.fr.html.erb @żelipapą |
||||
# /app/views/user_mailer/*.fr.text.erb @żelipapą |
||||
# /config/locales/*.fr.yml @żelipapą |
||||
# /config/locales/fr.yml @żelipapą |
||||
|
||||
/app/javascript/mastodon/locales/pl.json @m4sk1n |
||||
/app/views/user_mailer/*.pl.html.erb @m4sk1n |
||||
/app/views/user_mailer/*.pl.text.erb @m4sk1n |
||||
/config/locales/*.pl.yml @m4sk1n |
||||
/config/locales/pl.yml @m4sk1n |
@ -0,0 +1,36 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class ActivityPub::InboxesController < Api::BaseController |
||||
include SignatureVerification |
||||
|
||||
before_action :set_account |
||||
|
||||
def create |
||||
if signed_request_account |
||||
upgrade_account |
||||
process_payload |
||||
head 201 |
||||
else |
||||
head 202 |
||||
end |
||||
end |
||||
|
||||
private |
||||
|
||||
def set_account |
||||
@account = Account.find_local!(params[:account_username]) if params[:account_username] |
||||
end |
||||
|
||||
def body |
||||
@body ||= request.body.read |
||||
end |
||||
|
||||
def upgrade_account |
||||
return unless signed_request_account.subscribed? |
||||
Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) |
||||
end |
||||
|
||||
def process_payload |
||||
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body.force_encoding('UTF-8')) |
||||
end |
||||
end |
@ -0,0 +1,28 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class Api::V1::Statuses::PinsController < Api::BaseController |
||||
include Authorization |
||||
|
||||
before_action -> { doorkeeper_authorize! :write } |
||||
before_action :require_user! |
||||
before_action :set_status |
||||
|
||||
respond_to :json |
||||
|
||||
def create |
||||
StatusPin.create!(account: current_account, status: @status) |
||||
render json: @status, serializer: REST::StatusSerializer |
||||
end |
||||
|
||||
def destroy |
||||
pin = StatusPin.find_by(account: current_account, status: @status) |
||||
pin&.destroy! |
||||
render json: @status, serializer: REST::StatusSerializer |
||||
end |
||||
|
||||
private |
||||
|
||||
def set_status |
||||
@status = Status.find(params[:status_id]) |
||||
end |
||||
end |
@ -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 |
@ -0,0 +1,18 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class IntentsController < ApplicationController |
||||
def show |
||||
uri = Addressable::URI.parse(params[:uri]) |
||||
|
||||
if uri.scheme == 'web+mastodon' |
||||
case uri.host |
||||
when 'follow' |
||||
return redirect_to authorize_follow_path(acct: uri.query_values['uri'].gsub(/\Aacct:/, '')) |
||||
when 'share' |
||||
return redirect_to share_path(text: uri.query_values['text']) |
||||
end |
||||
end |
||||
|
||||
not_found |
||||
end |
||||
end |
@ -0,0 +1,72 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class Settings::ApplicationsController < ApplicationController |
||||
layout 'admin' |
||||
|
||||
before_action :authenticate_user! |
||||
before_action :set_application, only: [:show, :update, :destroy, :regenerate] |
||||
before_action :prepare_scopes, only: [:create, :update] |
||||
|
||||
def index |
||||
@applications = current_user.applications.page(params[:page]) |
||||
end |
||||
|
||||
def new |
||||
@application = Doorkeeper::Application.new( |
||||
redirect_uri: Doorkeeper.configuration.native_redirect_uri, |
||||
scopes: 'read write follow' |
||||
) |
||||
end |
||||
|
||||
def show; end |
||||
|
||||
def create |
||||
@application = current_user.applications.build(application_params) |
||||
|
||||
if @application.save |
||||
redirect_to settings_applications_path, notice: I18n.t('applications.created') |
||||
else |
||||
render :new |
||||
end |
||||
end |
||||
|
||||
def update |
||||
if @application.update(application_params) |
||||
redirect_to settings_applications_path, notice: I18n.t('generic.changes_saved_msg') |
||||
else |
||||
render :show |
||||
end |
||||
end |
||||
|
||||
def destroy |
||||
@application.destroy |
||||
redirect_to settings_applications_path, notice: I18n.t('applications.destroyed') |
||||
end |
||||
|
||||
def regenerate |
||||
@access_token = current_user.token_for_app(@application) |
||||
@access_token.destroy |
||||
|
||||
redirect_to settings_application_path(@application), notice: I18n.t('applications.token_regenerated') |
||||
end |
||||
|
||||
private |
||||
|
||||
def set_application |
||||
@application = current_user.applications.find(params[:id]) |
||||
end |
||||
|
||||
def application_params |
||||
params.require(:doorkeeper_application).permit( |
||||
:name, |
||||
:redirect_uri, |
||||
:scopes, |
||||
:website |
||||
) |
||||
end |
||||
|
||||
def prepare_scopes |
||||
scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil) |
||||
params[:doorkeeper_application][:scopes] = scopes.join(' ') if scopes.is_a? Array |
||||
end |
||||
end |
@ -0,0 +1,30 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class SharesController < ApplicationController |
||||
layout 'modal' |
||||
|
||||
before_action :authenticate_user! |
||||
before_action :set_body_classes |
||||
|
||||
def show |
||||
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) |
||||
@initial_state_json = serializable_resource.to_json |
||||
end |
||||
|
||||
private |
||||
|
||||
def initial_state_params |
||||
{ |
||||
settings: Web::Setting.find_by(user: current_user)&.data || {}, |
||||
push_subscription: current_account.user.web_push_subscription(current_session), |
||||
current_account: current_account, |
||||
token: current_session.token, |
||||
admin: Account.find_local(Setting.site_contact_username), |
||||
text: params[:text], |
||||
} |
||||
end |
||||
|
||||
def set_body_classes |
||||
@body_classes = 'compose-standalone' |
||||
end |
||||
end |
@ -0,0 +1,52 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
module JsonLdHelper |
||||
def equals_or_includes?(haystack, needle) |
||||
haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle |
||||
end |
||||
|
||||
def first_of_value(value) |
||||
value.is_a?(Array) ? value.first : value |
||||
end |
||||
|
||||
def value_or_id(value) |
||||
value.is_a?(String) || value.nil? ? value : value['id'] |
||||
end |
||||
|
||||
def supported_context?(json) |
||||
!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT) |
||||
end |
||||
|
||||
def canonicalize(json) |
||||
graph = RDF::Graph.new << JSON::LD::API.toRdf(json) |
||||
graph.dump(:normalize) |
||||
end |
||||
|
||||
def fetch_resource(uri) |
||||
response = build_request(uri).perform |
||||
return if response.code != 200 |
||||
body_to_json(response.to_s) |
||||
end |
||||
|
||||
def body_to_json(body) |
||||
body.is_a?(String) ? Oj.load(body, mode: :strict) : body |
||||
rescue Oj::ParseError |
||||
nil |
||||
end |
||||
|
||||
def merge_context(context, new_context) |
||||
if context.is_a?(Array) |
||||
context << new_context |
||||
else |
||||
[context, new_context] |
||||
end |
||||
end |
||||
|
||||
private |
||||
|
||||
def build_request(uri) |
||||
request = Request.new(:get, uri) |
||||
request.add_headers('Accept' => 'application/activity+json, application/ld+json') |
||||
request |
||||
end |
||||
end |
@ -0,0 +1,94 @@ |
||||
import createStream from '../stream'; |
||||
import { |
||||
updateTimeline, |
||||
deleteFromTimelines, |
||||
refreshHomeTimeline, |
||||
connectTimeline, |
||||
disconnectTimeline, |
||||
} from './timelines'; |
||||
import { updateNotifications, refreshNotifications } from './notifications'; |
||||
import { getLocale } from '../locales'; |
||||
|
||||
const { messages } = getLocale(); |
||||
|
||||
export function connectTimelineStream (timelineId, path, pollingRefresh = null) { |
||||
return (dispatch, getState) => { |
||||
const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']); |
||||
const accessToken = getState().getIn(['meta', 'access_token']); |
||||
const locale = getState().getIn(['meta', 'locale']); |
||||
let polling = null; |
||||
|
||||
const setupPolling = () => { |
||||
polling = setInterval(() => { |
||||
pollingRefresh(dispatch); |
||||
}, 20000); |
||||
}; |
||||
|
||||
const clearPolling = () => { |
||||
if (polling) { |
||||
clearInterval(polling); |
||||
polling = null; |
||||
} |
||||
}; |
||||
|
||||
const subscription = createStream(streamingAPIBaseURL, accessToken, path, { |
||||
|
||||
connected () { |
||||
if (pollingRefresh) { |
||||
clearPolling(); |
||||
} |
||||
dispatch(connectTimeline(timelineId)); |
||||
}, |
||||
|
||||
disconnected () { |
||||
if (pollingRefresh) { |
||||
setupPolling(); |
||||
} |
||||
dispatch(disconnectTimeline(timelineId)); |
||||
}, |
||||
|
||||
received (data) { |
||||
switch(data.event) { |
||||
case 'update': |
||||
dispatch(updateTimeline(timelineId, JSON.parse(data.payload))); |
||||
break; |
||||
case 'delete': |
||||
dispatch(deleteFromTimelines(data.payload)); |
||||
break; |
||||
case 'notification': |
||||
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale)); |
||||
break; |
||||
} |
||||
}, |
||||
|
||||
reconnected () { |
||||
if (pollingRefresh) { |
||||
clearPolling(); |
||||
pollingRefresh(dispatch); |
||||
} |
||||
dispatch(connectTimeline(timelineId)); |
||||
}, |
||||
|
||||
}); |
||||
|
||||
const disconnect = () => { |
||||
if (subscription) { |
||||
subscription.close(); |
||||
} |
||||
clearPolling(); |
||||
}; |
||||
|
||||
return disconnect; |
||||
}; |
||||
} |
||||
|
||||
function refreshHomeTimelineAndNotification (dispatch) { |
||||
dispatch(refreshHomeTimeline()); |
||||
dispatch(refreshNotifications()); |
||||
} |
||||
|
||||
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); |
||||
export const connectCommunityStream = () => connectTimelineStream('community', 'public:local'); |
||||
export const connectMediaStream = () => connectTimelineStream('community', 'public:local'); |
||||
export const connectPublicStream = () => connectTimelineStream('public', 'public'); |
||||
export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`); |
@ -0,0 +1,122 @@ |
||||
import React from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
import ImmutablePureComponent from 'react-immutable-pure-component'; |
||||
import scheduleIdleTask from '../features/ui/util/schedule_idle_task'; |
||||
import getRectFromEntry from '../features/ui/util/get_rect_from_entry'; |
||||
|
||||
export default class IntersectionObserverArticle extends ImmutablePureComponent { |
||||
|
||||
static propTypes = { |
||||
intersectionObserverWrapper: PropTypes.object, |
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
||||
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
||||
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
||||
children: PropTypes.node, |
||||
}; |
||||
|
||||
state = { |
||||
isHidden: false, // set to true in requestIdleCallback to trigger un-render
|
||||
} |
||||
|
||||
shouldComponentUpdate (nextProps, nextState) { |
||||
if (!nextState.isIntersecting && nextState.isHidden) { |
||||
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
|
||||
// that either "isIntersecting" or "isHidden" matter, and then they're
|
||||
// the only things that matter (and updated ARIA attributes).
|
||||
return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength; |
||||
} else if (nextState.isIntersecting && !this.state.isIntersecting) { |
||||
// If we're going from a non-intersecting state to an intersecting state,
|
||||
// (i.e. offscreen to onscreen), then we definitely need to re-render
|
||||
return true; |
||||
} |
||||
// Otherwise, diff based on "updateOnProps" and "updateOnStates"
|
||||
return super.shouldComponentUpdate(nextProps, nextState); |
||||
} |
||||
|
||||
componentDidMount () { |
||||
if (!this.props.intersectionObserverWrapper) { |
||||
// TODO: enable IntersectionObserver optimization for notification statuses.
|
||||
// These are managed in notifications/index.js rather than status_list.js
|
||||
return; |
||||
} |
||||
this.props.intersectionObserverWrapper.observe( |
||||
this.props.id, |
||||
this.node, |
||||
this.handleIntersection |
||||
); |
||||
|
||||
this.componentMounted = true; |
||||
} |
||||
|
||||
componentWillUnmount () { |
||||
if (this.props.intersectionObserverWrapper) { |
||||
this.props.intersectionObserverWrapper.unobserve(this.props.id, this.node); |
||||
} |
||||
|
||||
this.componentMounted = false; |
||||
} |
||||
|
||||
handleIntersection = (entry) => { |
||||
if (this.node && this.node.children.length !== 0) { |
||||
// save the height of the fully-rendered element
|
||||
this.height = getRectFromEntry(entry).height; |
||||
|
||||
if (this.props.onHeightChange) { |
||||
this.props.onHeightChange(this.props.status, this.height); |
||||
} |
||||
} |
||||
|
||||
this.setState((prevState) => { |
||||
if (prevState.isIntersecting && !entry.isIntersecting) { |
||||
scheduleIdleTask(this.hideIfNotIntersecting); |
||||
} |
||||
return { |
||||
isIntersecting: entry.isIntersecting, |
||||
isHidden: false, |
||||
}; |
||||
}); |
||||
} |
||||
|
||||
hideIfNotIntersecting = () => { |
||||
if (!this.componentMounted) { |
||||
return; |
||||
} |
||||
|
||||
// When the browser gets a chance, test if we're still not intersecting,
|
||||
// and if so, set our isHidden to true to trigger an unrender. The point of
|
||||
// this is to save DOM nodes and avoid using up too much memory.
|
||||
// See: https://github.com/tootsuite/mastodon/issues/2900
|
||||
this.setState((prevState) => ({ isHidden: !prevState.isIntersecting })); |
||||
} |
||||
|
||||
handleRef = (node) => { |
||||
this.node = node; |
||||
} |
||||
|
||||
render () { |
||||
const { children, id, index, listLength } = this.props; |
||||
const { isIntersecting, isHidden } = this.state; |
||||
|
||||
if (!isIntersecting && isHidden) { |
||||
return ( |
||||
<article |
||||
ref={this.handleRef} |
||||
aria-posinset={index} |
||||
aria-setsize={listLength} |
||||
style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }} |
||||
data-id={id} |
||||
tabIndex='0' |
||||
> |
||||
{children && React.cloneElement(children, { hidden: true })} |
||||
</article> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<article ref={this.handleRef} aria-posinset={index} aria-setsize={listLength} data-id={id} tabIndex='0'> |
||||
{children && React.cloneElement(children, { hidden: false })} |
||||
</article> |
||||
); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,179 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { ScrollContainer } from 'react-router-scroll'; |
||||
import PropTypes from 'prop-types'; |
||||
import IntersectionObserverArticle from './intersection_observer_article'; |
||||
import LoadMore from './load_more'; |
||||
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; |
||||
import { throttle } from 'lodash'; |
||||
|
||||
export default class ScrollableList extends PureComponent { |
||||
|
||||
static propTypes = { |
||||
scrollKey: PropTypes.string.isRequired, |
||||
onScrollToBottom: PropTypes.func, |
||||
onScrollToTop: PropTypes.func, |
||||
onScroll: PropTypes.func, |
||||
trackScroll: PropTypes.bool, |
||||
shouldUpdateScroll: PropTypes.func, |
||||
isLoading: PropTypes.bool, |
||||
hasMore: PropTypes.bool, |
||||
prepend: PropTypes.node, |
||||
emptyMessage: PropTypes.node, |
||||
children: PropTypes.node, |
||||
}; |
||||
|
||||
static defaultProps = { |
||||
trackScroll: true, |
||||
}; |
||||
|
||||
intersectionObserverWrapper = new IntersectionObserverWrapper(); |
||||
|
||||
handleScroll = throttle(() => { |
||||
if (this.node) { |
||||
const { scrollTop, scrollHeight, clientHeight } = this.node; |
||||
const offset = scrollHeight - scrollTop - clientHeight; |
||||
this._oldScrollPosition = scrollHeight - scrollTop; |
||||
|
||||
if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) { |
||||
this.props.onScrollToBottom(); |
||||
} else if (scrollTop < 100 && this.props.onScrollToTop) { |
||||
this.props.onScrollToTop(); |
||||
} else if (this.props.onScroll) { |
||||
this.props.onScroll(); |
||||
} |
||||
} |
||||
}, 150, { |
||||
trailing: true, |
||||
}); |
||||
|
||||
componentDidMount () { |
||||
this.attachScrollListener(); |
||||
this.attachIntersectionObserver(); |
||||
|
||||
// Handle initial scroll posiiton
|
||||
this.handleScroll(); |
||||
} |
||||
|
||||
componentDidUpdate (prevProps) { |
||||
// Reset the scroll position when a new child comes in in order not to
|
||||
// jerk the scrollbar around if you're already scrolled down the page.
|
||||
if (React.Children.count(prevProps.children) < React.Children.count(this.props.children) && this._oldScrollPosition && this.node.scrollTop > 0) { |
||||
if (this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props)) { |
||||
const newScrollTop = this.node.scrollHeight - this._oldScrollPosition; |
||||
if (this.node.scrollTop !== newScrollTop) { |
||||
this.node.scrollTop = newScrollTop; |
||||
} |
||||
} else { |
||||
this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop; |
||||
} |
||||
} |
||||
} |
||||
|
||||
componentWillUnmount () { |
||||
this.detachScrollListener(); |
||||
this.detachIntersectionObserver(); |
||||
} |
||||
|
||||
attachIntersectionObserver () { |
||||
this.intersectionObserverWrapper.connect({ |
||||
root: this.node, |
||||
rootMargin: '300% 0px', |
||||
}); |
||||
} |
||||
|
||||
detachIntersectionObserver () { |
||||
this.intersectionObserverWrapper.disconnect(); |
||||
} |
||||
|
||||
attachScrollListener () { |
||||
this.node.addEventListener('scroll', this.handleScroll); |
||||
} |
||||
|
||||
detachScrollListener () { |
||||
this.node.removeEventListener('scroll', this.handleScroll); |
||||
} |
||||
|
||||
getFirstChildKey (props) { |
||||
const { children } = props; |
||||
const firstChild = Array.isArray(children) ? children[0] : children; |
||||
return firstChild && firstChild.key; |
||||
} |
||||
|
||||
setRef = (c) => { |
||||
this.node = c; |
||||
} |
||||
|
||||
handleLoadMore = (e) => { |
||||
e.preventDefault(); |
||||
this.props.onScrollToBottom(); |
||||
} |
||||
|
||||
handleKeyDown = (e) => { |
||||
if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) { |
||||
const article = (() => { |
||||
switch (e.key) { |
||||
case 'PageDown': |
||||
return e.target.nodeName === 'ARTICLE' && e.target.nextElementSibling; |
||||
case 'PageUp': |
||||
return e.target.nodeName === 'ARTICLE' && e.target.previousElementSibling; |
||||
case 'End': |
||||
return this.node.querySelector('[role="feed"] > article:last-of-type'); |
||||
case 'Home': |
||||
return this.node.querySelector('[role="feed"] > article:first-of-type'); |
||||
default: |
||||
return null; |
||||
} |
||||
})(); |
||||
|
||||
|
||||
if (article) { |
||||
e.preventDefault(); |
||||
article.focus(); |
||||
article.scrollIntoView(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
render () { |
||||
const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props; |
||||
const childrenCount = React.Children.count(children); |
||||
|
||||
const loadMore = <LoadMore visible={!isLoading && childrenCount > 0 && hasMore} onClick={this.handleLoadMore} />; |
||||
let scrollableArea = null; |
||||
|
||||
if (isLoading || childrenCount > 0 || !emptyMessage) { |
||||
scrollableArea = ( |
||||
<div className='scrollable' ref={this.setRef}> |
||||
<div role='feed' className='item-list' onKeyDown={this.handleKeyDown}> |
||||
{prepend} |
||||
|
||||
{React.Children.map(this.props.children, (child, index) => ( |
||||
<IntersectionObserverArticle key={child.key} id={child.key} index={index} listLength={childrenCount} intersectionObserverWrapper={this.intersectionObserverWrapper}> |
||||
{child} |
||||
</IntersectionObserverArticle> |
||||
))} |
||||
|
||||
{loadMore} |
||||
</div> |
||||
</div> |
||||
); |
||||
} else { |
||||
scrollableArea = ( |
||||
<div className='empty-column-indicator' ref={this.setRef}> |
||||
{emptyMessage} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
if (trackScroll) { |
||||
return ( |
||||
<ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}> |
||||
{scrollableArea} |
||||
</ScrollContainer> |
||||
); |
||||
} else { |
||||
return scrollableArea; |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,39 @@ |
||||
import React from 'react'; |
||||
import { Provider } from 'react-redux'; |
||||
import PropTypes from 'prop-types'; |
||||
import configureStore from '../store/configureStore'; |
||||
import { hydrateStore } from '../actions/store'; |
||||
import { IntlProvider, addLocaleData } from 'react-intl'; |
||||
import { getLocale } from '../locales'; |
||||
import Compose from '../features/standalone/compose'; |
||||
|
||||
const { localeData, messages } = getLocale(); |
||||
addLocaleData(localeData); |
||||
|
||||
const store = configureStore(); |
||||
const initialStateContainer = document.getElementById('initial-state'); |
||||
|
||||
if (initialStateContainer !== null) { |
||||
const initialState = JSON.parse(initialStateContainer.textContent); |
||||
store.dispatch(hydrateStore(initialState)); |
||||
} |
||||
|
||||
export default class TimelineContainer extends React.PureComponent { |
||||
|
||||
static propTypes = { |
||||
locale: PropTypes.string.isRequired, |
||||
}; |
||||
|
||||
render () { |
||||
const { locale } = this.props; |
||||
|
||||
return ( |
||||
<IntlProvider locale={locale} messages={messages}> |
||||
<Provider store={store}> |
||||
<Compose /> |
||||
</Provider> |
||||
</IntlProvider> |
||||
); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,18 @@ |
||||
import React from 'react'; |
||||
import ComposeFormContainer from '../../compose/containers/compose_form_container'; |
||||
import NotificationsContainer from '../../ui/containers/notifications_container'; |
||||
import LoadingBarContainer from '../../ui/containers/loading_bar_container'; |
||||
|
||||
export default class Compose extends React.PureComponent { |
||||
|
||||
render () { |
||||
return ( |
||||
<div> |
||||
<ComposeFormContainer /> |
||||
<NotificationsContainer /> |
||||
<LoadingBarContainer className='loading-bar' /> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
} |
@ -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> |
||||
); |
||||
} |
||||
|
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue