diff --git a/app/gabsocial/features/status/components/detailed_status.js b/app/gabsocial/features/status/components/detailed_status.js
index 9372c9e47..b18ad11e0 100644
--- a/app/gabsocial/features/status/components/detailed_status.js
+++ b/app/gabsocial/features/status/components/detailed_status.js
@@ -14,6 +14,7 @@ import scheduleIdleTask from '../../ui/util/schedule_idle_task';
import classNames from 'classnames';
import Icon from 'gabsocial/components/icon';
import PollContainer from 'gabsocial/containers/poll_container';
+import { StatusInteractionBar } from './status_interaction_bar';
export default class DetailedStatus extends ImmutablePureComponent {
@@ -92,7 +93,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
let applicationLink = '';
let reblogLink = '';
let reblogIcon = 'retweet';
- let favouriteLink = '';
if (this.props.measureHeight) {
outerStyle.height = `${this.state.height}px`;
@@ -169,15 +169,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
);
}
- favouriteLink = (
-
-
-
-
-
-
- );
-
return (
@@ -197,9 +188,12 @@ export default class DetailedStatus extends ImmutablePureComponent {
{media}
-
-
- {applicationLink} ยท {reblogLink} ยท {favouriteLink}
+
+
+ {reblogLink} {applicationLink} ยท
+
+
+
diff --git a/app/gabsocial/features/status/components/status_interaction_bar.js b/app/gabsocial/features/status/components/status_interaction_bar.js
new file mode 100644
index 000000000..4746dd692
--- /dev/null
+++ b/app/gabsocial/features/status/components/status_interaction_bar.js
@@ -0,0 +1,55 @@
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import emojify from 'gabsocial/features/emoji/emoji';
+import { reduceEmoji } from 'gabsocial/utils/emoji_reacts';
+import SoapboxPropTypes from 'gabsocial/utils/soapbox_prop_types';
+
+export class StatusInteractionBar extends React.Component {
+
+ static propTypes = {
+ status: ImmutablePropTypes.map,
+ me: SoapboxPropTypes.me,
+ }
+
+ getNormalizedReacts = () => {
+ const { status } = this.props;
+ return reduceEmoji(
+ status.getIn(['pleroma', 'emoji_reactions']),
+ status.get('favourites_count'),
+ status.get('favourited'),
+ ).reverse();
+ }
+
+ render() {
+ const emojiReacts = this.getNormalizedReacts();
+ const count = emojiReacts.reduce((acc, cur) => (
+ acc + cur.get('count')
+ ), 0);
+
+ const EmojiReactsContainer = () => (
+
+
+ {emojiReacts.map((e, i) => (
+
+
+ {e.get('count')}
+
+ ))}
+
+
+ {count}
+
+
+ );
+
+ return (
+
+ {count > 0 && }
+
+ );
+ }
+
+}
diff --git a/app/gabsocial/features/status/index.js b/app/gabsocial/features/status/index.js
index 4b9fc2436..12527613b 100644
--- a/app/gabsocial/features/status/index.js
+++ b/app/gabsocial/features/status/index.js
@@ -17,6 +17,7 @@ import {
pin,
unpin,
} from '../../actions/interactions';
+import { simpleEmojiReact } from '../../actions/emoji_reacts';
import {
replyCompose,
mentionCompose,
@@ -161,6 +162,10 @@ class Status extends ImmutablePureComponent {
this.setState({ showMedia: !this.state.showMedia });
}
+ handleEmojiReactClick = (status, emoji) => {
+ this.props.dispatch(simpleEmojiReact(status, emoji));
+ }
+
handleFavouriteClick = (status) => {
if (status.get('favourited')) {
this.props.dispatch(unfavourite(status));
@@ -496,6 +501,7 @@ class Status extends ImmutablePureComponent {
status={status}
onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick}
+ onEmojiReact={this.handleEmojiReactClick}
onReblog={this.handleReblogClick}
onDelete={this.handleDeleteClick}
onDirect={this.handleDirectClick}
diff --git a/app/gabsocial/reducers/notifications.js b/app/gabsocial/reducers/notifications.js
index 4b2267e5a..81db680c6 100644
--- a/app/gabsocial/reducers/notifications.js
+++ b/app/gabsocial/reducers/notifications.js
@@ -41,6 +41,7 @@ const notificationToMap = notification => ImmutableMap({
account: notification.account.id,
created_at: notification.created_at,
status: notification.status ? notification.status.id : null,
+ emoji: notification.emoji,
});
const normalizeNotification = (state, notification) => {
diff --git a/app/gabsocial/reducers/statuses.js b/app/gabsocial/reducers/statuses.js
index 885cc221c..d41fc3834 100644
--- a/app/gabsocial/reducers/statuses.js
+++ b/app/gabsocial/reducers/statuses.js
@@ -10,9 +10,13 @@ import {
STATUS_REVEAL,
STATUS_HIDE,
} from '../actions/statuses';
+import {
+ EMOJI_REACT_REQUEST,
+} from '../actions/emoji_reacts';
import { TIMELINE_DELETE } from '../actions/timelines';
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
import { Map as ImmutableMap, fromJS } from 'immutable';
+import { simulateEmojiReact } from 'gabsocial/utils/emoji_reacts';
const importStatus = (state, status) => state.set(status.id, fromJS(status));
@@ -37,6 +41,10 @@ export default function statuses(state = initialState, action) {
return importStatuses(state, action.statuses);
case FAVOURITE_REQUEST:
return state.setIn([action.status.get('id'), 'favourited'], true);
+ case EMOJI_REACT_REQUEST:
+ const path = [action.status.get('id'), 'pleroma', 'emoji_reactions'];
+ const emojiReacts = state.getIn(path);
+ return state.setIn(path, simulateEmojiReact(emojiReacts, action.emoji));
case FAVOURITE_FAIL:
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false);
case REBLOG_REQUEST:
diff --git a/app/gabsocial/utils/__tests__/emoji_reacts-test.js b/app/gabsocial/utils/__tests__/emoji_reacts-test.js
new file mode 100644
index 000000000..fcce0a11b
--- /dev/null
+++ b/app/gabsocial/utils/__tests__/emoji_reacts-test.js
@@ -0,0 +1,207 @@
+import {
+ sortEmoji,
+ mergeEmojiFavourites,
+ filterEmoji,
+ oneEmojiPerAccount,
+ reduceEmoji,
+ getReactForStatus,
+ simulateEmojiReact,
+} from '../emoji_reacts';
+import { fromJS } from 'immutable';
+
+const ALLOWED_EMOJI = [
+ '๐',
+ 'โค',
+ '๐',
+ '๐ฏ',
+ '๐ข',
+ '๐ก',
+];
+
+describe('filterEmoji', () => {
+ describe('with a mix of allowed and disallowed emoji', () => {
+ const emojiReacts = fromJS([
+ { 'count': 1, 'me': true, 'name': '๐ต' },
+ { 'count': 1, 'me': true, 'name': '๐' },
+ { 'count': 1, 'me': true, 'name': '๐' },
+ { 'count': 1, 'me': true, 'name': '๐ฉ' },
+ { 'count': 1, 'me': true, 'name': '๐ก' },
+ { 'count': 1, 'me': true, 'name': '๐ช' },
+ { 'count': 1, 'me': true, 'name': '๐ ' },
+ ]);
+ it('filters only allowed emoji', () => {
+ expect(filterEmoji(emojiReacts, ALLOWED_EMOJI)).toEqual(fromJS([
+ { 'count': 1, 'me': true, 'name': '๐' },
+ { 'count': 1, 'me': true, 'name': '๐ก' },
+ ]));
+ });
+ });
+});
+
+describe('sortEmoji', () => {
+ describe('with an unsorted list of emoji', () => {
+ const emojiReacts = fromJS([
+ { 'count': 7, 'me': true, 'name': '๐ฏ' },
+ { 'count': 3, 'me': true, 'name': '๐ข' },
+ { 'count': 1, 'me': true, 'name': '๐ก' },
+ { 'count': 20, 'me': true, 'name': '๐' },
+ { 'count': 7, 'me': true, 'name': '๐' },
+ { 'count': 15, 'me': true, 'name': 'โค' },
+ ]);
+ it('sorts the emoji by count', () => {
+ expect(sortEmoji(emojiReacts)).toEqual(fromJS([
+ { 'count': 20, 'me': true, 'name': '๐' },
+ { 'count': 15, 'me': true, 'name': 'โค' },
+ { 'count': 7, 'me': true, 'name': '๐ฏ' },
+ { 'count': 7, 'me': true, 'name': '๐' },
+ { 'count': 3, 'me': true, 'name': '๐ข' },
+ { 'count': 1, 'me': true, 'name': '๐ก' },
+ ]));
+ });
+ });
+});
+
+describe('mergeEmojiFavourites', () => {
+ const favouritesCount = 12;
+ const favourited = true;
+
+ describe('with existing ๐ reacts', () => {
+ const emojiReacts = fromJS([
+ { 'count': 20, 'me': false, 'name': '๐' },
+ { 'count': 15, 'me': false, 'name': 'โค' },
+ { 'count': 7, 'me': false, 'name': '๐ฏ' },
+ ]);
+ it('combines ๐ reacts with favourites', () => {
+ expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([
+ { 'count': 32, 'me': true, 'name': '๐' },
+ { 'count': 15, 'me': false, 'name': 'โค' },
+ { 'count': 7, 'me': false, 'name': '๐ฏ' },
+ ]));
+ });
+ });
+
+ describe('without existing ๐ reacts', () => {
+ const emojiReacts = fromJS([
+ { 'count': 15, 'me': false, 'name': 'โค' },
+ { 'count': 7, 'me': false, 'name': '๐ฏ' },
+ ]);
+ it('adds ๐ reacts to the map equaling favourite count', () => {
+ expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([
+ { 'count': 15, 'me': false, 'name': 'โค' },
+ { 'count': 7, 'me': false, 'name': '๐ฏ' },
+ { 'count': 12, 'me': true, 'name': '๐' },
+ ]));
+ });
+ it('does not add ๐ reacts when there are no favourites', () => {
+ expect(mergeEmojiFavourites(emojiReacts, 0, false)).toEqual(fromJS([
+ { 'count': 15, 'me': false, 'name': 'โค' },
+ { 'count': 7, 'me': false, 'name': '๐ฏ' },
+ ]));
+ });
+ });
+});
+
+describe('reduceEmoji', () => {
+ describe('with a clusterfuck of emoji', () => {
+ const emojiReacts = fromJS([
+ { 'count': 1, 'me': false, 'name': '๐ก' },
+ { 'count': 1, 'me': true, 'name': '๐ช' },
+ { 'count': 7, 'me': true, 'name': '๐ฏ' },
+ { 'count': 3, 'me': false, 'name': '๐ข' },
+ { 'count': 1, 'me': true, 'name': '๐ต' },
+ { 'count': 20, 'me': true, 'name': '๐' },
+ { 'count': 7, 'me': false, 'name': '๐' },
+ { 'count': 15, 'me': true, 'name': 'โค' },
+ { 'count': 1, 'me': false, 'name': '๐' },
+ { 'count': 1, 'me': false, 'name': '๐ฉ' },
+ ]);
+ it('sorts, filters, and combines emoji and favourites', () => {
+ expect(reduceEmoji(emojiReacts, 7, true, ALLOWED_EMOJI)).toEqual(fromJS([
+ { 'count': 27, 'me': true, 'name': '๐' },
+ { 'count': 15, 'me': true, 'name': 'โค' },
+ { 'count': 7, 'me': true, 'name': '๐ฏ' },
+ { 'count': 7, 'me': false, 'name': '๐' },
+ { 'count': 3, 'me': false, 'name': '๐ข' },
+ { 'count': 1, 'me': false, 'name': '๐ก' },
+ ]));
+ });
+ });
+});
+
+describe('oneEmojiPerAccount', () => {
+ it('reduces to one react per account', () => {
+ const emojiReacts = fromJS([
+ // Sorted
+ { 'count': 2, 'me': true, 'name': '๐', accounts: [{ id: '1' }, { id: '2' }] },
+ { 'count': 2, 'me': true, 'name': 'โค', accounts: [{ id: '1' }, { id: '2' }] },
+ { 'count': 1, 'me': true, 'name': '๐ฏ', accounts: [{ id: '1' }] },
+ { 'count': 1, 'me': false, 'name': '๐', accounts: [{ id: '3' }] },
+ ]);
+ expect(oneEmojiPerAccount(emojiReacts, '1')).toEqual(fromJS([
+ { 'count': 2, 'me': true, 'name': '๐', accounts: [{ id: '1' }, { id: '2' }] },
+ { 'count': 1, 'me': false, 'name': '๐', accounts: [{ id: '3' }] },
+ ]));
+ });
+});
+
+describe('getReactForStatus', () => {
+ it('returns a single owned react (including favourite) for the status', () => {
+ const status = fromJS({
+ favourited: false,
+ pleroma: {
+ emoji_reactions: [
+ { 'count': 20, 'me': false, 'name': '๐' },
+ { 'count': 15, 'me': true, 'name': 'โค' },
+ { 'count': 7, 'me': true, 'name': '๐ฏ' },
+ { 'count': 7, 'me': false, 'name': '๐' },
+ ],
+ },
+ });
+ expect(getReactForStatus(status)).toEqual('โค');
+ });
+
+ it('returns a thumbs-up for a favourite', () => {
+ const status = fromJS({ favourites_count: 1, favourited: true });
+ expect(getReactForStatus(status)).toEqual('๐');
+ });
+
+ it('returns undefined when a status has no reacts (or favourites)', () => {
+ const status = fromJS([]);
+ expect(getReactForStatus(status)).toEqual(undefined);
+ });
+
+ it('returns undefined when a status has no valid reacts (or favourites)', () => {
+ const status = fromJS([
+ { 'count': 1, 'me': true, 'name': '๐ช' },
+ { 'count': 1, 'me': true, 'name': '๐ต' },
+ { 'count': 1, 'me': false, 'name': '๐' },
+ { 'count': 1, 'me': false, 'name': '๐ฉ' },
+ ]);
+ expect(getReactForStatus(status)).toEqual(undefined);
+ });
+});
+
+describe('simulateEmojiReact', () => {
+ it('adds the emoji to the list', () => {
+ const emojiReacts = fromJS([
+ { 'count': 2, 'me': false, 'name': '๐' },
+ { 'count': 2, 'me': false, 'name': 'โค' },
+ ]);
+ expect(simulateEmojiReact(emojiReacts, 'โค')).toEqual(fromJS([
+ { 'count': 2, 'me': false, 'name': '๐' },
+ { 'count': 3, 'me': true, 'name': 'โค' },
+ ]));
+ });
+
+ it('creates the emoji if it didn\'t already exist', () => {
+ const emojiReacts = fromJS([
+ { 'count': 2, 'me': false, 'name': '๐' },
+ { 'count': 2, 'me': false, 'name': 'โค' },
+ ]);
+ expect(simulateEmojiReact(emojiReacts, '๐ฏ')).toEqual(fromJS([
+ { 'count': 2, 'me': false, 'name': '๐' },
+ { 'count': 2, 'me': false, 'name': 'โค' },
+ { 'count': 1, 'me': true, 'name': '๐ฏ' },
+ ]));
+ });
+});
diff --git a/app/gabsocial/utils/emoji_reacts.js b/app/gabsocial/utils/emoji_reacts.js
new file mode 100644
index 000000000..2cb1b3fc3
--- /dev/null
+++ b/app/gabsocial/utils/emoji_reacts.js
@@ -0,0 +1,102 @@
+import {
+ Map as ImmutableMap,
+ List as ImmutableList,
+} from 'immutable';
+
+// https://emojipedia.org/facebook
+// I've customized them.
+export const ALLOWED_EMOJI = [
+ '๐',
+ 'โค',
+ '๐',
+ '๐ฎ',
+ '๐ข',
+ '๐ฉ',
+];
+
+export const sortEmoji = emojiReacts => (
+ emojiReacts.sortBy(emojiReact => -emojiReact.get('count'))
+);
+
+export const mergeEmoji = emojiReacts => (
+ emojiReacts // TODO: Merge similar emoji
+);
+
+export const mergeEmojiFavourites = (emojiReacts, favouritesCount, favourited) => {
+ if (!favouritesCount) return emojiReacts;
+ const likeIndex = emojiReacts.findIndex(emojiReact => emojiReact.get('name') === '๐');
+ if (likeIndex > -1) {
+ const likeCount = emojiReacts.getIn([likeIndex, 'count']);
+ favourited = favourited || emojiReacts.getIn([likeIndex, 'me'], false);
+ return emojiReacts
+ .setIn([likeIndex, 'count'], likeCount + favouritesCount)
+ .setIn([likeIndex, 'me'], favourited);
+ } else {
+ return emojiReacts.push(ImmutableMap({ count: favouritesCount, me: favourited, name: '๐' }));
+ }
+};
+
+const hasMultiReactions = (emojiReacts, account) => (
+ emojiReacts.filter(
+ e => e.get('accounts').filter(
+ a => a.get('id') === account.get('id')
+ ).count() > 0
+ ).count() > 1
+);
+
+const inAccounts = (accounts, id) => (
+ accounts.filter(a => a.get('id') === id).count() > 0
+);
+
+export const oneEmojiPerAccount = (emojiReacts, me) => {
+ emojiReacts = emojiReacts.reverse();
+
+ return emojiReacts.reduce((acc, cur, idx) => {
+ const accounts = cur.get('accounts', ImmutableList())
+ .filter(a => !hasMultiReactions(acc, a));
+
+ return acc.set(idx, cur.merge({
+ accounts: accounts,
+ count: accounts.count(),
+ me: me ? inAccounts(accounts, me) : false,
+ }));
+ }, emojiReacts)
+ .filter(e => e.get('count') > 0)
+ .reverse();
+};
+
+export const filterEmoji = (emojiReacts, allowedEmoji=ALLOWED_EMOJI) => (
+ emojiReacts.filter(emojiReact => (
+ allowedEmoji.includes(emojiReact.get('name'))
+ )));
+
+export const reduceEmoji = (emojiReacts, favouritesCount, favourited, allowedEmoji=ALLOWED_EMOJI) => (
+ filterEmoji(sortEmoji(mergeEmoji(mergeEmojiFavourites(
+ emojiReacts, favouritesCount, favourited
+ ))), allowedEmoji));
+
+export const getReactForStatus = status => {
+ return reduceEmoji(
+ status.getIn(['pleroma', 'emoji_reactions'], ImmutableList()),
+ status.get('favourites_count'),
+ status.get('favourited')
+ ).filter(e => e.get('me') === true)
+ .getIn([0, 'name']);
+};
+
+export const simulateEmojiReact = (emojiReacts, emoji) => {
+ const idx = emojiReacts.findIndex(e => e.get('name') === emoji);
+ if (idx > -1) {
+ const emojiReact = emojiReacts.get(idx);
+ return emojiReacts.set(idx, emojiReact.merge({
+ count: emojiReact.get('count') + 1,
+ me: true,
+ }));
+ } else {
+ return emojiReacts.push(ImmutableMap({
+ count: 1,
+ me: true,
+ name: emoji,
+ }));
+ }
+};
diff --git a/app/styles/application.scss b/app/styles/application.scss
index a57193533..69ab8cccd 100644
--- a/app/styles/application.scss
+++ b/app/styles/application.scss
@@ -32,6 +32,8 @@
@import 'gabsocial/components/group-sidebar-panel';
@import 'gabsocial/components/sidebar-menu';
@import 'gabsocial/components/hotkeys-modal';
+@import 'gabsocial/components/emoji-reacts';
+@import 'gabsocial/components/detailed-status';
@import 'gabsocial/polls';
@import 'gabsocial/introduction';
diff --git a/app/styles/gabsocial/components.scss b/app/styles/gabsocial/components.scss
index 7f152b0c6..ddf707a9a 100644
--- a/app/styles/gabsocial/components.scss
+++ b/app/styles/gabsocial/components.scss
@@ -119,7 +119,7 @@
display: inline-block;
padding: 0;
color: $gab-secondary-text;
- border: none;
+ border: 0;
background: transparent;
cursor: pointer;
transition: color 100ms ease-in;
@@ -718,82 +718,6 @@
width: 23.15px;
}
-.detailed-status__action-bar-dropdown {
- flex: 1 1 auto;
- display: flex;
- align-items: center;
- justify-content: center;
- position: relative;
-}
-
-.detailed-status {
- background: lighten($ui-base-color, 4%);
- padding: 14px 10px;
-
- &--flex {
- display: flex;
- flex-wrap: wrap;
- justify-content: space-between;
- align-items: flex-start;
-
- .status__content,
- .detailed-status__meta {
- flex: 100%;
- }
- }
-
- .status__content {
- font-size: 19px;
- line-height: 24px;
-
- .emojione {
- width: 24px;
- height: 24px;
- margin: -1px 0 0;
- }
-
- .status__content__spoiler-link {
- line-height: 24px;
- margin: -1px 0 0;
- }
- }
-
- .video-player {
- margin-top: 8px;
- }
-}
-
-.detailed-status__meta {
- margin-top: 15px;
- color: $dark-text-color;
- font-size: 14px;
- line-height: 18px;
-}
-
-.detailed-status__action-bar {
- background: lighten($ui-base-color, 4%);
- border-top: 1px solid lighten($ui-base-color, 8%);
- border-bottom: 1px solid lighten($ui-base-color, 8%);
- display: flex;
- flex-direction: row;
- padding: 10px 0;
-}
-
-.detailed-status__link {
- color: $action-button-color;
- cursor: pointer;
- text-decoration: none;
- font-size: 13px;
-}
-
-.detailed-status__favorites,
-.detailed-status__reblogs {
- display: inline-block;
- font-weight: 500;
- font-size: 12px;
- margin-left: 6px;
-}
-
.reply-indicator__content {
color: $gab-default-text-light;
font-size: 14px;
diff --git a/app/styles/gabsocial/components/detailed-status.scss b/app/styles/gabsocial/components/detailed-status.scss
new file mode 100644
index 000000000..9009a8202
--- /dev/null
+++ b/app/styles/gabsocial/components/detailed-status.scss
@@ -0,0 +1,94 @@
+.detailed-status__action-bar-dropdown {
+ flex: 1 1 auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+}
+
+.detailed-status {
+ background: lighten($ui-base-color, 4%);
+ padding: 14px 10px;
+
+ &--flex {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: flex-start;
+
+ .status__content,
+ .detailed-status__meta {
+ flex: 100%;
+ }
+ }
+
+ .status__content {
+ font-size: 19px;
+ line-height: 24px;
+
+ .emojione {
+ width: 24px;
+ height: 24px;
+ margin: -1px 0 0;
+ }
+
+ .status__content__spoiler-link {
+ line-height: 24px;
+ margin: -1px 0 0;
+ }
+ }
+
+ .video-player {
+ margin-top: 8px;
+ }
+}
+
+.detailed-status__meta {
+ margin-top: 15px;
+ color: $dark-text-color;
+ font-size: 14px;
+ line-height: 18px;
+ display: flex;
+}
+
+.detailed-status__action-bar {
+ background: lighten($ui-base-color, 4%);
+ border-top: 1px solid lighten($ui-base-color, 8%);
+ border-bottom: 1px solid lighten($ui-base-color, 8%);
+ display: flex;
+ flex-direction: row;
+}
+
+.detailed-status__link {
+ color: $action-button-color;
+ cursor: pointer;
+ text-decoration: none;
+ font-size: 13px;
+}
+
+.detailed-status__favorites,
+.detailed-status__reblogs {
+ display: inline-block;
+ font-weight: 500;
+ font-size: 12px;
+ margin-left: 6px;
+}
+
+.detailed-status__button {
+ padding: 10px 0;
+
+ .icon-button {
+ display: inline-flex;
+ align-items: center;
+
+ .icon_button__text {
+ font-size: 14px;
+ padding-left: 3px;
+ transform: translateY(-1px);
+ }
+ }
+}
+
+.detailed-status__wrapper {
+ position: relative;
+}
diff --git a/app/styles/gabsocial/components/emoji-reacts.scss b/app/styles/gabsocial/components/emoji-reacts.scss
new file mode 100644
index 000000000..24783b3f3
--- /dev/null
+++ b/app/styles/gabsocial/components/emoji-reacts.scss
@@ -0,0 +1,120 @@
+.emoji-react {
+ display: inline-block;
+ transition: 0.1s;
+
+ &__emoji {
+ img {
+ width: 20px;
+ height: 20px;
+ filter: drop-shadow(2px 0 0 #fff); // FIXME: Use theme color
+ }
+ }
+
+ &__count {
+ display: none;
+ }
+
+ + .emoji-react {
+ margin-right: -8px;
+ }
+}
+
+.emoji-reacts {
+ display: inline-flex;
+ flex-direction: row-reverse;
+}
+
+.status-interaction-bar {
+ margin-right: auto;
+ min-height: 20px;
+}
+
+.emoji-reacts-container {
+ display: inline-flex;
+ margin-right: auto;
+
+ &:hover {
+ .emoji-react {
+ margin: 0;
+
+ &__count {
+ display: inline;
+ }
+ }
+
+ .emoji-reacts__count {
+ display: none;
+ }
+ }
+}
+
+.emoji-reacts__count,
+.emoji-react__count {
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.emoji-react-selector {
+ position: absolute;
+ display: flex;
+ background-color: #fff;
+ padding: 5px 8px;
+ border-radius: 9999px;
+ box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.1);
+ opacity: 0;
+ pointer-events: none;
+ transition: 0.1s;
+ z-index: 999;
+
+ &--visible {
+ opacity: 1;
+ pointer-events: all;
+ }
+
+ &__emoji {
+ display: block;
+ padding: 0;
+ margin: 0;
+ border: 0;
+ background: transparent;
+
+ img {
+ width: 30px;
+ height: 30px;
+ padding: 3px;
+ transition: 0.1s;
+ }
+
+ &:hover {
+ img {
+ width: 36px;
+ height: 36px;
+ padding: 0;
+ }
+ }
+ }
+}
+
+.status__action-bar__counter--favourite {
+ position: relative;
+
+ @media(max-width: 455px) {
+ position: static;
+ }
+}
+
+.detailed-status__wrapper .emoji-react-selector {
+ bottom: 40px;
+ right: 10px;
+}
+
+.status .emoji-react-selector {
+ bottom: 100%;
+ left: -20px;
+
+ @media(max-width: 455px) {
+ bottom: 31px;
+ right: 10px;
+ left: auto;
+ }
+}
diff --git a/package.json b/package.json
index 7b73a739d..8e700dc41 100644
--- a/package.json
+++ b/package.json
@@ -74,7 +74,7 @@
"html-webpack-harddisk-plugin": "^1.0.1",
"html-webpack-plugin": "^4.3.0",
"http-link-header": "^1.0.2",
- "immutable": "^3.8.2",
+ "immutable": "^4.0.0-rc.12",
"imports-loader": "^0.8.0",
"intersection-observer": "^0.7.0",
"intl": "^1.2.5",
diff --git a/yarn.lock b/yarn.lock
index be6152bc9..8185280d5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5167,10 +5167,10 @@ ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
-immutable@^3.8.2:
- version "3.8.2"
- resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
- integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
+immutable@^4.0.0-rc.12:
+ version "4.0.0-rc.12"
+ resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0-rc.12.tgz#ca59a7e4c19ae8d9bf74a97bdf0f6e2f2a5d0217"
+ integrity sha512-0M2XxkZLx/mi3t8NVwIm1g8nHoEmM9p9UBl/G9k4+hm0kBgOVdMV/B3CY5dQ8qG8qc80NN4gDV4HQv6FTJ5q7A==
import-cwd@^2.0.0:
version "2.1.0"