Add list of users who reacted

This commit is contained in:
Essem 2024-01-26 22:27:23 -06:00
parent 9862e1591e
commit a4bfa67151
No known key found for this signature in database
GPG key ID: 7D497397CC3A2A8C
8 changed files with 256 additions and 4 deletions

View file

@ -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));

View file

@ -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();

View file

@ -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,

View 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));

View file

@ -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>

View file

@ -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} />

View file

@ -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');
}

View file

@ -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: