@@ -3,6 +3,8 @@ | |||
class TagsController < ApplicationController | |||
PAGE_SIZE = 20 | |||
layout 'public' | |||
before_action :set_body_classes | |||
before_action :set_instance_presenter | |||
@@ -1,15 +1,17 @@ | |||
import React from 'react'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import PropTypes from 'prop-types'; | |||
export default class DisplayName extends React.PureComponent { | |||
static propTypes = { | |||
account: ImmutablePropTypes.map.isRequired, | |||
others: ImmutablePropTypes.list, | |||
localDomain: PropTypes.string, | |||
}; | |||
render () { | |||
const { account, others } = this.props; | |||
const { account, others, localDomain } = this.props; | |||
const displayNameHtml = { __html: account.get('display_name_html') }; | |||
let suffix; | |||
@@ -17,7 +19,13 @@ export default class DisplayName extends React.PureComponent { | |||
if (others && others.size > 1) { | |||
suffix = `+${others.size}`; | |||
} else { | |||
suffix = <span className='display-name__account'>@{account.get('acct')}</span>; | |||
let acct = account.get('acct'); | |||
if (acct.indexOf('@') === -1 && localDomain) { | |||
acct = `${acct}@${localDomain}`; | |||
} | |||
suffix = <span className='display-name__account'>@{acct}</span>; | |||
} | |||
return ( | |||
@@ -77,7 +77,7 @@ class Status extends ImmutablePureComponent { | |||
'account', | |||
'muted', | |||
'hidden', | |||
] | |||
]; | |||
handleClick = () => { | |||
if (this.props.onClick) { | |||
@@ -1,28 +1,32 @@ | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import PropTypes from 'prop-types'; | |||
import StatusListContainer from '../../ui/containers/status_list_container'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import { expandHashtagTimeline } from '../../../actions/timelines'; | |||
import Column from '../../../components/column'; | |||
import ColumnHeader from '../../../components/column_header'; | |||
import { connectHashtagStream } from '../../../actions/streaming'; | |||
import Masonry from 'react-masonry-infinite'; | |||
import { List as ImmutableList } from 'immutable'; | |||
import DetailedStatusContainer from '../../status/containers/detailed_status_container'; | |||
import { debounce } from 'lodash'; | |||
import LoadingIndicator from '../../../components/loading_indicator'; | |||
export default @connect() | |||
const mapStateToProps = (state, { hashtag }) => ({ | |||
statusIds: state.getIn(['timelines', `hashtag:${hashtag}`, 'items'], ImmutableList()), | |||
isLoading: state.getIn(['timelines', `hashtag:${hashtag}`, 'isLoading'], false), | |||
hasMore: state.getIn(['timelines', `hashtag:${hashtag}`, 'hasMore'], false), | |||
}); | |||
export default @connect(mapStateToProps) | |||
class HashtagTimeline extends React.PureComponent { | |||
static propTypes = { | |||
dispatch: PropTypes.func.isRequired, | |||
statusIds: ImmutablePropTypes.list.isRequired, | |||
isLoading: PropTypes.bool.isRequired, | |||
hasMore: PropTypes.bool.isRequired, | |||
hashtag: PropTypes.string.isRequired, | |||
}; | |||
handleHeaderClick = () => { | |||
this.column.scrollTop(); | |||
} | |||
setRef = c => { | |||
this.column = c; | |||
} | |||
componentDidMount () { | |||
const { dispatch, hashtag } = this.props; | |||
@@ -37,28 +41,52 @@ class HashtagTimeline extends React.PureComponent { | |||
} | |||
} | |||
handleLoadMore = maxId => { | |||
this.props.dispatch(expandHashtagTimeline(this.props.hashtag, { maxId })); | |||
handleLoadMore = () => { | |||
const maxId = this.props.statusIds.last(); | |||
if (maxId) { | |||
this.props.dispatch(expandHashtagTimeline(this.props.hashtag, { maxId })); | |||
} | |||
} | |||
setRef = c => { | |||
this.masonry = c; | |||
} | |||
handleHeightChange = debounce(() => { | |||
if (!this.masonry) { | |||
return; | |||
} | |||
this.masonry.forcePack(); | |||
}, 50) | |||
render () { | |||
const { hashtag } = this.props; | |||
const { statusIds, hasMore, isLoading } = this.props; | |||
const sizes = [ | |||
{ columns: 1, gutter: 0 }, | |||
{ mq: '415px', columns: 1, gutter: 10 }, | |||
{ mq: '640px', columns: 2, gutter: 10 }, | |||
{ mq: '960px', columns: 3, gutter: 10 }, | |||
{ mq: '1255px', columns: 3, gutter: 10 }, | |||
]; | |||
const loader = (isLoading && statusIds.isEmpty()) ? <LoadingIndicator key={0} /> : undefined; | |||
return ( | |||
<Column ref={this.setRef}> | |||
<ColumnHeader | |||
icon='hashtag' | |||
title={hashtag} | |||
onClick={this.handleHeaderClick} | |||
/> | |||
<StatusListContainer | |||
trackScroll={false} | |||
scrollKey='standalone_hashtag_timeline' | |||
timelineId={`hashtag:${hashtag}`} | |||
onLoadMore={this.handleLoadMore} | |||
/> | |||
</Column> | |||
<Masonry ref={this.setRef} className='statuses-grid' hasMore={hasMore} loadMore={this.handleLoadMore} sizes={sizes} loader={loader}> | |||
{statusIds.map(statusId => ( | |||
<div className='statuses-grid__item' key={statusId}> | |||
<DetailedStatusContainer | |||
id={statusId} | |||
showThread | |||
measureHeight | |||
onHeightChange={this.handleHeightChange} | |||
/> | |||
</div> | |||
)).toArray()} | |||
</Masonry> | |||
); | |||
} | |||
@@ -11,6 +11,7 @@ import { FormattedDate, FormattedNumber } from 'react-intl'; | |||
import Card from './card'; | |||
import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
import Video from '../../video'; | |||
import scheduleIdleTask from '../../ui/util/schedule_idle_task'; | |||
export default class DetailedStatus extends ImmutablePureComponent { | |||
@@ -23,10 +24,17 @@ export default class DetailedStatus extends ImmutablePureComponent { | |||
onOpenMedia: PropTypes.func.isRequired, | |||
onOpenVideo: PropTypes.func.isRequired, | |||
onToggleHidden: PropTypes.func.isRequired, | |||
measureHeight: PropTypes.bool, | |||
onHeightChange: PropTypes.func, | |||
domain: PropTypes.string.isRequired, | |||
}; | |||
state = { | |||
height: null, | |||
}; | |||
handleAccountClick = (e) => { | |||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { | |||
if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) { | |||
e.preventDefault(); | |||
this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); | |||
} | |||
@@ -42,13 +50,56 @@ export default class DetailedStatus extends ImmutablePureComponent { | |||
this.props.onToggleHidden(this.props.status); | |||
} | |||
_measureHeight (heightJustChanged) { | |||
if (this.props.measureHeight && this.node) { | |||
scheduleIdleTask(() => this.node && this.setState({ height: this.node.offsetHeight })); | |||
if (this.props.onHeightChange && heightJustChanged) { | |||
this.props.onHeightChange(); | |||
} | |||
} | |||
} | |||
setRef = c => { | |||
this.node = c; | |||
this._measureHeight(); | |||
} | |||
componentDidUpdate (prevProps, prevState) { | |||
this._measureHeight(prevState.height !== this.state.height); | |||
} | |||
handleModalLink = e => { | |||
e.preventDefault(); | |||
let href; | |||
if (e.target.nodeName !== 'A') { | |||
href = e.target.parentNode.href; | |||
} else { | |||
href = e.target.href; | |||
} | |||
window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); | |||
} | |||
render () { | |||
const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; | |||
const outerStyle = { boxSizing: 'border-box' }; | |||
if (!status) { | |||
return null; | |||
} | |||
let media = ''; | |||
let applicationLink = ''; | |||
let reblogLink = ''; | |||
let reblogIcon = 'retweet'; | |||
let favouriteLink = ''; | |||
if (this.props.measureHeight) { | |||
outerStyle.height = `${this.state.height}px`; | |||
} | |||
if (status.get('media_attachments').size > 0) { | |||
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { | |||
@@ -95,20 +146,51 @@ export default class DetailedStatus extends ImmutablePureComponent { | |||
if (status.get('visibility') === 'private') { | |||
reblogLink = <i className={`fa fa-${reblogIcon}`} />; | |||
} else if (this.context.router) { | |||
reblogLink = ( | |||
<Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'> | |||
<i className={`fa fa-${reblogIcon}`} /> | |||
<span className='detailed-status__reblogs'> | |||
<FormattedNumber value={status.get('reblogs_count')} /> | |||
</span> | |||
</Link> | |||
); | |||
} else { | |||
reblogLink = ( | |||
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}> | |||
<i className={`fa fa-${reblogIcon}`} /> | |||
<span className='detailed-status__reblogs'> | |||
<FormattedNumber value={status.get('reblogs_count')} /> | |||
</span> | |||
</a> | |||
); | |||
} | |||
if (this.context.router) { | |||
favouriteLink = ( | |||
<Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'> | |||
<i className='fa fa-star' /> | |||
<span className='detailed-status__favorites'> | |||
<FormattedNumber value={status.get('favourites_count')} /> | |||
</span> | |||
</Link> | |||
); | |||
} else { | |||
reblogLink = (<Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'> | |||
<i className={`fa fa-${reblogIcon}`} /> | |||
<span className='detailed-status__reblogs'> | |||
<FormattedNumber value={status.get('reblogs_count')} /> | |||
</span> | |||
</Link>); | |||
favouriteLink = ( | |||
<a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}> | |||
<i className='fa fa-star' /> | |||
<span className='detailed-status__favorites'> | |||
<FormattedNumber value={status.get('favourites_count')} /> | |||
</span> | |||
</a> | |||
); | |||
} | |||
return ( | |||
<div className='detailed-status'> | |||
<div ref={this.setRef} className='detailed-status' style={outerStyle}> | |||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'> | |||
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div> | |||
<DisplayName account={status.get('account')} /> | |||
<DisplayName account={status.get('account')} localDomain={this.props.domain} /> | |||
</a> | |||
<StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} /> | |||
@@ -118,12 +200,7 @@ export default class DetailedStatus extends ImmutablePureComponent { | |||
<div className='detailed-status__meta'> | |||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'> | |||
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /> | |||
</a>{applicationLink} · {reblogLink} · <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'> | |||
<i className='fa fa-star' /> | |||
<span className='detailed-status__favorites'> | |||
<FormattedNumber value={status.get('favourites_count')} /> | |||
</span> | |||
</Link> | |||
</a>{applicationLink} · {reblogLink} · {favouriteLink} | |||
</div> | |||
</div> | |||
); | |||
@@ -0,0 +1,172 @@ | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import DetailedStatus from '../components/detailed_status'; | |||
import { makeGetStatus } from '../../../selectors'; | |||
import { | |||
replyCompose, | |||
mentionCompose, | |||
directCompose, | |||
} from '../../../actions/compose'; | |||
import { | |||
reblog, | |||
favourite, | |||
unreblog, | |||
unfavourite, | |||
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 { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | |||
import { boostModal, deleteModal } from '../../../initial_state'; | |||
import { showAlertForError } from '../../../actions/alerts'; | |||
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? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, | |||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, | |||
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?' }, | |||
}); | |||
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) => { | |||
let 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)); | |||
} | |||
}); | |||
}, | |||
onModalReblog (status) { | |||
dispatch(reblog(status)); | |||
}, | |||
onReblog (status, e) { | |||
if (status.get('reblogged')) { | |||
dispatch(unreblog(status)); | |||
} else { | |||
if (e.shiftKey || !boostModal) { | |||
this.onModalReblog(status); | |||
} else { | |||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); | |||
} | |||
} | |||
}, | |||
onFavourite (status) { | |||
if (status.get('favourited')) { | |||
dispatch(unfavourite(status)); | |||
} else { | |||
dispatch(favourite(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, withRedraft = false) { | |||
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)), | |||
})); | |||
} | |||
}, | |||
onDirect (account, router) { | |||
dispatch(directCompose(account, router)); | |||
}, | |||
onMention (account, router) { | |||
dispatch(mentionCompose(account, router)); | |||
}, | |||
onOpenMedia (media, index) { | |||
dispatch(openModal('MEDIA', { media, index })); | |||
}, | |||
onOpenVideo (media, time) { | |||
dispatch(openModal('VIDEO', { media, time })); | |||
}, | |||
onBlock (account) { | |||
dispatch(openModal('CONFIRM', { | |||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, | |||
confirm: intl.formatMessage(messages.blockConfirm), | |||
onConfirm: () => dispatch(blockAccount(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)); |
@@ -425,3 +425,30 @@ | |||
border-radius: 0; | |||
} | |||
} | |||
$maximum-width: 1235px; | |||
$fluid-breakpoint: $maximum-width + 20px; | |||
.statuses-grid { | |||
min-height: 600px; | |||
&__item { | |||
width: (960px - 20px) / 3; | |||
@media screen and (max-width: $fluid-breakpoint) { | |||
width: (940px - 20px) / 3; | |||
} | |||
@media screen and (max-width: $no-gap-breakpoint) { | |||
width: 100vw; | |||
} | |||
} | |||
.detailed-status { | |||
border-radius: 4px; | |||
@media screen and (max-width: $no-gap-breakpoint) { | |||
border-bottom: 1px solid lighten($ui-base-color, 12%); | |||
} | |||
} | |||
} |
@@ -8,33 +8,5 @@ | |||
= javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous' | |||
= render 'og' | |||
.landing-page.tag-page.alternative | |||
.features | |||
.container | |||
.grid | |||
.column-1 | |||
#mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name)) } } | |||
.column-2 | |||
.about-mastodon | |||
.about-hashtag.landing-page__information | |||
.brand | |||
= link_to root_url do | |||
= image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' | |||
%p= t 'about.about_hashtag_html', hashtag: @tag.name | |||
.cta | |||
- if user_signed_in? | |||
= link_to t('settings.back'), root_path, class: 'button button-secondary' | |||
- else | |||
= link_to t('auth.login'), new_user_session_path, class: 'button button-secondary' | |||
= link_to t('about.learn_more'), about_path, class: 'button button-alternative' | |||
.landing-page__features.landing-page__information | |||
%h3= t 'about.what_is_mastodon' | |||
%p= t 'about.about_mastodon_html' | |||
= render 'features' | |||
#mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name)) } } | |||
#modal-container |
@@ -98,6 +98,7 @@ | |||
"react-immutable-proptypes": "^2.1.0", | |||
"react-immutable-pure-component": "^1.1.1", | |||
"react-intl": "^2.7.2", | |||
"react-masonry-infinite": "^1.2.2", | |||
"react-motion": "^0.5.2", | |||
"react-notification": "^6.8.4", | |||
"react-overlays": "^0.8.3", | |||
@@ -17,7 +17,7 @@ RSpec.describe TagsController, type: :controller do | |||
it 'renders application layout' do | |||
get :show, params: { id: 'test', max_id: late.id } | |||
expect(response).to render_template layout: 'application' | |||
expect(response).to render_template layout: 'public' | |||
end | |||
end | |||
@@ -1681,6 +1681,13 @@ braces@^2.3.0, braces@^2.3.1: | |||
split-string "^3.0.2" | |||
to-regex "^3.0.1" | |||
bricks.js@^1.7.0: | |||
version "1.8.0" | |||
resolved "https://registry.yarnpkg.com/bricks.js/-/bricks.js-1.8.0.tgz#8fdeb3c0226af251f4d5727a7df7f9ac0092b4b2" | |||
integrity sha1-j96zwCJq8lH01XJ6fff5rACStLI= | |||
dependencies: | |||
knot.js "^1.1.5" | |||
brorand@^1.0.1: | |||
version "1.1.0" | |||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" | |||
@@ -5528,6 +5535,11 @@ kleur@^2.0.1: | |||
resolved "https://registry.yarnpkg.com/kleur/-/kleur-2.0.2.tgz#b704f4944d95e255d038f0cb05fb8a602c55a300" | |||
integrity sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ== | |||
knot.js@^1.1.5: | |||
version "1.1.5" | |||
resolved "https://registry.yarnpkg.com/knot.js/-/knot.js-1.1.5.tgz#28e72522f703f50fe98812fde224dd72728fef5d" | |||
integrity sha1-KOclIvcD9Q/piBL94iTdcnKP710= | |||
lcid@^1.0.0: | |||
version "1.0.0" | |||
resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" | |||
@@ -7558,6 +7570,13 @@ react-immutable-pure-component@^1.1.1: | |||
optionalDependencies: | |||
"@types/react" "16.4.6" | |||
react-infinite-scroller@^1.0.12: | |||
version "1.2.4" | |||
resolved "https://registry.yarnpkg.com/react-infinite-scroller/-/react-infinite-scroller-1.2.4.tgz#f67eaec4940a4ce6417bebdd6e3433bfc38826e9" | |||
integrity sha512-/oOa0QhZjXPqaD6sictN2edFMsd3kkMiE19Vcz5JDgHpzEJVqYcmq+V3mkwO88087kvKGe1URNksHEOt839Ubw== | |||
dependencies: | |||
prop-types "^15.5.8" | |||
react-input-autosize@^2.2.1: | |||
version "2.2.1" | |||
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8" | |||
@@ -7596,6 +7615,15 @@ react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4: | |||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" | |||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== | |||
react-masonry-infinite@^1.2.2: | |||
version "1.2.2" | |||
resolved "https://registry.yarnpkg.com/react-masonry-infinite/-/react-masonry-infinite-1.2.2.tgz#20c1386f9ccdda9747527c8f42bc2c02dd2e7951" | |||
integrity sha1-IME4b5zN2pdHUnyPQrwsAt0ueVE= | |||
dependencies: | |||
bricks.js "^1.7.0" | |||
prop-types "^15.5.10" | |||
react-infinite-scroller "^1.0.12" | |||
react-motion@^0.5.2: | |||
version "0.5.2" | |||
resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" | |||