[Glitch] Federate pinned statuses over ActivityPub

Port 9110db41c5 to glitch
This commit is contained in:
Thibaut Girka 2018-03-16 20:29:42 +01:00
parent cd73af3bd0
commit 6f0e50f9a0
8 changed files with 58 additions and 27 deletions

View File

@ -117,14 +117,15 @@ 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 refreshDirectTimeline = () => refreshTimeline('direct', '/api/v1/timelines/direct'); export const refreshDirectTimeline = () => refreshTimeline('direct', '/api/v1/timelines/direct');
export const refreshAccountTimeline = (accountId, withReplies) => refreshTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies }); 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 refreshAccountFeaturedTimeline = accountId => refreshTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const refreshListTimeline = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`); 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 function refreshTimelineFail(timeline, error, skipLoading) { export function refreshTimelineFail(timeline, error, skipLoading) {
return { return {

View File

@ -11,6 +11,7 @@ export default class StatusList extends ImmutablePureComponent {
static propTypes = { static propTypes = {
scrollKey: PropTypes.string.isRequired, scrollKey: PropTypes.string.isRequired,
statusIds: ImmutablePropTypes.list.isRequired, statusIds: ImmutablePropTypes.list.isRequired,
featuredStatusIds: ImmutablePropTypes.list,
onScrollToBottom: PropTypes.func, onScrollToBottom: PropTypes.func,
onScrollToTop: PropTypes.func, onScrollToTop: PropTypes.func,
onScroll: PropTypes.func, onScroll: PropTypes.func,
@ -50,7 +51,7 @@ export default class StatusList extends ImmutablePureComponent {
} }
render () { render () {
const { statusIds, ...other } = this.props; const { statusIds, featuredStatusIds, ...other } = this.props;
const { isLoading, isPartial } = other; const { isLoading, isPartial } = other;
if (isPartial) { if (isPartial) {
@ -68,8 +69,8 @@ export default class StatusList extends ImmutablePureComponent {
); );
} }
const scrollableContent = (isLoading || statusIds.size > 0) ? ( let scrollableContent = (isLoading || statusIds.size > 0) ? (
statusIds.map((statusId) => ( statusIds.map(statusId => (
<StatusContainer <StatusContainer
key={statusId} key={statusId}
id={statusId} id={statusId}
@ -79,6 +80,18 @@ export default class StatusList extends ImmutablePureComponent {
)) ))
) : null; ) : null;
if (scrollableContent && featuredStatusIds) {
scrollableContent = featuredStatusIds.map(statusId => (
<StatusContainer
key={`f-${statusId}`}
id={statusId}
featured
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
/>
)).concat(scrollableContent);
}
return ( return (
<ScrollableList {...other} ref={this.setRef}> <ScrollableList {...other} ref={this.setRef}>
{scrollableContent} {scrollableContent}

View File

@ -34,6 +34,10 @@ export default class StatusPrepend extends React.PureComponent {
</a> </a>
); );
switch (type) { switch (type) {
case 'featured':
return (
<FormattedMessage id='status.pinned' defaultMessage='Pinned toot' />
);
case 'reblogged_by': case 'reblogged_by':
return ( return (
<FormattedMessage <FormattedMessage
@ -67,11 +71,11 @@ export default class StatusPrepend extends React.PureComponent {
const { type } = this.props; const { type } = this.props;
return !type ? null : ( return !type ? null : (
<aside className={type === 'reblogged_by' ? 'status__prepend' : 'notification__message'}> <aside className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend' : 'notification__message'}>
<div className={type === 'reblogged_by' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}> <div className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}>
<i <i
className={`fa fa-fw fa-${ className={`fa fa-fw fa-${
type === 'favourite' ? 'star star-icon' : 'retweet' type === 'favourite' ? 'star star-icon' : (type === 'featured' ? 'thumb-tack' : 'retweet')
} status__prepend-icon`} } status__prepend-icon`}
/> />
</div> </div>

View File

@ -38,7 +38,10 @@ const makeMapStateToProps = () => {
let account = undefined; let account = undefined;
let prepend = 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'); account = status.get('account');
status = reblogStatus; status = reblogStatus;
prepend = 'reblogged_by'; prepend = 'reblogged_by';

View File

@ -20,6 +20,7 @@ export default class Header extends ImmutablePureComponent {
onMute: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired, onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired, onUnblockDomain: PropTypes.func.isRequired,
hideTabs: PropTypes.bool,
}; };
static contextTypes = { static contextTypes = {
@ -67,7 +68,7 @@ export default class Header extends ImmutablePureComponent {
} }
render () { render () {
const { account } = this.props; const { account, hideTabs } = this.props;
if (account === null) { if (account === null) {
return <MissingIndicator />; return <MissingIndicator />;
@ -92,11 +93,13 @@ export default class Header extends ImmutablePureComponent {
onUnblockDomain={this.handleUnblockDomain} onUnblockDomain={this.handleUnblockDomain}
/> />
<div className='account__section-headline'> {!hideTabs && (
<NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink> <div className='account__section-headline'>
<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')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink> <NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>
</div> <NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
</div>
)}
</div> </div>
); );
} }

View File

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { fetchAccount } from 'flavours/glitch/actions/accounts'; 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 StatusList from '../../components/status_list';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
@ -17,6 +17,7 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false })
return { return {
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()), 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']), isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
hasMore: !!state.getIn(['timelines', `account:${path}`, 'next']), hasMore: !!state.getIn(['timelines', `account:${path}`, 'next']),
}; };
@ -29,19 +30,24 @@ export default class AccountTimeline extends ImmutablePureComponent {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list, statusIds: ImmutablePropTypes.list,
featuredStatusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
withReplies: PropTypes.bool, withReplies: PropTypes.bool,
}; };
componentWillMount () { componentWillMount () {
this.props.dispatch(fetchAccount(this.props.params.accountId)); const { params: { accountId }, withReplies } = this.props;
this.props.dispatch(refreshAccountTimeline(this.props.params.accountId, this.props.withReplies));
this.props.dispatch(fetchAccount(accountId));
this.props.dispatch(refreshAccountFeaturedTimeline(accountId));
this.props.dispatch(refreshAccountTimeline(accountId, withReplies));
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { 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(refreshAccountFeaturedTimeline(nextProps.params.accountId));
this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId, nextProps.params.withReplies)); this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId, nextProps.params.withReplies));
} }
} }
@ -53,7 +59,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
} }
render () { render () {
const { statusIds, isLoading, hasMore } = this.props; const { statusIds, featuredStatusIds, isLoading, hasMore } = this.props;
if (!statusIds && isLoading) { if (!statusIds && isLoading) {
return ( return (
@ -71,6 +77,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
prepend={<HeaderContainer accountId={this.props.params.accountId} />} prepend={<HeaderContainer accountId={this.props.params.accountId} />}
scrollKey='account_timeline' scrollKey='account_timeline'
statusIds={statusIds} statusIds={statusIds}
featuredStatusIds={featuredStatusIds}
isLoading={isLoading} isLoading={isLoading}
hasMore={hasMore} hasMore={hasMore}
onScrollToBottom={this.handleScrollToBottom} onScrollToBottom={this.handleScrollToBottom}

View File

@ -80,7 +80,7 @@ export default class Followers extends ImmutablePureComponent {
<ScrollContainer scrollKey='followers'> <ScrollContainer scrollKey='followers'>
<div className='scrollable' onScroll={this.handleScroll}> <div className='scrollable' onScroll={this.handleScroll}>
<div className='followers'> <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} />)} {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
{loadMore} {loadMore}
</div> </div>

View File

@ -80,7 +80,7 @@ export default class Following extends ImmutablePureComponent {
<ScrollContainer scrollKey='following'> <ScrollContainer scrollKey='following'>
<div className='scrollable' onScroll={this.handleScroll}> <div className='scrollable' onScroll={this.handleScroll}>
<div className='following'> <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} />)} {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
{loadMore} {loadMore}
</div> </div>