* Add "Toots/Toots with replies/Media" tab below profile header * Add focal point display to account gallery timeline * Fix visual glitch of standalone GIFVmaster
@@ -120,7 +120,7 @@ export function refreshTimeline(timelineId, path, params = {}) { | |||
export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home'); | |||
export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public'); | |||
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 refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); | |||
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 expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public'); | |||
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 expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); | |||
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 = ''; | |||
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); | |||
if ('share' in navigator) { | |||
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); | |||
if (account.get('id') === me) { | |||
@@ -12,24 +12,26 @@ export default class MediaItem extends ImmutablePureComponent { | |||
render () { | |||
const { media } = this.props; | |||
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') { | |||
content = <span className='media-gallery__gifv__label'>GIF</span>; | |||
} | |||
if (!status.get('sensitive')) { | |||
style = { backgroundImage: `url(${media.get('preview_url')})` }; | |||
style.backgroundImage = `url(${media.get('preview_url')})`; | |||
style.backgroundPosition = `${x}% ${y}%`; | |||
} | |||
return ( | |||
<div className='account-gallery__item'> | |||
<Permalink | |||
to={`/statuses/${status.get('id')}`} | |||
href={status.get('url')} | |||
style={style} | |||
> | |||
<Permalink to={`/statuses/${status.get('id')}`} href={status.get('url')} style={style}> | |||
{content} | |||
</Permalink> | |||
</div> | |||
@@ -11,7 +11,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
import { getAccountGallery } from '../../selectors'; | |||
import MediaItem from './components/media_item'; | |||
import HeaderContainer from '../account_timeline/containers/header_container'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { ScrollContainer } from 'react-router-scroll-4'; | |||
import LoadMore from '../../components/load_more'; | |||
@@ -89,10 +88,6 @@ export default class AccountGallery extends ImmutablePureComponent { | |||
<div className='scrollable' onScroll={this.handleScroll}> | |||
<HeaderContainer accountId={this.props.params.accountId} /> | |||
<div className='account-section-headline'> | |||
<FormattedMessage id='account.media' defaultMessage='Media' /> | |||
</div> | |||
<div className='account-gallery__container'> | |||
{medias.map(media => ( | |||
<MediaItem | |||
@@ -6,6 +6,8 @@ import ActionBar from '../../account/components/action_bar'; | |||
import MissingIndicator from '../../../components/missing_indicator'; | |||
import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
import MovedNote from './moved_note'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { NavLink } from 'react-router-dom'; | |||
export default class Header extends ImmutablePureComponent { | |||
@@ -91,6 +93,12 @@ export default class Header extends ImmutablePureComponent { | |||
onBlockDomain={this.handleBlockDomain} | |||
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> | |||
); | |||
} | |||
@@ -12,11 +12,15 @@ import ColumnBackButton from '../../components/column_back_button'; | |||
import { List as ImmutableList } from 'immutable'; | |||
import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
const mapStateToProps = (state, props) => ({ | |||
statusIds: state.getIn(['timelines', `account:${props.params.accountId}`, 'items'], ImmutableList()), | |||
isLoading: state.getIn(['timelines', `account:${props.params.accountId}`, 'isLoading']), | |||
hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}`, 'next']), | |||
}); | |||
const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => { | |||
const path = withReplies ? `${accountId}:with_replies` : accountId; | |||
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) | |||
export default class AccountTimeline extends ImmutablePureComponent { | |||
@@ -27,23 +31,24 @@ export default class AccountTimeline extends ImmutablePureComponent { | |||
statusIds: ImmutablePropTypes.list, | |||
isLoading: PropTypes.bool, | |||
hasMore: PropTypes.bool, | |||
withReplies: PropTypes.bool, | |||
}; | |||
componentWillMount () { | |||
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) { | |||
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(refreshAccountTimeline(nextProps.params.accountId)); | |||
this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId, nextProps.params.withReplies)); | |||
} | |||
} | |||
handleScrollToBottom = () => { | |||
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='/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/following' component={Following} 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, | |||
content: PropTypes.node, | |||
multiColumn: PropTypes.bool, | |||
} | |||
componentParams: PropTypes.object, | |||
}; | |||
static defaultProps = { | |||
componentParams: {}, | |||
}; | |||
renderComponent = ({ match }) => { | |||
const { component, content, multiColumn } = this.props; | |||
const { component, content, multiColumn, componentParams } = this.props; | |||
return ( | |||
<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> | |||
); | |||
} | |||
@@ -4230,6 +4230,7 @@ a.status-card { | |||
&.standalone { | |||
.media-gallery__item-gifv-thumbnail { | |||
transform: none; | |||
top: 0; | |||
} | |||
} | |||
} | |||
@@ -4579,64 +4580,94 @@ a.status-card { | |||
/* End Video Player */ | |||
.account-gallery__container { | |||
margin: -2px; | |||
padding: 4px; | |||
display: flex; | |||
justify-content: center; | |||
flex-wrap: wrap; | |||
padding: 2px; | |||
} | |||
.account-gallery__item { | |||
flex: 1 1 auto; | |||
width: calc(100% / 3 - 4px); | |||
height: 95px; | |||
margin: 2px; | |||
flex-grow: 1; | |||
width: 50%; | |||
overflow: hidden; | |||
position: relative; | |||
&::before { | |||
content: ""; | |||
display: block; | |||
padding-top: 100%; | |||
} | |||
a { | |||
display: block; | |||
width: 100%; | |||
height: 100%; | |||
width: calc(100% - 4px); | |||
height: calc(100% - 4px); | |||
margin: 2px; | |||
top: 0; | |||
left: 0; | |||
background-color: $base-overlay-background; | |||
background-size: cover; | |||
background-position: center; | |||
position: relative; | |||
position: absolute; | |||
color: inherit; | |||
text-decoration: none; | |||
border-radius: 4px; | |||
&:hover, | |||
&:active, | |||
&:focus { | |||
outline: 0; | |||
&::before { | |||
content: ""; | |||
display: block; | |||
width: 100%; | |||
height: 100%; | |||
background: rgba($base-overlay-background, 0.3); | |||
border-radius: 4px; | |||
} | |||
} | |||
} | |||
} | |||
.account-section-headline { | |||
color: $ui-base-lighter-color; | |||
.account__section-headline { | |||
background: lighten($ui-base-color, 2%); | |||
border-bottom: 1px solid lighten($ui-base-color, 4%); | |||
padding: 15px 10px; | |||
font-size: 14px; | |||
font-weight: 500; | |||
position: relative; | |||
cursor: default; | |||
display: flex; | |||
&::before, | |||
&::after { | |||
a { | |||
display: block; | |||
content: ""; | |||
position: absolute; | |||
bottom: 0; | |||
left: 18px; | |||
width: 0; | |||
height: 0; | |||
border-style: solid; | |||
border-width: 0 10px 10px; | |||
border-color: transparent transparent lighten($ui-base-color, 4%); | |||
} | |||
color: $ui-base-lighter-color; | |||
padding: 15px 10px; | |||
font-size: 14px; | |||
font-weight: 500; | |||
text-decoration: none; | |||
position: relative; | |||
&::after { | |||
bottom: -1px; | |||
border-color: transparent transparent $ui-base-color; | |||
&.active { | |||
color: $ui-highlight-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; | |||
} | |||
} | |||
} | |||
} | |||