Add emoji reaction detail status

This commit is contained in:
KMY
2023-12-31 11:10:07 +00:00
committed by yoheizuho
parent 0ea43929b7
commit d7ce457a3f
23 changed files with 456 additions and 23 deletions
@@ -41,7 +41,11 @@ export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
export const FAVOURITES_EXPAND_REQUEST = 'FAVOURITES_EXPAND_REQUEST';
export const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS';
export const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL';
export const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL';
export const EMOJI_REACTIONS_FETCH_REQUEST = 'EMOJI_REACTIONS_FETCH_REQUEST';
export const EMOJI_REACTIONS_FETCH_SUCCESS = 'EMOJI_REACTIONS_FETCH_SUCCESS';
export const EMOJI_REACTIONS_FETCH_FAIL = 'EMOJI_REACTIONS_FETCH_FAIL';
export const PIN_REQUEST = 'PIN_REQUEST';
export const PIN_SUCCESS = 'PIN_SUCCESS';
@@ -531,6 +535,85 @@ export function expandFavouritesFail(id, error) {
};
}
export function fetchEmojiReactions(id) {
return (dispatch, getState) => {
dispatch(fetchEmojiReactionsRequest(id));
api(getState).get(`/api/v1/statuses/${id}/emoji_reactioned_by`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map((er) => er.account)));
dispatch(fetchEmojiReactionsSuccess(id, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchEmojiReactionsFail(id, error));
});
};
}
export function fetchEmojiReactionsRequest(id) {
return {
type: EMOJI_REACTIONS_FETCH_REQUEST,
id,
};
}
export function fetchEmojiReactionsSuccess(id, accounts, next) {
return {
type: EMOJI_REACTIONS_FETCH_SUCCESS,
id,
accounts,
next,
};
}
export function fetchEmojiReactionsFail(id, error) {
return {
type: EMOJI_REACTIONS_FETCH_FAIL,
error,
};
}
export function expandEmojiReactions(id) {
return (dispatch, getState) => {
const url = getState().getIn(['user_lists', 'emoji_reactioned_by', id, 'next']);
if (url === null) {
return;
}
dispatch(expandEmojiReactionsRequest(id));
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map((er) => er.account)));
dispatch(expandEmojiReactionsSuccess(id, response.data, next ? next.uri : null));
}).catch(error => dispatch(expandEmojiReactionsFail(id, error)));
};
}
export function expandEmojiReactionsRequest(id) {
return {
type: EMOJI_REACTIONS_EXPAND_REQUEST,
id,
};
}
export function expandEmojiReactionsSuccess(id, accounts, next) {
return {
type: EMOJI_REACTIONS_EXPAND_SUCCESS,
id,
accounts,
next,
};
}
export function expandEmojiReactionsFail(id, error) {
return {
type: EMOJI_REACTIONS_EXPAND_FAIL,
id,
error,
};
}
export function pin(status) {
return (dispatch, getState) => {
dispatch(pinRequest(status));
+10 -6
View File
@@ -46,6 +46,8 @@ class Account extends ImmutablePureComponent {
minimal: PropTypes.bool,
defaultAction: PropTypes.string,
withBio: PropTypes.bool,
onActionClick: PropTypes.func,
children: PropTypes.object,
};
static defaultProps = {
@@ -73,7 +75,7 @@ class Account extends ImmutablePureComponent {
};
render () {
const { account, intl, hidden, withBio, defaultAction, size, minimal } = this.props;
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, withBio, defaultAction, size, minimal, children } = this.props;
if (!account) {
return <EmptyAccount size={size} minimal={minimal} />;
@@ -156,11 +158,13 @@ class Account extends ImmutablePureComponent {
</div>
</Link>
{!minimal && (
<div className='account__relationship'>
{buttons}
</div>
)}
<div>
{children}
</div>
<div className='account__relationship'>
{buttons}
</div>
</div>
{withBio && (account.get('note').length > 0 ? (
@@ -0,0 +1,33 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { injectIntl } from 'react-intl';
import emojify from '../features/emoji/emoji';
import classNames from 'classnames';
export default class EmojiView extends React.PureComponent {
static propTypes = {
name: PropTypes.string,
url: PropTypes.string,
staticUrl: PropTypes.string,
};
render () {
const { name, url, staticUrl } = this.props;
let emojiHtml = null;
if (url) {
let customEmojis = {};
customEmojis[`:${name}:`] = { url, static_url: staticUrl };
emojiHtml = emojify(`:${name}:`, customEmojis);
} else {
emojiHtml = emojify(name);
}
return (
<span className='emoji' dangerouslySetInnerHTML={{ __html: emojiHtml }} />
);
}
}
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import { injectIntl } from 'react-intl';
import emojify from '../features/emoji/emoji';
import classNames from 'classnames';
import EmojiView from './emoji_view';
class EmojiReactionButton extends React.PureComponent {
@@ -32,15 +33,6 @@ class EmojiReactionButton extends React.PureComponent {
render () {
const { name, url, staticUrl, count, me } = this.props;
let emojiHtml = null;
if (url) {
let customEmojis = {};
customEmojis[`:${name}:`] = { url, static_url: staticUrl };
emojiHtml = emojify(`:${name}:`, customEmojis);
} else {
emojiHtml = emojify(name);
}
const classList = {
'emoji-reactions-bar__button': true,
'toggled': me,
@@ -48,7 +40,9 @@ class EmojiReactionButton extends React.PureComponent {
return (
<button className={classNames(classList)} type='button' onClick={this.onClick}>
<span className='emoji' dangerouslySetInnerHTML={{ __html: emojiHtml }} />
<span className='emoji'>
<EmojiView name={name} url={url} staticUrl={staticUrl} />
</span>
<span className='count'>{count}</span>
</button>
);
@@ -0,0 +1,121 @@
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 { ReactComponent as RefreshIcon } from '@material-symbols/svg-600/outlined/refresh.svg';
import { debounce } from 'lodash';
import { fetchEmojiReactions, expandEmojiReactions } from 'mastodon/actions/interactions';
import ColumnHeader from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list';
import AccountContainer from 'mastodon/containers/account_container';
import Column from 'mastodon/features/ui/components/column';
import EmojiView from '../../components/emoji_view';
import { LoadingIndicator } from '../../components/loading_indicator';
const messages = defineMessages({
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
});
const mapStateToProps = (state, props) => {
return {
accountIds: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'items']),
hasMore: !!state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'next']),
isLoading: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'isLoading'], true),
};
};
class EmojiReactions 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,
};
componentWillMount () {
if (!this.props.accountIds) {
this.props.dispatch(fetchEmojiReactions(this.props.params.statusId));
}
}
handleRefresh = () => {
this.props.dispatch(fetchEmojiReactions(this.props.params.statusId));
};
handleLoadMore = debounce(() => {
this.props.dispatch(expandEmojiReactions(this.props.params.statusId));
}, 300, { leading: true });
render () {
const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
if (!accountIds) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
let groups = {};
for (const emoji_reaction of accountIds) {
const key = emoji_reaction.account_id;
const value = emoji_reaction;
if (!groups[key]) groups[key] = [value];
else groups[key].push(value);
}
const emptyMessage = <FormattedMessage id='empty_column.emoji_reactions' defaultMessage='No one has reacted with emoji this post yet. When someone does, they will show up here.' />;
return (
<Column bindToDocument={!multiColumn}>
<ColumnHeader
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='emoji_reactions'
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
isLoading={isLoading}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{Object.keys(groups).map((key) =>(
<AccountContainer key={key} id={key} withNote={false} hideButtons>
<div style={{ 'maxWidth': '100px' }}>
{groups[key].map((value, index2) => <EmojiView key={index2} name={value.name} url={value.url} staticUrl={value.static_url} />)}
</div>
</AccountContainer>
))}
</ScrollableList>
<Helmet>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}
}
export default connect(mapStateToProps)(injectIntl(EmojiReactions));
@@ -55,6 +55,8 @@ class DetailedStatus extends ImmutablePureComponent {
onToggleMediaVisibility: PropTypes.func,
onQuoteToggleMediaVisibility: PropTypes.func,
...WithRouterPropTypes,
onEmojiReact: PropTypes.func,
onUnEmojiReact: PropTypes.func,
};
state = {
@@ -260,7 +262,7 @@ class DetailedStatus extends ImmutablePureComponent {
let emojiReactionsBar = null;
if (status.get('emoji_reactions')) {
const emojiReactions = status.get('emoji_reactions');
emojiReactionsBar = <StatusEmojiReactionsBar emojiReactions={emojiReactions} status={status} onEmojiReaction={this.props.onEmojiReaction} OnUnEmojiReaction={this.props.OnUnEmojiReaction} />;
emojiReactionsBar = <StatusEmojiReactionsBar emojiReactions={emojiReactions} status={status} onEmojiReact={this.props.onEmojiReact} onUnEmojiReact={this.props.onUnEmojiReact} />;
}
if (status.get('application')) {
@@ -18,6 +18,7 @@ import {
pin,
unpin,
emojiReact,
unEmojiReact,
} from '../../../actions/interactions';
import { openModal } from '../../../actions/modal';
import { initMuteModal } from '../../../actions/mutes';
@@ -105,6 +106,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(emojiReact(status, emoji));
},
onUnEmojiReact (status, emoji) {
dispatch(unEmojiReact(status, emoji));
},
onPin (status) {
if (status.get('pinned')) {
dispatch(unpin(status));
@@ -41,6 +41,8 @@ import {
import {
favourite,
unfavourite,
emojiReact,
unEmojiReact,
bookmark,
unbookmark,
reblog,
@@ -279,6 +281,16 @@ class Status extends ImmutablePureComponent {
}
};
handleEmojiReact = (status, emoji) => {
const { dispatch } = this.props;
dispatch(emojiReact(status, emoji));
};
handleUnEmojiReact = (status, emoji) => {
const { dispatch } = this.props;
dispatch(unEmojiReact(status, emoji));
};
handlePin = (status) => {
if (status.get('pinned')) {
this.props.dispatch(unpin(status));
@@ -746,6 +758,8 @@ class Status extends ImmutablePureComponent {
onToggleMediaVisibility={this.handleToggleMediaVisibility}
onToggleQuoteMediaVisibility={this.handleToggleQuoteMediaVisibility}
pictureInPicture={pictureInPicture}
onEmojiReact={this.handleEmojiReact}
onUnEmojiReact={this.handleUnEmojiReact}
/>
<ActionBar
@@ -753,6 +767,7 @@ class Status extends ImmutablePureComponent {
status={status}
onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick}
onEmojiReact={this.handleEmojiReact}
onReblog={this.handleReblogClick}
onQuote={this.handleQuoteClick}
onBookmark={this.handleBookmarkClick}
@@ -45,6 +45,7 @@ import {
Following,
Reblogs,
Favourites,
EmojiReactions,
DirectTimeline,
HashtagTimeline,
Notifications,
@@ -214,6 +215,7 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/directory' component={Directory} content={children} />
<WrappedRoute path={['/explore', '/search']} component={Explore} content={children} />
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
<WrappedRoute path='/@:acct/:statusId/emoji_reactions' component={EmojiReactions} content={children} />
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
<WrappedRoute path='/@:acct/tagged/:tagged?' exact component={AccountTimeline} content={children} />
@@ -224,6 +226,7 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} />
<WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} />
<WrappedRoute path='/@:acct/:statusId/favourites' component={Favourites} content={children} />
<WrappedRoute path='/statuses/:statusId/emoji_reactions' component={EmojiReactions} content={children} />
{/* Legacy routes, cannot be easily factored with other routes because they share a param name */}
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
@@ -82,6 +82,10 @@ export function Favourites () {
return import(/* webpackChunkName: "features/favourites" */'../../favourites');
}
export function EmojiReactions () {
return import(/* webpackChunkName: "features/favourites" */'../../emoji_reactions');
}
export function FollowRequests () {
return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests');
}
@@ -1,5 +1,4 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import {
DIRECTORY_FETCH_REQUEST,
DIRECTORY_FETCH_SUCCESS,
@@ -57,6 +56,7 @@ import {
FAVOURITES_EXPAND_REQUEST,
FAVOURITES_EXPAND_SUCCESS,
FAVOURITES_EXPAND_FAIL,
EMOJI_REACTIONS_FETCH_SUCCESS,
} from '../actions/interactions';
import {
MUTES_FETCH_REQUEST,
@@ -79,6 +79,7 @@ const initialState = ImmutableMap({
following: initialListState,
reblogged_by: initialListState,
favourited_by: initialListState,
emoji_reactioned_by: initialListState,
follow_requests: initialListState,
blocks: initialListState,
mutes: initialListState,
@@ -161,6 +162,10 @@ export default function userLists(state = initialState, action) {
return state.setIn(['favourited_by', action.id, 'isLoading'], false);
case notificationsUpdate.type:
return action.payload.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.payload.notification) : state;
case EMOJI_REACTIONS_FETCH_SUCCESS:
console.log('===================')
console.dir(state);
return state.setIn(['emoji_reactioned_by', action.id], ImmutableList(action.accounts));
case FOLLOW_REQUESTS_FETCH_SUCCESS:
return normalizeList(state, ['follow_requests'], action.accounts, action.next);
case FOLLOW_REQUESTS_EXPAND_SUCCESS: