Merge pull request #388 from ThibG/glitch-soc/federated-pinned-toots
Port WebUI toots/with reply/media tabs and pinned toots to glitchmain
commit
a387f99659
|
@ -121,7 +121,8 @@ export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v
|
|||
export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public');
|
||||
export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
|
||||
export const refreshDirectTimeline = () => refreshTimeline('direct', '/api/v1/timelines/direct');
|
||||
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 refreshAccountFeaturedTimeline = accountId => refreshTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: 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 refreshListTimeline = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
|
||||
|
@ -163,7 +164,7 @@ export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/
|
|||
export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public');
|
||||
export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true });
|
||||
export const expandDirectTimeline = () => expandTimeline('direct', '/api/v1/timelines/direct');
|
||||
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}`);
|
||||
|
|
|
@ -11,6 +11,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
scrollKey: PropTypes.string.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
featuredStatusIds: ImmutablePropTypes.list,
|
||||
onScrollToBottom: PropTypes.func,
|
||||
onScrollToTop: PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
|
@ -50,7 +51,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { statusIds, ...other } = this.props;
|
||||
const { statusIds, featuredStatusIds, ...other } = this.props;
|
||||
const { isLoading, isPartial } = other;
|
||||
|
||||
if (isPartial) {
|
||||
|
@ -68,8 +69,8 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
const scrollableContent = (isLoading || statusIds.size > 0) ? (
|
||||
statusIds.map((statusId) => (
|
||||
let scrollableContent = (isLoading || statusIds.size > 0) ? (
|
||||
statusIds.map(statusId => (
|
||||
<StatusContainer
|
||||
key={statusId}
|
||||
id={statusId}
|
||||
|
@ -79,6 +80,18 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
))
|
||||
) : null;
|
||||
|
||||
if (scrollableContent && featuredStatusIds) {
|
||||
scrollableContent = featuredStatusIds.map(statusId => (
|
||||
<StatusContainer
|
||||
key={`f-${statusId}`}
|
||||
id={statusId}
|
||||
featured
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
/>
|
||||
)).concat(scrollableContent);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollableList {...other} ref={this.setRef}>
|
||||
{scrollableContent}
|
||||
|
|
|
@ -34,6 +34,10 @@ export default class StatusPrepend extends React.PureComponent {
|
|||
</a>
|
||||
);
|
||||
switch (type) {
|
||||
case 'featured':
|
||||
return (
|
||||
<FormattedMessage id='status.pinned' defaultMessage='Pinned toot' />
|
||||
);
|
||||
case 'reblogged_by':
|
||||
return (
|
||||
<FormattedMessage
|
||||
|
@ -67,11 +71,11 @@ export default class StatusPrepend extends React.PureComponent {
|
|||
const { type } = this.props;
|
||||
|
||||
return !type ? null : (
|
||||
<aside className={type === 'reblogged_by' ? 'status__prepend' : 'notification__message'}>
|
||||
<div className={type === 'reblogged_by' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}>
|
||||
<aside className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend' : 'notification__message'}>
|
||||
<div className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}>
|
||||
<i
|
||||
className={`fa fa-fw fa-${
|
||||
type === 'favourite' ? 'star star-icon' : 'retweet'
|
||||
type === 'favourite' ? 'star star-icon' : (type === 'featured' ? 'thumb-tack' : 'retweet')
|
||||
} status__prepend-icon`}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -38,7 +38,10 @@ const makeMapStateToProps = () => {
|
|||
let account = undefined;
|
||||
let prepend = undefined;
|
||||
|
||||
if (reblogStatus !== null && typeof reblogStatus === 'object') {
|
||||
if (props.featured) {
|
||||
account = status.get('account');
|
||||
prepend = 'featured';
|
||||
} else if (reblogStatus !== null && typeof reblogStatus === 'object') {
|
||||
account = status.get('account');
|
||||
status = reblogStatus;
|
||||
prepend = 'reblogged_by';
|
||||
|
|
|
@ -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 'flavours/glitch/selectors';
|
||||
import MediaItem from './components/media_item';
|
||||
import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { ScrollContainer } from 'react-router-scroll-4';
|
||||
import LoadMore from 'flavours/glitch/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
|
||||
|
|
|
@ -5,6 +5,8 @@ import InnerHeader from 'flavours/glitch/features/account/components/header';
|
|||
import ActionBar from 'flavours/glitch/features/account/components/action_bar';
|
||||
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
export default class Header extends ImmutablePureComponent {
|
||||
|
||||
|
@ -18,6 +20,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
onMute: PropTypes.func.isRequired,
|
||||
onBlockDomain: PropTypes.func.isRequired,
|
||||
onUnblockDomain: PropTypes.func.isRequired,
|
||||
hideTabs: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
|
@ -65,7 +68,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { account } = this.props;
|
||||
const { account, hideTabs } = this.props;
|
||||
|
||||
if (account === null) {
|
||||
return <MissingIndicator />;
|
||||
|
@ -89,6 +92,14 @@ export default class Header extends ImmutablePureComponent {
|
|||
onBlockDomain={this.handleBlockDomain}
|
||||
onUnblockDomain={this.handleUnblockDomain}
|
||||
/>
|
||||
|
||||
{!hideTabs && (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { fetchAccount } from 'flavours/glitch/actions/accounts';
|
||||
import { refreshAccountTimeline, expandAccountTimeline } from 'flavours/glitch/actions/timelines';
|
||||
import { refreshAccountTimeline, refreshAccountFeaturedTimeline, expandAccountTimeline } from 'flavours/glitch/actions/timelines';
|
||||
import StatusList from '../../components/status_list';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import Column from '../ui/components/column';
|
||||
|
@ -12,11 +12,16 @@ 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()),
|
||||
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
|
||||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||
hasMore: !!state.getIn(['timelines', `account:${path}`, 'next']),
|
||||
};
|
||||
};
|
||||
|
||||
@connect(mapStateToProps)
|
||||
export default class AccountTimeline extends ImmutablePureComponent {
|
||||
|
@ -25,30 +30,36 @@ export default class AccountTimeline extends ImmutablePureComponent {
|
|||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list,
|
||||
featuredStatusIds: 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));
|
||||
const { params: { accountId }, withReplies } = this.props;
|
||||
|
||||
this.props.dispatch(fetchAccount(accountId));
|
||||
this.props.dispatch(refreshAccountFeaturedTimeline(accountId));
|
||||
this.props.dispatch(refreshAccountTimeline(accountId, 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(refreshAccountFeaturedTimeline(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));
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { statusIds, isLoading, hasMore } = this.props;
|
||||
const { statusIds, featuredStatusIds, isLoading, hasMore } = this.props;
|
||||
|
||||
if (!statusIds && isLoading) {
|
||||
return (
|
||||
|
@ -66,6 +77,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
|
|||
prepend={<HeaderContainer accountId={this.props.params.accountId} />}
|
||||
scrollKey='account_timeline'
|
||||
statusIds={statusIds}
|
||||
featuredStatusIds={featuredStatusIds}
|
||||
isLoading={isLoading}
|
||||
hasMore={hasMore}
|
||||
onScrollToBottom={this.handleScrollToBottom}
|
||||
|
|
|
@ -80,7 +80,7 @@ export default class Followers extends ImmutablePureComponent {
|
|||
<ScrollContainer scrollKey='followers'>
|
||||
<div className='scrollable' onScroll={this.handleScroll}>
|
||||
<div className='followers'>
|
||||
<HeaderContainer accountId={this.props.params.accountId} />
|
||||
<HeaderContainer accountId={this.props.params.accountId} hideTabs />
|
||||
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
|
||||
{loadMore}
|
||||
</div>
|
||||
|
|
|
@ -80,7 +80,7 @@ export default class Following extends ImmutablePureComponent {
|
|||
<ScrollContainer scrollKey='following'>
|
||||
<div className='scrollable' onScroll={this.handleScroll}>
|
||||
<div className='following'>
|
||||
<HeaderContainer accountId={this.props.params.accountId} />
|
||||
<HeaderContainer accountId={this.props.params.accountId} hideTabs />
|
||||
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
|
||||
{loadMore}
|
||||
</div>
|
||||
|
|
|
@ -431,6 +431,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} />
|
||||
|
|
|
@ -380,46 +380,75 @@
|
|||
}
|
||||
|
||||
.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;
|
||||
background: lighten($ui-base-color, 2%);
|
||||
border-bottom: 1px solid lighten($ui-base-color, 4%);
|
||||
padding: 15px 10px;
|
||||
.account__section-headline {
|
||||
background: darken($ui-base-color, 4%);
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
cursor: default;
|
||||
display: flex;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
color: $ui-primary-color;
|
||||
padding: 15px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
cursor: default;
|
||||
|
||||
&.active {
|
||||
color: $ui-secondary-color;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
|
@ -427,18 +456,21 @@
|
|||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 18px;
|
||||
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%);
|
||||
border-color: transparent transparent lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: -1px;
|
||||
border-color: transparent transparent $ui-base-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account__moved-note {
|
||||
|
|
|
@ -102,6 +102,7 @@
|
|||
&.standalone {
|
||||
.media-gallery__item-gifv-thumbnail {
|
||||
transform: none;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue