Add "Toots/Toots with replies/Media" tab below profile header (#6572)

* Add "Toots/Toots with replies/Media" tab below profile header

* Add focal point display to account gallery timeline

* Fix visual glitch of standalone GIFV
This commit is contained in:
Eugen Rochko 2018-03-01 02:48:44 +01:00 committed by GitHub
parent 473a69ab18
commit 5131012505
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 105 additions and 58 deletions

View file

@ -120,7 +120,7 @@ export function refreshTimeline(timelineId, path, params = {}) {
export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home'); export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home');
export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public'); export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public');
export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true }); export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); export const refreshAccountTimeline = (accountId, withReplies) => refreshTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies });
export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true }); export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
export const refreshListTimeline = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`); export const refreshListTimeline = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
@ -161,7 +161,7 @@ export function expandTimeline(timelineId, path, params = {}) {
export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home'); export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home');
export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public'); export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public');
export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true }); export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true });
export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); export const expandAccountTimeline = (accountId, withReplies) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies });
export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true }); export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
export const expandListTimeline = id => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`); export const expandListTimeline = id => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);

View file

@ -53,11 +53,11 @@ export default class ActionBar extends React.PureComponent {
let extraInfo = ''; let extraInfo = '';
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
if ('share' in navigator) { if ('share' in navigator) {
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
} }
menu.push(null);
menu.push({ text: intl.formatMessage(messages.media), to: `/accounts/${account.get('id')}/media` });
menu.push(null); menu.push(null);
if (account.get('id') === me) { if (account.get('id') === me) {

View file

@ -12,24 +12,26 @@ export default class MediaItem extends ImmutablePureComponent {
render () { render () {
const { media } = this.props; const { media } = this.props;
const status = media.get('status'); const status = media.get('status');
const focusX = media.getIn(['meta', 'focus', 'x']);
const focusY = media.getIn(['meta', 'focus', 'y']);
const x = ((focusX / 2) + .5) * 100;
const y = ((focusY / -2) + .5) * 100;
const style = {};
let content, style; let content;
if (media.get('type') === 'gifv') { if (media.get('type') === 'gifv') {
content = <span className='media-gallery__gifv__label'>GIF</span>; content = <span className='media-gallery__gifv__label'>GIF</span>;
} }
if (!status.get('sensitive')) { if (!status.get('sensitive')) {
style = { backgroundImage: `url(${media.get('preview_url')})` }; style.backgroundImage = `url(${media.get('preview_url')})`;
style.backgroundPosition = `${x}% ${y}%`;
} }
return ( return (
<div className='account-gallery__item'> <div className='account-gallery__item'>
<Permalink <Permalink to={`/statuses/${status.get('id')}`} href={status.get('url')} style={style}>
to={`/statuses/${status.get('id')}`}
href={status.get('url')}
style={style}
>
{content} {content}
</Permalink> </Permalink>
</div> </div>

View file

@ -11,7 +11,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { getAccountGallery } from '../../selectors'; import { getAccountGallery } from '../../selectors';
import MediaItem from './components/media_item'; import MediaItem from './components/media_item';
import HeaderContainer from '../account_timeline/containers/header_container'; import HeaderContainer from '../account_timeline/containers/header_container';
import { FormattedMessage } from 'react-intl';
import { ScrollContainer } from 'react-router-scroll-4'; import { ScrollContainer } from 'react-router-scroll-4';
import LoadMore from '../../components/load_more'; import LoadMore from '../../components/load_more';
@ -89,10 +88,6 @@ export default class AccountGallery extends ImmutablePureComponent {
<div className='scrollable' onScroll={this.handleScroll}> <div className='scrollable' onScroll={this.handleScroll}>
<HeaderContainer accountId={this.props.params.accountId} /> <HeaderContainer accountId={this.props.params.accountId} />
<div className='account-section-headline'>
<FormattedMessage id='account.media' defaultMessage='Media' />
</div>
<div className='account-gallery__container'> <div className='account-gallery__container'>
{medias.map(media => ( {medias.map(media => (
<MediaItem <MediaItem

View file

@ -6,6 +6,8 @@ import ActionBar from '../../account/components/action_bar';
import MissingIndicator from '../../../components/missing_indicator'; import MissingIndicator from '../../../components/missing_indicator';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import MovedNote from './moved_note'; import MovedNote from './moved_note';
import { FormattedMessage } from 'react-intl';
import { NavLink } from 'react-router-dom';
export default class Header extends ImmutablePureComponent { export default class Header extends ImmutablePureComponent {
@ -91,6 +93,12 @@ export default class Header extends ImmutablePureComponent {
onBlockDomain={this.handleBlockDomain} onBlockDomain={this.handleBlockDomain}
onUnblockDomain={this.handleUnblockDomain} onUnblockDomain={this.handleUnblockDomain}
/> />
<div className='account__section-headline'>
<NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
</div>
</div> </div>
); );
} }

View file

@ -12,11 +12,15 @@ import ColumnBackButton from '../../components/column_back_button';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => {
statusIds: state.getIn(['timelines', `account:${props.params.accountId}`, 'items'], ImmutableList()), const path = withReplies ? `${accountId}:with_replies` : accountId;
isLoading: state.getIn(['timelines', `account:${props.params.accountId}`, 'isLoading']),
hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}`, 'next']), return {
}); statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
hasMore: !!state.getIn(['timelines', `account:${path}`, 'next']),
};
};
@connect(mapStateToProps) @connect(mapStateToProps)
export default class AccountTimeline extends ImmutablePureComponent { export default class AccountTimeline extends ImmutablePureComponent {
@ -27,23 +31,24 @@ export default class AccountTimeline extends ImmutablePureComponent {
statusIds: ImmutablePropTypes.list, statusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
withReplies: PropTypes.bool,
}; };
componentWillMount () { componentWillMount () {
this.props.dispatch(fetchAccount(this.props.params.accountId)); this.props.dispatch(fetchAccount(this.props.params.accountId));
this.props.dispatch(refreshAccountTimeline(this.props.params.accountId)); this.props.dispatch(refreshAccountTimeline(this.props.params.accountId, this.props.withReplies));
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
this.props.dispatch(fetchAccount(nextProps.params.accountId)); this.props.dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId)); this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId, nextProps.params.withReplies));
} }
} }
handleScrollToBottom = () => { handleScrollToBottom = () => {
if (!this.props.isLoading && this.props.hasMore) { if (!this.props.isLoading && this.props.hasMore) {
this.props.dispatch(expandAccountTimeline(this.props.params.accountId)); this.props.dispatch(expandAccountTimeline(this.props.params.accountId, this.props.withReplies));
} }
} }

View file

@ -398,6 +398,7 @@ export default class UI extends React.Component {
<WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} /> <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
<WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} /> <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} />
<WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
<WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} /> <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} />
<WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} /> <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} />
<WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} /> <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} />

View file

@ -35,14 +35,19 @@ export class WrappedRoute extends React.Component {
component: PropTypes.func.isRequired, component: PropTypes.func.isRequired,
content: PropTypes.node, content: PropTypes.node,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
} componentParams: PropTypes.object,
};
static defaultProps = {
componentParams: {},
};
renderComponent = ({ match }) => { renderComponent = ({ match }) => {
const { component, content, multiColumn } = this.props; const { component, content, multiColumn, componentParams } = this.props;
return ( return (
<BundleContainer fetchComponent={component} loading={this.renderLoading} error={this.renderError}> <BundleContainer fetchComponent={component} loading={this.renderLoading} error={this.renderError}>
{Component => <Component params={match.params} multiColumn={multiColumn}>{content}</Component>} {Component => <Component params={match.params} multiColumn={multiColumn} {...componentParams}>{content}</Component>}
</BundleContainer> </BundleContainer>
); );
} }

View file

@ -4230,6 +4230,7 @@ a.status-card {
&.standalone { &.standalone {
.media-gallery__item-gifv-thumbnail { .media-gallery__item-gifv-thumbnail {
transform: none; transform: none;
top: 0;
} }
} }
} }
@ -4579,64 +4580,94 @@ a.status-card {
/* End Video Player */ /* End Video Player */
.account-gallery__container { .account-gallery__container {
margin: -2px;
padding: 4px;
display: flex; display: flex;
justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
padding: 2px;
} }
.account-gallery__item { .account-gallery__item {
flex: 1 1 auto; flex-grow: 1;
width: calc(100% / 3 - 4px); width: 50%;
height: 95px; overflow: hidden;
margin: 2px; position: relative;
&::before {
content: "";
display: block;
padding-top: 100%;
}
a { a {
display: block; display: block;
width: 100%; width: calc(100% - 4px);
height: 100%; height: calc(100% - 4px);
margin: 2px;
top: 0;
left: 0;
background-color: $base-overlay-background; background-color: $base-overlay-background;
background-size: cover; background-size: cover;
background-position: center; background-position: center;
position: relative; position: absolute;
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
border-radius: 4px;
&:hover, &:hover,
&:active, &:active,
&:focus { &:focus {
outline: 0; outline: 0;
&::before {
content: "";
display: block;
width: 100%;
height: 100%;
background: rgba($base-overlay-background, 0.3);
border-radius: 4px;
}
} }
} }
} }
.account-section-headline { .account__section-headline {
color: $ui-base-lighter-color;
background: lighten($ui-base-color, 2%); background: lighten($ui-base-color, 2%);
border-bottom: 1px solid lighten($ui-base-color, 4%); border-bottom: 1px solid lighten($ui-base-color, 4%);
padding: 15px 10px;
font-size: 14px;
font-weight: 500;
position: relative;
cursor: default; cursor: default;
display: flex;
&::before, a {
&::after {
display: block; display: block;
content: ""; color: $ui-base-lighter-color;
position: absolute; padding: 15px 10px;
bottom: 0; font-size: 14px;
left: 18px; font-weight: 500;
width: 0; text-decoration: none;
height: 0; position: relative;
border-style: solid;
border-width: 0 10px 10px;
border-color: transparent transparent lighten($ui-base-color, 4%);
}
&::after { &.active {
bottom: -1px; color: $ui-highlight-color;
border-color: transparent transparent $ui-base-color;
&::before,
&::after {
display: block;
content: "";
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 0;
transform: translateX(-50%);
border-style: solid;
border-width: 0 10px 10px;
border-color: transparent transparent lighten($ui-base-color, 4%);
}
&::after {
bottom: -1px;
border-color: transparent transparent $ui-base-color;
}
}
} }
} }