forked from cybrespace/mastodon
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:
parent
473a69ab18
commit
5131012505
|
@ -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}`);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue