Add emoji reacts page
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
e652de227c
commit
c80f87efaa
13 changed files with 241 additions and 28 deletions
|
@ -28,6 +28,10 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
|
|||
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
|
||||
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_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 PIN_REQUEST = 'PIN_REQUEST';
|
||||
export const PIN_SUCCESS = 'PIN_SUCCESS';
|
||||
export const PIN_FAIL = 'PIN_FAIL';
|
||||
|
@ -359,6 +363,41 @@ export function fetchFavouritesFail(id, error) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchReactions(id) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(fetchReactionsRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/pleroma/statuses/${id}/reactions`).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data.map(({ accounts }) => accounts).flat()));
|
||||
dispatch(fetchReactionsSuccess(id, response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchReactionsFail(id, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchReactionsRequest(id) {
|
||||
return {
|
||||
type: REACTIONS_FETCH_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchReactionsSuccess(id, reactions) {
|
||||
return {
|
||||
type: REACTIONS_FETCH_SUCCESS,
|
||||
id,
|
||||
reactions,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchReactionsFail(id, error) {
|
||||
return {
|
||||
type: REACTIONS_FETCH_FAIL,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function pin(status) {
|
||||
return (dispatch, getState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
|
|
@ -12,6 +12,7 @@ import RelativeTimestamp from './relative_timestamp';
|
|||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import classNames from 'classnames';
|
||||
import emojify from 'soapbox/features/emoji/emoji';
|
||||
|
||||
const messages = defineMessages({
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
|
@ -46,6 +47,7 @@ class Account extends ImmutablePureComponent {
|
|||
onActionClick: PropTypes.func,
|
||||
withDate: PropTypes.bool,
|
||||
withRelationship: PropTypes.bool,
|
||||
reaction: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -78,7 +80,7 @@ class Account extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, me, withDate, withRelationship } = this.props;
|
||||
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, me, withDate, withRelationship, reaction } = this.props;
|
||||
|
||||
if (!account) {
|
||||
return <div />;
|
||||
|
@ -95,6 +97,7 @@ class Account extends ImmutablePureComponent {
|
|||
|
||||
let buttons;
|
||||
let followedBy;
|
||||
let emoji;
|
||||
|
||||
if (onActionClick && actionIcon) {
|
||||
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
|
||||
|
@ -128,6 +131,15 @@ class Account extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
if (reaction) {
|
||||
emoji = (
|
||||
<span
|
||||
className='emoji-react__emoji'
|
||||
dangerouslySetInnerHTML={{ __html: emojify(reaction) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const createdAt = account.get('created_at');
|
||||
|
||||
const joinedAt = createdAt ? (
|
||||
|
@ -141,7 +153,10 @@ class Account extends ImmutablePureComponent {
|
|||
<div className={classNames('account', { 'account--with-relationship': withRelationship, 'account--with-date': withDate })}>
|
||||
<div className='account__wrapper'>
|
||||
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={`/@${account.get('acct')}`} to={`/@${account.get('acct')}`}>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<div className='account__avatar-wrapper'>
|
||||
{emoji}
|
||||
<Avatar account={account} size={36} />
|
||||
</div>
|
||||
<DisplayName account={account} withDate={Boolean(withDate && withRelationship)} />
|
||||
</Permalink>
|
||||
|
||||
|
|
|
@ -5,12 +5,20 @@ import Icon from 'soapbox/components/icon';
|
|||
|
||||
export default class ColumnBackButton extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
to: PropTypes.string,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
if (window.history && window.history.length === 1) {
|
||||
const { to } = this.props;
|
||||
|
||||
if (to) {
|
||||
this.context.router.history.push(to);
|
||||
} else if (window.history && window.history.length === 1) {
|
||||
this.context.router.history.push('/');
|
||||
} else {
|
||||
this.context.router.history.goBack();
|
||||
|
|
110
app/soapbox/features/reactions/index.js
Normal file
110
app/soapbox/features/reactions/index.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import MissingIndicator from '../../components/missing_indicator';
|
||||
import { fetchFavourites, fetchReactions } from '../../actions/interactions';
|
||||
import { fetchStatus } from '../../actions/statuses';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import Column from '../ui/components/column';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import { makeGetStatus } from '../../selectors';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const getStatus = makeGetStatus();
|
||||
const status = getStatus(state, {
|
||||
id: props.params.statusId,
|
||||
username: props.params.username,
|
||||
});
|
||||
|
||||
const favourites = state.getIn(['user_lists', 'favourited_by', props.params.statusId]);
|
||||
const reactions = state.getIn(['user_lists', 'reactions', props.params.statusId]);
|
||||
const allReactions = favourites && reactions && ImmutableOrderedSet(favourites ? [{ accounts: favourites, count: favourites.size, name: '👍' }] : []).union(reactions || []);
|
||||
|
||||
return {
|
||||
status,
|
||||
reactions: allReactions,
|
||||
accounts: allReactions && (props.params.reaction
|
||||
? allReactions.find(reaction => reaction.name === props.params.reaction).accounts.map(account => ({ id: account, reaction: props.params.reaction }))
|
||||
: allReactions.map(reaction => reaction.accounts.map(account => ({ id: account, reaction: reaction.name }))).flatten()),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Reactions extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.array.isRequired,
|
||||
reactions: PropTypes.array,
|
||||
accounts: PropTypes.array,
|
||||
status: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchFavourites(this.props.params.statusId));
|
||||
this.props.dispatch(fetchReactions(this.props.params.statusId));
|
||||
this.props.dispatch(fetchStatus(this.props.params.statusId));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { params } = this.props;
|
||||
if (params.statusId !== prevProps.params.statusId && params.statusId) {
|
||||
this.props.dispatch(fetchFavourites(this.props.params.statusId));
|
||||
prevProps.dispatch(fetchReactions(params.statusId));
|
||||
prevProps.dispatch(fetchStatus(params.statusId));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { params, reactions, accounts, status } = this.props;
|
||||
const { username, statusId } = params;
|
||||
|
||||
const back = `/@${username}/posts/${statusId}`;
|
||||
|
||||
if (!accounts) {
|
||||
return (
|
||||
<Column back={back}>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
return (
|
||||
<Column back={back}>
|
||||
<MissingIndicator />
|
||||
</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 back={back}>
|
||||
{
|
||||
reactions.size > 0 && (
|
||||
<div className='reaction__filter-bar'>
|
||||
<NavLink to={`/@${username}/posts/${statusId}/reactions`} exact>All</NavLink>
|
||||
{reactions?.map(reaction => <NavLink to={`/@${username}/posts/${statusId}/reactions/${reaction.name}`}>{reaction.name} {reaction.count}</NavLink>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<ScrollableList
|
||||
scrollKey='reactions'
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{accounts.map((account) =>
|
||||
<AccountContainer key={`${account.id}-${account.reaction}`} id={account.id} withNote={false} reaction={account.reaction} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -49,24 +49,26 @@ class StatusInteractionBar extends ImmutablePureComponent {
|
|||
return '';
|
||||
}
|
||||
|
||||
render() {
|
||||
getEmojiReacts = () => {
|
||||
const { status } = this.props;
|
||||
|
||||
const emojiReacts = this.getNormalizedReacts();
|
||||
const count = emojiReacts.reduce((acc, cur) => (
|
||||
acc + cur.get('count')
|
||||
), 0);
|
||||
const repost = this.getRepost();
|
||||
|
||||
const EmojiReactsContainer = () => (
|
||||
if (count > 0) {
|
||||
return (
|
||||
<div className='emoji-reacts-container'>
|
||||
<div className='emoji-reacts'>
|
||||
{emojiReacts.map((e, i) => (
|
||||
<span className='emoji-react' key={i}>
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}/reactions/${e.get('name')}`} className='emoji-react' key={i}>
|
||||
<span
|
||||
className='emoji-react__emoji'
|
||||
dangerouslySetInnerHTML={{ __html: emojify(e.get('name')) }}
|
||||
/>
|
||||
<span className='emoji-react__count'>{e.get('count')}</span>
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className='emoji-reacts__count'>
|
||||
|
@ -74,10 +76,18 @@ class StatusInteractionBar extends ImmutablePureComponent {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
render() {
|
||||
const emojiReacts = this.getEmojiReacts();
|
||||
const repost = this.getRepost();
|
||||
|
||||
return (
|
||||
<div className='status-interaction-bar'>
|
||||
{count > 0 && <EmojiReactsContainer />}
|
||||
{emojiReacts}
|
||||
{repost}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -12,12 +12,13 @@ export default class Column extends React.PureComponent {
|
|||
children: PropTypes.node,
|
||||
active: PropTypes.bool,
|
||||
backBtnSlim: PropTypes.bool,
|
||||
back: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { heading, icon, children, active, backBtnSlim } = this.props;
|
||||
const { heading, icon, children, active, backBtnSlim, back } = this.props;
|
||||
const columnHeaderId = heading && heading.replace(/ /g, '-');
|
||||
const backBtn = backBtnSlim ? (<ColumnBackButtonSlim />) : (<ColumnBackButton />);
|
||||
const backBtn = backBtnSlim ? (<ColumnBackButtonSlim to={back} />) : (<ColumnBackButton to={back} />);
|
||||
|
||||
return (
|
||||
<div role='region' aria-labelledby={columnHeaderId} className='column'>
|
||||
|
|
|
@ -56,6 +56,7 @@ import {
|
|||
Followers,
|
||||
Following,
|
||||
Reblogs,
|
||||
Reactions,
|
||||
// Favourites,
|
||||
DirectTimeline,
|
||||
HashtagTimeline,
|
||||
|
@ -261,6 +262,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<WrappedRoute path='/@:username/pins' component={PinnedStatuses} page={ProfilePage} content={children} />
|
||||
<WrappedRoute path='/@:username/posts/:statusId' publicRoute exact page={DefaultPage} component={Status} content={children} />
|
||||
<WrappedRoute path='/@:username/posts/:statusId/reblogs' page={DefaultPage} component={Reblogs} content={children} />
|
||||
<WrappedRoute path='/@:username/posts/:statusId/reactions/:reaction?' page={DefaultPage} component={Reactions} content={children} />
|
||||
|
||||
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
||||
<WrappedRoute path='/scheduled_statuses' page={DefaultPage} component={ScheduledStatuses} content={children} />
|
||||
|
|
|
@ -94,6 +94,10 @@ export function Reblogs() {
|
|||
return import(/* webpackChunkName: "features/reblogs" */'../../reblogs');
|
||||
}
|
||||
|
||||
export function Reactions() {
|
||||
return import(/* webpackChunkName: "features/reblogs" */'../../reactions');
|
||||
}
|
||||
|
||||
export function Favourites() {
|
||||
return import(/* webpackChunkName: "features/favourites" */'../../favourites');
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ describe('user_lists reducer', () => {
|
|||
favourited_by: ImmutableMap(),
|
||||
follow_requests: ImmutableMap(),
|
||||
blocks: ImmutableMap(),
|
||||
reactions: ImmutableMap(),
|
||||
mutes: ImmutableMap(),
|
||||
groups: ImmutableMap(),
|
||||
groups_removed_accounts: ImmutableMap(),
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
import {
|
||||
REBLOGS_FETCH_SUCCESS,
|
||||
FAVOURITES_FETCH_SUCCESS,
|
||||
REACTIONS_FETCH_SUCCESS,
|
||||
} from '../actions/interactions';
|
||||
import {
|
||||
BLOCKS_FETCH_SUCCESS,
|
||||
|
@ -37,6 +38,7 @@ const initialState = ImmutableMap({
|
|||
following: ImmutableMap(),
|
||||
reblogged_by: ImmutableMap(),
|
||||
favourited_by: ImmutableMap(),
|
||||
reactions: ImmutableMap(),
|
||||
follow_requests: ImmutableMap(),
|
||||
blocks: ImmutableMap(),
|
||||
mutes: ImmutableMap(),
|
||||
|
@ -77,6 +79,8 @@ export default function userLists(state = initialState, action) {
|
|||
return state.setIn(['reblogged_by', action.id], ImmutableOrderedSet(action.accounts.map(item => item.id)));
|
||||
case FAVOURITES_FETCH_SUCCESS:
|
||||
return state.setIn(['favourited_by', action.id], ImmutableOrderedSet(action.accounts.map(item => item.id)));
|
||||
case REACTIONS_FETCH_SUCCESS:
|
||||
return state.setIn(['reactions', action.id], action.reactions.map(({ accounts, ...reaction }) => ({ ...reaction, accounts: ImmutableOrderedSet(accounts.map(account => account.id)) })));
|
||||
case NOTIFICATIONS_UPDATE:
|
||||
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
|
||||
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
||||
|
|
|
@ -216,6 +216,13 @@
|
|||
.account__avatar-wrapper {
|
||||
float: left;
|
||||
margin-right: 12px;
|
||||
|
||||
.emoji-react__emoji {
|
||||
position: absolute;
|
||||
top: 36px;
|
||||
left: 32px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.account__avatar {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
.emoji-react {
|
||||
display: inline-block;
|
||||
transition: 0.1s;
|
||||
color: var(--primary-text-color--faint);
|
||||
text-decoration: none;
|
||||
|
||||
&__emoji {
|
||||
img {
|
||||
|
@ -20,8 +22,6 @@
|
|||
}
|
||||
|
||||
.emoji-react--reblogs {
|
||||
color: var(--primary-text-color--faint);
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
display: inline-flex;
|
||||
|
||||
|
|
|
@ -611,7 +611,8 @@
|
|||
|
||||
.notification__filter-bar,
|
||||
.search__filter-bar,
|
||||
.account__section-headline {
|
||||
.account__section-headline,
|
||||
.reaction__filter-bar {
|
||||
border-bottom: 1px solid var(--brand-color--faint);
|
||||
cursor: default;
|
||||
display: flex;
|
||||
|
@ -661,6 +662,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.reaction__filter-bar {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
a {
|
||||
flex: unset;
|
||||
padding: 15px 24px;
|
||||
min-width: max-content;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue