Redesign public hashtag page to use a masonry layout (#9822)
parent
9b0c7cfa6f
commit
380b246728
|
@ -3,6 +3,8 @@
|
||||||
class TagsController < ApplicationController
|
class TagsController < ApplicationController
|
||||||
PAGE_SIZE = 20
|
PAGE_SIZE = 20
|
||||||
|
|
||||||
|
layout 'public'
|
||||||
|
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_instance_presenter
|
before_action :set_instance_presenter
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
export default class DisplayName extends React.PureComponent {
|
export default class DisplayName extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
others: ImmutablePropTypes.list,
|
others: ImmutablePropTypes.list,
|
||||||
|
localDomain: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, others } = this.props;
|
const { account, others, localDomain } = this.props;
|
||||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||||
|
|
||||||
let suffix;
|
let suffix;
|
||||||
|
@ -17,7 +19,13 @@ export default class DisplayName extends React.PureComponent {
|
||||||
if (others && others.size > 1) {
|
if (others && others.size > 1) {
|
||||||
suffix = `+${others.size}`;
|
suffix = `+${others.size}`;
|
||||||
} else {
|
} 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 (
|
return (
|
||||||
|
|
|
@ -77,7 +77,7 @@ class Status extends ImmutablePureComponent {
|
||||||
'account',
|
'account',
|
||||||
'muted',
|
'muted',
|
||||||
'hidden',
|
'hidden',
|
||||||
]
|
];
|
||||||
|
|
||||||
handleClick = () => {
|
handleClick = () => {
|
||||||
if (this.props.onClick) {
|
if (this.props.onClick) {
|
||||||
|
|
|
@ -1,28 +1,32 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
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 { expandHashtagTimeline } from '../../../actions/timelines';
|
||||||
import Column from '../../../components/column';
|
|
||||||
import ColumnHeader from '../../../components/column_header';
|
|
||||||
import { connectHashtagStream } from '../../../actions/streaming';
|
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 {
|
class HashtagTimeline extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
statusIds: ImmutablePropTypes.list.isRequired,
|
||||||
|
isLoading: PropTypes.bool.isRequired,
|
||||||
|
hasMore: PropTypes.bool.isRequired,
|
||||||
hashtag: PropTypes.string.isRequired,
|
hashtag: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleHeaderClick = () => {
|
|
||||||
this.column.scrollTop();
|
|
||||||
}
|
|
||||||
|
|
||||||
setRef = c => {
|
|
||||||
this.column = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch, hashtag } = this.props;
|
const { dispatch, hashtag } = this.props;
|
||||||
|
|
||||||
|
@ -37,28 +41,52 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = maxId => {
|
handleLoadMore = () => {
|
||||||
this.props.dispatch(expandHashtagTimeline(this.props.hashtag, { maxId }));
|
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 () {
|
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 (
|
return (
|
||||||
<Column ref={this.setRef}>
|
<Masonry ref={this.setRef} className='statuses-grid' hasMore={hasMore} loadMore={this.handleLoadMore} sizes={sizes} loader={loader}>
|
||||||
<ColumnHeader
|
{statusIds.map(statusId => (
|
||||||
icon='hashtag'
|
<div className='statuses-grid__item' key={statusId}>
|
||||||
title={hashtag}
|
<DetailedStatusContainer
|
||||||
onClick={this.handleHeaderClick}
|
id={statusId}
|
||||||
/>
|
showThread
|
||||||
|
measureHeight
|
||||||
<StatusListContainer
|
onHeightChange={this.handleHeightChange}
|
||||||
trackScroll={false}
|
/>
|
||||||
scrollKey='standalone_hashtag_timeline'
|
</div>
|
||||||
timelineId={`hashtag:${hashtag}`}
|
)).toArray()}
|
||||||
onLoadMore={this.handleLoadMore}
|
</Masonry>
|
||||||
/>
|
|
||||||
</Column>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { FormattedDate, FormattedNumber } from 'react-intl';
|
||||||
import Card from './card';
|
import Card from './card';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import Video from '../../video';
|
import Video from '../../video';
|
||||||
|
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
|
||||||
|
|
||||||
export default class DetailedStatus extends ImmutablePureComponent {
|
export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
@ -23,10 +24,17 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
onOpenMedia: PropTypes.func.isRequired,
|
onOpenMedia: PropTypes.func.isRequired,
|
||||||
onOpenVideo: PropTypes.func.isRequired,
|
onOpenVideo: PropTypes.func.isRequired,
|
||||||
onToggleHidden: PropTypes.func.isRequired,
|
onToggleHidden: PropTypes.func.isRequired,
|
||||||
|
measureHeight: PropTypes.bool,
|
||||||
|
onHeightChange: PropTypes.func,
|
||||||
|
domain: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
height: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleAccountClick = (e) => {
|
handleAccountClick = (e) => {
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
|
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);
|
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 () {
|
render () {
|
||||||
const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
|
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 media = '';
|
||||||
let applicationLink = '';
|
let applicationLink = '';
|
||||||
let reblogLink = '';
|
let reblogLink = '';
|
||||||
let reblogIcon = 'retweet';
|
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').size > 0) {
|
||||||
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
|
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') {
|
if (status.get('visibility') === 'private') {
|
||||||
reblogLink = <i className={`fa fa-${reblogIcon}`} />;
|
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 {
|
} else {
|
||||||
reblogLink = (<Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
reblogLink = (
|
||||||
<i className={`fa fa-${reblogIcon}`} />
|
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||||
<span className='detailed-status__reblogs'>
|
<i className={`fa fa-${reblogIcon}`} />
|
||||||
<FormattedNumber value={status.get('reblogs_count')} />
|
<span className='detailed-status__reblogs'>
|
||||||
</span>
|
<FormattedNumber value={status.get('reblogs_count')} />
|
||||||
</Link>);
|
</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 {
|
||||||
|
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 (
|
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'>
|
<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>
|
<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>
|
</a>
|
||||||
|
|
||||||
<StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
|
<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'>
|
<div className='detailed-status__meta'>
|
||||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
|
<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' />
|
<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'>
|
</a>{applicationLink} · {reblogLink} · {favouriteLink}
|
||||||
<i className='fa fa-star' />
|
|
||||||
<span className='detailed-status__favorites'>
|
|
||||||
<FormattedNumber value={status.get('favourites_count')} />
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</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;
|
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'
|
= javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous'
|
||||||
= render 'og'
|
= render 'og'
|
||||||
|
|
||||||
.landing-page.tag-page.alternative
|
#mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name)) } }
|
||||||
.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'
|
|
||||||
|
|
||||||
#modal-container
|
#modal-container
|
||||||
|
|
|
@ -98,6 +98,7 @@
|
||||||
"react-immutable-proptypes": "^2.1.0",
|
"react-immutable-proptypes": "^2.1.0",
|
||||||
"react-immutable-pure-component": "^1.1.1",
|
"react-immutable-pure-component": "^1.1.1",
|
||||||
"react-intl": "^2.7.2",
|
"react-intl": "^2.7.2",
|
||||||
|
"react-masonry-infinite": "^1.2.2",
|
||||||
"react-motion": "^0.5.2",
|
"react-motion": "^0.5.2",
|
||||||
"react-notification": "^6.8.4",
|
"react-notification": "^6.8.4",
|
||||||
"react-overlays": "^0.8.3",
|
"react-overlays": "^0.8.3",
|
||||||
|
|
|
@ -17,7 +17,7 @@ RSpec.describe TagsController, type: :controller do
|
||||||
|
|
||||||
it 'renders application layout' do
|
it 'renders application layout' do
|
||||||
get :show, params: { id: 'test', max_id: late.id }
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
28
yarn.lock
28
yarn.lock
|
@ -1681,6 +1681,13 @@ braces@^2.3.0, braces@^2.3.1:
|
||||||
split-string "^3.0.2"
|
split-string "^3.0.2"
|
||||||
to-regex "^3.0.1"
|
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:
|
brorand@^1.0.1:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
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"
|
resolved "https://registry.yarnpkg.com/kleur/-/kleur-2.0.2.tgz#b704f4944d95e255d038f0cb05fb8a602c55a300"
|
||||||
integrity sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ==
|
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:
|
lcid@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
|
resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
|
||||||
|
@ -7558,6 +7570,13 @@ react-immutable-pure-component@^1.1.1:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@types/react" "16.4.6"
|
"@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:
|
react-input-autosize@^2.2.1:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8"
|
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"
|
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
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:
|
react-motion@^0.5.2:
|
||||||
version "0.5.2"
|
version "0.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316"
|
resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316"
|
||||||
|
|
Loading…
Reference in New Issue