Skip to content

Commit

Permalink
Add list of users who reacted
Browse files Browse the repository at this point in the history
  • Loading branch information
TheEssem committed Feb 8, 2024
1 parent 96a84c6 commit 73172ec
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 6 deletions.
92 changes: 92 additions & 0 deletions app/javascript/flavours/glitch/actions/interactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/flavours/glitch/components/status.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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}
Expand All @@ -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,
Expand Down
117 changes: 117 additions & 0 deletions app/javascript/flavours/glitch/features/reactions/index.jsx
Original file line number Diff line number Diff line change
@@ -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));
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';


import MoodIcon from '@/material-icons/400-24px/mood.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import { AnimatedNumber } from 'flavours/glitch/components/animated_number';
Expand All @@ -25,7 +26,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';
Expand Down Expand Up @@ -143,6 +144,7 @@ class DetailedStatus extends ImmutablePureComponent {
const reblogIcon = 'retweet';
const reblogIconComponent = RepeatIcon;
let favouriteLink = '';
let reactionLink = '';
let edited = '';

// Depending on user settings, some media are considered as parts of the
Expand Down Expand Up @@ -288,6 +290,14 @@ class DetailedStatus extends ImmutablePureComponent {
</span>
</Link>
);
reactionLink = (
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reactions`} className='detailed-status__link'>
<Icon id='mood' icon={MoodIcon} />
<span className='detailed-status__reactions'>
<AnimatedNumber value={status.get('reactions').reduce((total, obj) => total + obj.get('count'), 0)} />
</span>
</Link>
);
} else {
favouriteLink = (
<a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
Expand All @@ -297,6 +307,14 @@ class DetailedStatus extends ImmutablePureComponent {
</span>
</a>
);
reactionLink = (
<a href={`/interact/${status.get('id')}?type=reaction`} className='detailed-status__link' onClick={this.handleModalLink}>
<Icon id='mood' icon={MoodIcon} />
<span className='detailed-status__reactions'>
<AnimatedNumber value={status.get('reactions').reduce((total, obj) => total + obj.get('count'), 0)} />
</span>
</a>
);
}

if (status.get('edited_at')) {
Expand Down Expand Up @@ -347,7 +365,7 @@ class DetailedStatus extends ImmutablePureComponent {
<div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
</a>{edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
</a>{edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink} · {reactionLink}
</div>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/flavours/glitch/features/ui/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
HomeTimeline,
Followers,
Following,
Reactions,
Reblogs,
Favourites,
DirectTimeline,
Expand Down Expand Up @@ -231,6 +232,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} />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ export function Following () {
return import(/* webpackChunkName: "flavours/glitch/async/following" */'flavours/glitch/features/following');
}

export function Reactions () {
return import(/* webpackChunkName: "flavours/glitch/async/reactions" */'flavours/glitch/features/reactions');
}

export function Reblogs () {
return import(/* webpackChunkName: "flavours/glitch/async/reblogs" */'flavours/glitch/features/reblogs');
}
Expand Down
17 changes: 17 additions & 0 deletions app/javascript/flavours/glitch/reducers/user_lists.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
Loading

0 comments on commit 73172ec

Please sign in to comment.