From c80f87efaa889b0c83427bd68e73b33c639624d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 6 Sep 2021 21:54:48 +0200 Subject: [PATCH] Add emoji reacts page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/interactions.js | 39 +++++++ app/soapbox/components/account.js | 19 ++- app/soapbox/components/column_back_button.js | 10 +- app/soapbox/features/reactions/index.js | 110 ++++++++++++++++++ .../components/status_interaction_bar.js | 50 ++++---- app/soapbox/features/ui/components/column.js | 5 +- app/soapbox/features/ui/index.js | 2 + .../features/ui/util/async-components.js | 4 + .../reducers/__tests__/user_lists-test.js | 1 + app/soapbox/reducers/user_lists.js | 4 + app/styles/accounts.scss | 7 ++ app/styles/components/emoji-reacts.scss | 4 +- app/styles/ui.scss | 14 ++- 13 files changed, 241 insertions(+), 28 deletions(-) create mode 100644 app/soapbox/features/reactions/index.js diff --git a/app/soapbox/actions/interactions.js b/app/soapbox/actions/interactions.js index aaa6b6143..1e2d729f1 100644 --- a/app/soapbox/actions/interactions.js +++ b/app/soapbox/actions/interactions.js @@ -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; diff --git a/app/soapbox/components/account.js b/app/soapbox/components/account.js index c23c0420e..d4b616d5d 100644 --- a/app/soapbox/components/account.js +++ b/app/soapbox/components/account.js @@ -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
; @@ -95,6 +97,7 @@ class Account extends ImmutablePureComponent { let buttons; let followedBy; + let emoji; if (onActionClick && actionIcon) { buttons = ; @@ -128,6 +131,15 @@ class Account extends ImmutablePureComponent { } } + if (reaction) { + emoji = ( + + ); + } + const createdAt = account.get('created_at'); const joinedAt = createdAt ? ( @@ -141,7 +153,10 @@ class Account extends ImmutablePureComponent {
-
+
+ {emoji} + +
diff --git a/app/soapbox/components/column_back_button.js b/app/soapbox/components/column_back_button.js index 1ce0d8f1f..bbc2fc701 100644 --- a/app/soapbox/components/column_back_button.js +++ b/app/soapbox/components/column_back_button.js @@ -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(); diff --git a/app/soapbox/features/reactions/index.js b/app/soapbox/features/reactions/index.js new file mode 100644 index 000000000..260c8726b --- /dev/null +++ b/app/soapbox/features/reactions/index.js @@ -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 ( + + + + ); + } + + if (!status) { + return ( + + + + ); + } + + const emptyMessage = ; + + return ( + + { + reactions.size > 0 && ( +
+ All + {reactions?.map(reaction => {reaction.name} {reaction.count})} +
+ ) + } + + {accounts.map((account) => + , + )} + +
+ ); + } + +} diff --git a/app/soapbox/features/status/components/status_interaction_bar.js b/app/soapbox/features/status/components/status_interaction_bar.js index d921efca0..fbd943a63 100644 --- a/app/soapbox/features/status/components/status_interaction_bar.js +++ b/app/soapbox/features/status/components/status_interaction_bar.js @@ -49,35 +49,45 @@ 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 = () => ( -
-
- {emojiReacts.map((e, i) => ( - - - {e.get('count')} - - ))} + if (count > 0) { + return ( +
+
+ {emojiReacts.map((e, i) => ( + + + {e.get('count')} + + ))} +
+
+ {count} +
-
- {count} -
-
- ); + ); + } + + return ''; + }; + + render() { + const emojiReacts = this.getEmojiReacts(); + const repost = this.getRepost(); return (
- {count > 0 && } + {emojiReacts} {repost}
); diff --git a/app/soapbox/features/ui/components/column.js b/app/soapbox/features/ui/components/column.js index 5f31399e3..e915e5e70 100644 --- a/app/soapbox/features/ui/components/column.js +++ b/app/soapbox/features/ui/components/column.js @@ -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 ? () : (); + const backBtn = backBtnSlim ? () : (); return (
diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 010242b0d..c6136f69b 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -56,6 +56,7 @@ import { Followers, Following, Reblogs, + Reactions, // Favourites, DirectTimeline, HashtagTimeline, @@ -261,6 +262,7 @@ class SwitchingColumnsArea extends React.PureComponent { + diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js index 1fe50212c..61f6c2143 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -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'); } diff --git a/app/soapbox/reducers/__tests__/user_lists-test.js b/app/soapbox/reducers/__tests__/user_lists-test.js index feaaca3e6..7d571e208 100644 --- a/app/soapbox/reducers/__tests__/user_lists-test.js +++ b/app/soapbox/reducers/__tests__/user_lists-test.js @@ -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(), diff --git a/app/soapbox/reducers/user_lists.js b/app/soapbox/reducers/user_lists.js index 9afaf55b9..f0d80de77 100644 --- a/app/soapbox/reducers/user_lists.js +++ b/app/soapbox/reducers/user_lists.js @@ -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: diff --git a/app/styles/accounts.scss b/app/styles/accounts.scss index ad2190be5..14f74f59a 100644 --- a/app/styles/accounts.scss +++ b/app/styles/accounts.scss @@ -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 { diff --git a/app/styles/components/emoji-reacts.scss b/app/styles/components/emoji-reacts.scss index 4fca2108c..bc69b0542 100644 --- a/app/styles/components/emoji-reacts.scss +++ b/app/styles/components/emoji-reacts.scss @@ -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; diff --git a/app/styles/ui.scss b/app/styles/ui.scss index bf8dd9f7e..72c55a6bd 100644 --- a/app/styles/ui.scss +++ b/app/styles/ui.scss @@ -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; }