Merge branch 'origin/master' into sync/upstream

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.rb
This commit is contained in:
David Yip
2017-09-09 14:27:47 -05:00
352 changed files with 8629 additions and 2380 deletions

View File

@@ -14,7 +14,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
});
const makeMapStateToProps = () => {
@@ -105,7 +105,7 @@ export default class Header extends ImmutablePureComponent {
if (account.getIn(['relationship', 'requested'])) {
actionBtn = (
<div className='account--action-button'>
<IconButton size={26} disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />
<IconButton size={26} active icon='hourglass' title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />
</div>
);
} else if (!account.getIn(['relationship', 'blocking'])) {

View File

@@ -38,7 +38,7 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following'])) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
if (this.unfollowModal) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,

View File

@@ -7,15 +7,11 @@ import ColumnHeader from '../../components/column_header';
import {
refreshCommunityTimeline,
expandCommunityTimeline,
updateTimeline,
deleteFromTimelines,
connectTimeline,
disconnectTimeline,
} from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
import createStream from '../../stream';
import { connectCommunityStream } from '../../actions/streaming';
const messages = defineMessages({
title: { id: 'column.community', defaultMessage: 'Local timeline' },
@@ -23,8 +19,6 @@ const messages = defineMessages({
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token']),
});
@connect(mapStateToProps)
@@ -35,8 +29,6 @@ export default class CommunityTimeline extends React.PureComponent {
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
};
@@ -61,46 +53,16 @@ export default class CommunityTimeline extends React.PureComponent {
}
componentDidMount () {
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
const { dispatch } = this.props;
dispatch(refreshCommunityTimeline());
if (typeof this._subscription !== 'undefined') {
return;
}
this._subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', {
connected () {
dispatch(connectTimeline('community'));
},
reconnected () {
dispatch(connectTimeline('community'));
},
disconnected () {
dispatch(disconnectTimeline('community'));
},
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline('community', JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
},
});
this.disconnect = dispatch(connectCommunityStream());
}
componentWillUnmount () {
if (typeof this._subscription !== 'undefined') {
this._subscription.close();
this._subscription = null;
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}

View File

@@ -16,6 +16,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
});
@connect(mapStateToProps)
@@ -28,6 +29,7 @@ export default class Favourites extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
hasMore: PropTypes.bool,
};
componentWillMount () {
@@ -62,7 +64,7 @@ export default class Favourites extends ImmutablePureComponent {
}
render () {
const { intl, statusIds, columnId, multiColumn } = this.props;
const { intl, statusIds, columnId, multiColumn, hasMore } = this.props;
const pinned = !!columnId;
return (
@@ -81,6 +83,7 @@ export default class Favourites extends ImmutablePureComponent {
trackScroll={!pinned}
statusIds={statusIds}
scrollKey={`favourited_statuses-${columnId}`}
hasMore={hasMore}
onScrollToBottom={this.handleScrollToBottom}
/>
</Column>

View File

@@ -7,17 +7,13 @@ import ColumnHeader from '../../components/column_header';
import {
refreshHashtagTimeline,
expandHashtagTimeline,
updateTimeline,
deleteFromTimelines,
} from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { FormattedMessage } from 'react-intl';
import createStream from '../../stream';
import { connectHashtagStream } from '../../actions/streaming';
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0,
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token']),
const mapStateToProps = (state, props) => ({
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
});
@connect(mapStateToProps)
@@ -27,8 +23,6 @@ export default class HashtagTimeline extends React.PureComponent {
params: PropTypes.object.isRequired,
columnId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
};
@@ -53,28 +47,13 @@ export default class HashtagTimeline extends React.PureComponent {
}
_subscribe (dispatch, id) {
const { streamingAPIBaseURL, accessToken } = this.props;
this.subscription = createStream(streamingAPIBaseURL, accessToken, `hashtag&tag=${id}`, {
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline(`hashtag:${id}`, JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
},
});
this.disconnect = dispatch(connectHashtagStream(id));
}
_unsubscribe () {
if (typeof this.subscription !== 'undefined') {
this.subscription.close();
this.subscription = null;
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}

View File

@@ -2,6 +2,7 @@
// SEE INSTEAD : glitch/components/notification
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import StatusContainer from '../../../containers/status_container';
import AccountContainer from '../../../containers/account_container';
@@ -13,6 +14,7 @@ export default class Notification extends ImmutablePureComponent {
static propTypes = {
notification: ImmutablePropTypes.map.isRequired,
hidden: PropTypes.bool,
};
renderFollow (account, link) {
@@ -26,13 +28,13 @@ export default class Notification extends ImmutablePureComponent {
<FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
</div>
<AccountContainer id={account.get('id')} withNote={false} />
<AccountContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
</div>
);
}
renderMention (notification) {
return <StatusContainer id={notification.get('status')} withDismiss />;
return <StatusContainer id={notification.get('status')} withDismiss hidden={this.props.hidden} />;
}
renderFavourite (notification, link) {
@@ -45,7 +47,7 @@ export default class Notification extends ImmutablePureComponent {
<FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} />
</div>
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss />
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={!!this.props.hidden} />
</div>
);
}
@@ -60,7 +62,7 @@ export default class Notification extends ImmutablePureComponent {
<FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} />
</div>
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss />
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={this.props.hidden} />
</div>
);
}

View File

@@ -16,8 +16,8 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
import { createSelector } from 'reselect';
import { List as ImmutableList } from 'immutable';
import LoadMore from '../../components/load_more';
import { debounce } from 'lodash';
import ScrollableList from '../../components/scrollable_list';
const messages = defineMessages({
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
@@ -68,40 +68,18 @@ export default class Notifications extends React.PureComponent {
trackScroll: true,
};
dispatchExpandNotifications = debounce(() => {
handleScrollToBottom = debounce(() => {
this.props.dispatch(scrollTopNotifications(false));
this.props.dispatch(expandNotifications());
}, 300, { leading: true });
dispatchScrollToTop = debounce((top) => {
this.props.dispatch(scrollTopNotifications(top));
handleScrollToTop = debounce(() => {
this.props.dispatch(scrollTopNotifications(true));
}, 100);
handleScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
const offset = scrollHeight - scrollTop - clientHeight;
this._oldScrollPosition = scrollHeight - scrollTop;
if (250 > offset && this.props.hasMore && !this.props.isLoading) {
this.dispatchExpandNotifications();
}
if (scrollTop < 100) {
this.dispatchScrollToTop(true);
} else {
this.dispatchScrollToTop(false);
}
}
componentDidUpdate (prevProps) {
if (this.node.scrollTop > 0 && (prevProps.notifications.size < this.props.notifications.size && prevProps.notifications.first() !== this.props.notifications.first() && !!this._oldScrollPosition)) {
this.node.scrollTop = this.node.scrollHeight - this._oldScrollPosition;
}
}
handleLoadMore = (e) => {
e.preventDefault();
this.dispatchExpandNotifications();
}
handleScroll = debounce(() => {
this.props.dispatch(scrollTopNotifications(false));
}, 100);
handlePin = () => {
const { columnId, dispatch } = this.props;
@@ -122,10 +100,6 @@ export default class Notifications extends React.PureComponent {
this.column.scrollTop();
}
setRef = (c) => {
this.node = c;
}
setColumnRef = c => {
this.column = c;
}
@@ -133,52 +107,34 @@ export default class Notifications extends React.PureComponent {
render () {
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore } = this.props;
const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
let loadMore = '';
let scrollableArea = '';
let unread = '';
let scrollContainer = '';
let scrollableContent = null;
if (!isLoading && hasMore) {
loadMore = <LoadMore onClick={this.handleLoadMore} />;
}
if (isUnread) {
unread = <div className='notifications__unread-indicator' />;
}
if (isLoading && this.scrollableArea) {
scrollableArea = this.scrollableArea;
if (isLoading && this.scrollableContent) {
scrollableContent = this.scrollableContent;
} else if (notifications.size > 0 || hasMore) {
scrollableArea = (
<div className='scrollable' onScroll={this.handleScroll} ref={this.setRef}>
{unread}
<div>
{notifications.map(item => <NotificationContainer key={item.get('id')} notification={item} accountId={item.get('account')} />)}
{loadMore}
</div>
</div>
);
scrollableContent = notifications.map((item) => <NotificationContainer key={item.get('id')} notification={item} accountId={item.get('account')} />);
} else {
scrollableArea = (
<div className='empty-column-indicator' ref={this.setRef}>
<FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />
</div>
);
scrollableContent = null;
}
if (pinned) {
scrollContainer = scrollableArea;
} else {
scrollContainer = (
<ScrollContainer scrollKey={`notifications-${columnId}`} shouldUpdateScroll={shouldUpdateScroll}>
{scrollableArea}
</ScrollContainer>
);
}
this.scrollableContent = scrollableContent;
this.scrollableArea = scrollableArea;
const scrollContainer = (
<ScrollableList
scrollKey={`notifications-${columnId}`}
isLoading={isLoading}
hasMore={hasMore}
emptyMessage={emptyMessage}
onScrollToBottom={this.handleScrollToBottom}
onScrollToTop={this.handleScrollToTop}
onScroll={this.handleScroll}
shouldUpdateScroll={shouldUpdateScroll}
>
{scrollableContent}
</ScrollableList>
);
return (
<Column

View File

@@ -7,15 +7,11 @@ import ColumnHeader from '../../components/column_header';
import {
refreshPublicTimeline,
expandPublicTimeline,
updateTimeline,
deleteFromTimelines,
connectTimeline,
disconnectTimeline,
} from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
import createStream from '../../stream';
import { connectPublicStream } from '../../actions/streaming';
const messages = defineMessages({
title: { id: 'column.public', defaultMessage: 'Federated timeline' },
@@ -23,8 +19,6 @@ const messages = defineMessages({
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token']),
});
@connect(mapStateToProps)
@@ -36,8 +30,6 @@ export default class PublicTimeline extends React.PureComponent {
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
hasUnread: PropTypes.bool,
};
@@ -61,46 +53,16 @@ export default class PublicTimeline extends React.PureComponent {
}
componentDidMount () {
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
const { dispatch } = this.props;
dispatch(refreshPublicTimeline());
if (typeof this._subscription !== 'undefined') {
return;
}
this._subscription = createStream(streamingAPIBaseURL, accessToken, 'public', {
connected () {
dispatch(connectTimeline('public'));
},
reconnected () {
dispatch(connectTimeline('public'));
},
disconnected () {
dispatch(disconnectTimeline('public'));
},
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline('public', JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
},
});
this.disconnect = dispatch(connectPublicStream());
}
componentWillUnmount () {
if (typeof this._subscription !== 'undefined') {
this._subscription.close();
this._subscription = null;
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}

View File

@@ -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>
);
}
}

View File

@@ -14,6 +14,9 @@ const messages = defineMessages({
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
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
@@ -31,6 +34,8 @@ export default class ActionBar extends React.PureComponent {
onDelete: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onReport: PropTypes.func,
onPin: PropTypes.func,
onEmbed: PropTypes.func,
me: PropTypes.number.isRequired,
intl: PropTypes.object.isRequired,
};
@@ -59,6 +64,10 @@ export default class ActionBar extends React.PureComponent {
this.props.onReport(this.props.status);
}
handlePinClick = () => {
this.props.onPin(this.props.status);
}
handleShare = () => {
navigator.share({
text: this.props.status.get('search_index'),
@@ -66,12 +75,26 @@ export default class ActionBar extends React.PureComponent {
});
}
handleEmbed = () => {
this.props.onEmbed(this.props.status);
}
render () {
const { status, me, intl } = this.props;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
let menu = [];
if (publicStatus) {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
}
if (me === status.getIn(['account', 'id'])) {
if (publicStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
}
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });

View File

@@ -1,6 +1,7 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import punycode from 'punycode';
import classnames from 'classnames';
const IDNA_PREFIX = 'xn--';
@@ -32,7 +33,7 @@ export default class Card extends React.PureComponent {
if (card.get('image')) {
image = (
<div className='status-card__image'>
<img src={card.get('image')} alt={card.get('title')} className='status-card__image-image' />
<img src={card.get('image')} alt={card.get('title')} className='status-card__image-image' width={card.get('width')} height={card.get('height')} />
</div>
);
}
@@ -41,8 +42,12 @@ export default class Card extends React.PureComponent {
provider = decodeIDNA(getHostname(card.get('url')));
}
const className = classnames('status-card', {
'horizontal': card.get('width') > card.get('height'),
});
return (
<a href={card.get('url')} className='status-card' target='_blank' rel='noopener'>
<a href={card.get('url')} className={className} target='_blank' rel='noopener'>
{image}
<div className='status-card__content'>

View File

@@ -12,6 +12,8 @@ import {
unfavourite,
reblog,
unreblog,
pin,
unpin,
} from '../../actions/interactions';
import {
replyCompose,
@@ -89,6 +91,14 @@ export default class Status extends ImmutablePureComponent {
}
}
handlePin = (status) => {
if (status.get('pinned')) {
this.props.dispatch(unpin(status));
} else {
this.props.dispatch(pin(status));
}
}
handleReplyClick = (status) => {
this.props.dispatch(replyCompose(status, this.context.router.history));
}
@@ -139,6 +149,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} />);
}
@@ -190,6 +204,8 @@ export default class Status extends ImmutablePureComponent {
onDelete={this.handleDeleteClick}
onMention={this.handleMentionClick}
onReport={this.handleReport}
onPin={this.handlePin}
onEmbed={this.handleEmbed}
/>
{descendants}

View File

@@ -25,6 +25,17 @@ export default class Column extends React.PureComponent {
this._interruptScrollAnimation = scrollTop(scrollable);
}
scrollTop () {
const scrollable = this.node.querySelector('.scrollable');
if (!scrollable) {
return;
}
this._interruptScrollAnimation = scrollTop(scrollable);
}
handleScroll = debounce(() => {
if (typeof this._interruptScrollAnimation !== 'undefined') {
this._interruptScrollAnimation();

View File

@@ -12,6 +12,7 @@ import ColumnLoading from './column_loading';
import BundleColumnError from './bundle_column_error';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components';
import detectPassiveEvents from 'detect-passive-events';
import { scrollRight } from '../../../scroll';
const componentMap = {
@@ -24,7 +25,7 @@ const componentMap = {
'FAVOURITES': FavouritedStatuses,
};
@injectIntl
@component => injectIntl(component, { withRef: true })
export default class ColumnsArea extends ImmutablePureComponent {
static contextTypes = {
@@ -47,16 +48,36 @@ export default class ColumnsArea extends ImmutablePureComponent {
}
componentDidMount() {
if (!this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
}
this.lastIndex = getIndex(this.context.router.history.location.pathname);
this.setState({ shouldAnimate: true });
}
componentWillUpdate(nextProps) {
if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}
}
componentDidUpdate(prevProps) {
if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
}
this.lastIndex = getIndex(this.context.router.history.location.pathname);
this.setState({ shouldAnimate: true });
}
if (this.props.children !== prevProps.children && !this.props.singleColumn) {
scrollRight(this.node);
componentWillUnmount () {
if (!this.props.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}
}
handleChildrenContentChange() {
if (!this.props.singleColumn) {
scrollRight(this.node, this.node.scrollWidth - window.innerWidth);
}
}
@@ -80,6 +101,14 @@ export default class ColumnsArea extends ImmutablePureComponent {
}
}
handleWheel = () => {
if (typeof this._interruptScrollAnimation !== 'function') {
return;
}
this._interruptScrollAnimation();
}
setRef = (node) => {
this.node = node;
}

View File

@@ -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>
);
}
}

View File

@@ -14,6 +14,7 @@ import {
ConfirmationModal,
ReportModal,
SettingsModal,
EmbedModal,
} from '../../../features/ui/util/async-components';
const MODAL_COMPONENTS = {
@@ -25,6 +26,7 @@ const MODAL_COMPONENTS = {
'REPORT': ReportModal,
'SETTINGS': SettingsModal,
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
'EMBED': EmbedModal,
};
export default class ModalRoot extends React.PureComponent {

View File

@@ -30,7 +30,7 @@ const PageOne = ({ acct, domain }) => (
<div>
<h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to {domain}!' values={{ domain }} /></h1>
<p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='{domain} is an "instance" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.' values={{ domain }} /></p>
<p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, so your full handle is {handle}' values={{ domain, handle: <strong>{acct}@{domain}</strong> }} /></p>
<p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, so your full handle is {handle}' values={{ domain, handle: <strong>@{acct}@{domain}</strong> }} /></p>
</div>
</div>
);

View File

@@ -5,4 +5,4 @@ const mapStateToProps = state => ({
columns: state.getIn(['settings', 'columns']),
});
export default connect(mapStateToProps)(ColumnsArea);
export default connect(mapStateToProps, null, null, { withRef: true })(ColumnsArea);

View File

@@ -1,12 +1,11 @@
import React from 'react';
import classNames from 'classnames';
import Redirect from 'react-router-dom/Redirect';
import NotificationsContainer from './containers/notifications_container';
import PropTypes from 'prop-types';
import LoadingBarContainer from './containers/loading_bar_container';
import TabsBar from './components/tabs_bar';
import ModalContainer from './containers/modal_container';
import { connect } from 'react-redux';
import { Redirect, withRouter } from 'react-router-dom';
import { isMobile } from '../../is_mobile';
import { debounce } from 'lodash';
import { uploadCompose } from '../../actions/compose';
@@ -51,6 +50,7 @@ const mapStateToProps = state => ({
});
@connect(mapStateToProps)
@withRouter
export default class UI extends React.PureComponent {
static contextTypes = {
@@ -65,6 +65,7 @@ export default class UI extends React.PureComponent {
systemFontUi: PropTypes.bool,
navbarUnder: PropTypes.bool,
isComposing: PropTypes.bool,
location: PropTypes.object,
};
state = {
@@ -141,7 +142,7 @@ export default class UI extends React.PureComponent {
if (data.type === 'navigate') {
this.context.router.history.push(data.path);
} else {
console.warn('Unknown message type:', data.type); // eslint-disable-line no-console
console.warn('Unknown message type:', data.type);
}
}
@@ -175,6 +176,12 @@ export default class UI extends React.PureComponent {
return true;
}
componentDidUpdate (prevProps) {
if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
this.columnsAreaNode.handleChildrenContentChange();
}
}
componentWillUnmount () {
window.removeEventListener('resize', this.handleResize);
document.removeEventListener('dragenter', this.handleDragEnter);
@@ -188,6 +195,10 @@ export default class UI extends React.PureComponent {
this.node = c;
}
setColumnsAreaRef = (c) => {
this.columnsAreaNode = c.getWrappedInstance().getWrappedInstance();
}
render () {
const { width, draggingOver } = this.state;
const { children, layout, isWide, navbarUnder } = this.props;
@@ -212,7 +223,7 @@ export default class UI extends React.PureComponent {
return (
<div className={className} ref={this.setRef}>
{navbarUnder ? null : (<TabsBar />)}
<ColumnsAreaContainer singleColumn={isMobile(width, layout)}>
<ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width, layout)}>
<WrappedSwitch>
<Redirect from='/' to='/getting-started' exact />
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />

View File

@@ -116,3 +116,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');
}