-
+
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..186bec37d 100644
--- a/app/soapbox/features/status/components/status_interaction_bar.js
+++ b/app/soapbox/features/status/components/status_interaction_bar.js
@@ -1,18 +1,26 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { FormattedNumber } from 'react-intl';
import emojify from 'soapbox/features/emoji/emoji';
import { reduceEmoji } from 'soapbox/utils/emoji_reacts';
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
+import { getFeatures } from 'soapbox/utils/features';
import { Link } from 'react-router-dom';
import Icon from 'soapbox/components/icon';
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
-const mapStateToProps = state => ({
- allowedEmoji: getSoapboxConfig(state).get('allowedEmoji'),
-});
+const mapStateToProps = state => {
+ const instance = state.get('instance');
+ const features = getFeatures(instance);
+
+ return {
+ allowedEmoji: getSoapboxConfig(state).get('allowedEmoji'),
+ reactionList: features.exposableReactions,
+ };
+};
export default @connect(mapStateToProps)
class StatusInteractionBar extends ImmutablePureComponent {
@@ -21,6 +29,7 @@ class StatusInteractionBar extends ImmutablePureComponent {
status: ImmutablePropTypes.map,
me: SoapboxPropTypes.me,
allowedEmoji: ImmutablePropTypes.list,
+ reactionList: PropTypes.bool,
}
getNormalizedReacts = () => {
@@ -49,35 +58,53 @@ class StatusInteractionBar extends ImmutablePureComponent {
return '';
}
- render() {
+ getEmojiReacts = () => {
+ const { status, reactionList } = 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) => {
+ const emojiReact = (
+ <>
+
+ {e.get('count')}
+ >
+ );
+
+ if (reactionList) {
+ return {emojiReact};
+ }
+
+ return {emojiReact};
+ })}
+
+
+ {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 fc00c7702..73644d6f4 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,
@@ -262,6 +263,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 0a4f44405..fb98c4f5b 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/reactions" */'../../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/soapbox/utils/features.js b/app/soapbox/utils/features.js
index 518f3e669..86a02479a 100644
--- a/app/soapbox/utils/features.js
+++ b/app/soapbox/utils/features.js
@@ -25,6 +25,7 @@ export const getFeatures = createSelector([
settingsStore: v.software === 'Pleroma',
accountAliasesAPI: v.software === 'Pleroma',
resetPasswordAPI: v.software === 'Pleroma',
+ exposableReactions: features.includes('exposable_reactions'),
};
});
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 d710bc180..8a111f31e 100644
--- a/app/styles/ui.scss
+++ b/app/styles/ui.scss
@@ -613,7 +613,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;
@@ -663,6 +664,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;
}