Add list of users who reacted
This commit is contained in:
parent
9862e1591e
commit
a4bfa67151
8 changed files with 256 additions and 4 deletions
|
@ -7,6 +7,10 @@ export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
|||
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
||||
export const REBLOG_FAIL = 'REBLOG_FAIL';
|
||||
|
||||
export const REACTIONS_EXPAND_REQUEST = 'REACTIONS_EXPAND_REQUEST';
|
||||
export const REACTIONS_EXPAND_SUCCESS = 'REACTIONS_EXPAND_SUCCESS';
|
||||
export const REACTIONS_EXPAND_FAIL = 'REACTIONS_EXPAND_FAIL';
|
||||
|
||||
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
||||
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
||||
export const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
|
||||
|
@ -23,6 +27,10 @@ export const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST';
|
|||
export const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
|
||||
export const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL';
|
||||
|
||||
export const REACTIONS_FETCH_REQUEST = 'REACTIONS_FETCH_REQUEST';
|
||||
export const REACTIONS_FETCH_SUCCESS = 'REACTIONS_FETCH_SUCCESS';
|
||||
export const REACTIONS_FETCH_FAIL = 'REACTIONS_FETCH_FAIL';
|
||||
|
||||
export const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
|
||||
export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
|
||||
export const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL';
|
||||
|
@ -287,6 +295,90 @@ export function unbookmarkFail(status, error) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchReactions(id) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(fetchReactionsRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/reactions`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
const accounts = response.data.map(item => item.account);
|
||||
dispatch(importFetchedAccounts(accounts));
|
||||
dispatch(fetchReactionsSuccess(id, accounts, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(accounts.map(item => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(fetchReactionsFail(id, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchReactionsRequest(id) {
|
||||
return {
|
||||
type: REACTIONS_FETCH_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchReactionsSuccess(id, accounts, next) {
|
||||
return {
|
||||
type: REACTIONS_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchReactionsFail(id, error) {
|
||||
return {
|
||||
type: REACTIONS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandReactions(id) {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['user_lists', 'reactions', id, 'next']);
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandReactionsRequest(id));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
const accounts = response.data.map(item => item.account);
|
||||
|
||||
dispatch(importFetchedAccounts(accounts));
|
||||
dispatch(expandReactionsSuccess(id, accounts, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(accounts.map(item => item.id)));
|
||||
}).catch(error => dispatch(expandReactionsFail(id, error)));
|
||||
};
|
||||
}
|
||||
|
||||
export function expandReactionsRequest(id) {
|
||||
return {
|
||||
type: REACTIONS_EXPAND_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandReactionsSuccess(id, accounts, next) {
|
||||
return {
|
||||
type: REACTIONS_EXPAND_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandReactionsFail(id, error) {
|
||||
return {
|
||||
type: REACTIONS_EXPAND_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchReblogs(id) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(fetchReblogsRequest(id));
|
||||
|
|
|
@ -32,7 +32,7 @@ import StatusContent from './status_content';
|
|||
import StatusHeader from './status_header';
|
||||
import StatusIcons from './status_icons';
|
||||
import StatusPrepend from './status_prepend';
|
||||
import StatusReactions from './status_reactions';
|
||||
import { StatusReactions } from './status_reactions';
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import { assetHost } from '../utils/config';
|
|||
|
||||
import { AnimatedNumber } from './animated_number';
|
||||
|
||||
export default class StatusReactions extends ImmutablePureComponent {
|
||||
export class StatusReactions extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
statusId: PropTypes.string.isRequired,
|
||||
|
@ -107,6 +107,7 @@ class Reaction extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className={classNames('reactions-bar__item', { active: reaction.get('me') })}
|
||||
onClick={this.handleClick}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
|
@ -131,7 +132,7 @@ class Reaction extends ImmutablePureComponent {
|
|||
|
||||
}
|
||||
|
||||
class Emoji extends React.PureComponent {
|
||||
export class Emoji extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
emoji: PropTypes.string.isRequired,
|
||||
|
|
117
app/javascript/flavours/glitch/features/reactions/index.jsx
Normal file
117
app/javascript/flavours/glitch/features/reactions/index.jsx
Normal file
|
@ -0,0 +1,117 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import MoodIcon from '@/material-icons/400-24px/mood.svg?react';
|
||||
import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
import { fetchReactions, expandReactions } from '../../actions/interactions';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.reacted_by', defaultMessage: 'Reacted by' },
|
||||
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
accountIds: state.getIn(['user_lists', 'reactions', props.params.statusId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'reactions', props.params.statusId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'reactions', props.params.statusId, 'isLoading'], true),
|
||||
});
|
||||
|
||||
class Reactions extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
UNSAFE_componentWillMount () {
|
||||
if (!this.props.accountIds) {
|
||||
this.props.dispatch(fetchReactions(this.props.params.statusId));
|
||||
}
|
||||
}
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
};
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
};
|
||||
|
||||
handleRefresh = () => {
|
||||
this.props.dispatch(fetchReactions(this.props.params.statusId));
|
||||
};
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandReactions(this.props.params.statusId));
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />;
|
||||
|
||||
return (
|
||||
<Column ref={this.setRef}>
|
||||
<ColumnHeader
|
||||
icon='mood'
|
||||
iconComponent={MoodIcon}
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
onClick={this.handleHeaderClick}
|
||||
showBackButton
|
||||
multiColumn={multiColumn}
|
||||
extraButton={(
|
||||
<button type='button' className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' icon={RefreshIcon} /></button>
|
||||
)}
|
||||
/>
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='reactions'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} withNote={false} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Reactions));
|
|
@ -21,7 +21,7 @@ import { Avatar } from '../../../components/avatar';
|
|||
import { DisplayName } from '../../../components/display_name';
|
||||
import MediaGallery from '../../../components/media_gallery';
|
||||
import StatusContent from '../../../components/status_content';
|
||||
import StatusReactions from '../../../components/status_reactions';
|
||||
import { StatusReactions } from '../../../components/status_reactions';
|
||||
import Audio from '../../audio';
|
||||
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
|
||||
import Video from '../../video';
|
||||
|
@ -137,6 +137,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
let applicationLink = '';
|
||||
let reblogLink = '';
|
||||
let favouriteLink = '';
|
||||
let reactionLink = '';
|
||||
|
||||
// Depending on user settings, some media are considered as parts of the
|
||||
// contents (affected by CW) while other will be displayed outside of the
|
||||
|
@ -275,6 +276,14 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
||||
</Link>
|
||||
);
|
||||
reactionLink = (
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reactions`} className='detailed-status__link'>
|
||||
<span className='detailed-status__reactions'>
|
||||
<AnimatedNumber value={status.get('reactions').reduce((total, obj) => total + obj.get('count'), 0)} />
|
||||
</span>
|
||||
<FormattedMessage id='status.reactions' defaultMessage='{count, plural, one {reaction} other {reactions}}' values={{ count: status.get('reactions').reduce((total, obj) => total + obj.get('count'), 0) }} />
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
favouriteLink = (
|
||||
<a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||
|
@ -284,6 +293,14 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
||||
</a>
|
||||
);
|
||||
reactionLink = (
|
||||
<a href={`/interact/${status.get('id')}?type=reaction`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||
<span className='detailed-status__reactions'>
|
||||
<AnimatedNumber value={status.get('reactions').reduce((total, obj) => total + obj.get('count'), 0)} />
|
||||
</span>
|
||||
<FormattedMessage id='status.reactions' defaultMessage='{count, plural, one {reaction} other {reactions}}' values={{ count: status.get('reactions').reduce((total, obj) => total + obj.get('count'), 0) }} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||
|
@ -339,6 +356,8 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
{reblogLink}
|
||||
{reblogLink && <>·</>}
|
||||
{favouriteLink}
|
||||
·
|
||||
{reactionLink}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -45,6 +45,7 @@ import {
|
|||
HomeTimeline,
|
||||
Followers,
|
||||
Following,
|
||||
Reactions,
|
||||
Reblogs,
|
||||
Favourites,
|
||||
DirectTimeline,
|
||||
|
@ -234,6 +235,7 @@ class SwitchingColumnsArea extends PureComponent {
|
|||
<WrappedRoute path={['/accounts/:id/following', '/users/:acct/following', '/@:acct/following']} component={Following} content={children} />
|
||||
<WrappedRoute path={['/@:acct/media', '/accounts/:id/media']} component={AccountGallery} content={children} />
|
||||
<WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} />
|
||||
<WrappedRoute path='/@:acct/:statusId/reactions' component={Reactions} content={children} />
|
||||
<WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} />
|
||||
<WrappedRoute path='/@:acct/:statusId/favourites' component={Favourites} content={children} />
|
||||
|
||||
|
|
|
@ -90,6 +90,10 @@ export function Favourites () {
|
|||
return import(/* webpackChunkName: "flavours/glitch/async/favourites" */'../../favourites');
|
||||
}
|
||||
|
||||
export function Reactions () {
|
||||
return import(/* webpackChunkName: "flavours/glitch/async/reactions" */'../../reactions');
|
||||
}
|
||||
|
||||
export function FollowRequests () {
|
||||
return import(/* webpackChunkName: "flavours/glitch/async/follow_requests" */'../../follow_requests');
|
||||
}
|
||||
|
|
|
@ -57,6 +57,12 @@ import {
|
|||
FAVOURITES_EXPAND_REQUEST,
|
||||
FAVOURITES_EXPAND_SUCCESS,
|
||||
FAVOURITES_EXPAND_FAIL,
|
||||
REACTIONS_FETCH_SUCCESS,
|
||||
REACTIONS_EXPAND_SUCCESS,
|
||||
REACTIONS_FETCH_REQUEST,
|
||||
REACTIONS_EXPAND_REQUEST,
|
||||
REACTIONS_FETCH_FAIL,
|
||||
REACTIONS_EXPAND_FAIL,
|
||||
} from '../actions/interactions';
|
||||
import {
|
||||
MUTES_FETCH_REQUEST,
|
||||
|
@ -77,6 +83,7 @@ const initialListState = ImmutableMap({
|
|||
const initialState = ImmutableMap({
|
||||
followers: initialListState,
|
||||
following: initialListState,
|
||||
reactions: initialListState,
|
||||
reblogged_by: initialListState,
|
||||
favourited_by: initialListState,
|
||||
follow_requests: initialListState,
|
||||
|
@ -139,6 +146,16 @@ export default function userLists(state = initialState, action) {
|
|||
case FOLLOWING_FETCH_FAIL:
|
||||
case FOLLOWING_EXPAND_FAIL:
|
||||
return state.setIn(['following', action.id, 'isLoading'], false);
|
||||
case REACTIONS_FETCH_SUCCESS:
|
||||
return normalizeList(state, ['reactions', action.id], action.accounts, action.next);
|
||||
case REACTIONS_EXPAND_SUCCESS:
|
||||
return appendToList(state, ['reactions', action.id], action.accounts, action.next);
|
||||
case REACTIONS_FETCH_REQUEST:
|
||||
case REACTIONS_EXPAND_REQUEST:
|
||||
return state.setIn(['reactions', action.id, 'isLoading'], true);
|
||||
case REACTIONS_FETCH_FAIL:
|
||||
case REACTIONS_EXPAND_FAIL:
|
||||
return state.setIn(['reactions', action.id, 'isLoading'], false);
|
||||
case REBLOGS_FETCH_SUCCESS:
|
||||
return normalizeList(state, ['reblogged_by', action.id], action.accounts, action.next);
|
||||
case REBLOGS_EXPAND_SUCCESS:
|
||||
|
|
Loading…
Reference in a new issue