Emoji reactions See merge request soapbox-pub/soapbox-fe!18merge-requests/12/merge
commit
963c68c642
@ -0,0 +1,177 @@
|
||||
import api from '../api';
|
||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||
import { favourite, unfavourite } from './interactions';
|
||||
|
||||
export const EMOJI_REACT_REQUEST = 'EMOJI_REACT_REQUEST';
|
||||
export const EMOJI_REACT_SUCCESS = 'EMOJI_REACT_SUCCESS';
|
||||
export const EMOJI_REACT_FAIL = 'EMOJI_REACT_FAIL';
|
||||
|
||||
export const UNEMOJI_REACT_REQUEST = 'UNEMOJI_REACT_REQUEST';
|
||||
export const UNEMOJI_REACT_SUCCESS = 'UNEMOJI_REACT_SUCCESS';
|
||||
export const UNEMOJI_REACT_FAIL = 'UNEMOJI_REACT_FAIL';
|
||||
|
||||
export const EMOJI_REACTS_FETCH_REQUEST = 'EMOJI_REACTS_FETCH_REQUEST';
|
||||
export const EMOJI_REACTS_FETCH_SUCCESS = 'EMOJI_REACTS_FETCH_SUCCESS';
|
||||
export const EMOJI_REACTS_FETCH_FAIL = 'EMOJI_REACTS_FETCH_FAIL';
|
||||
|
||||
const noOp = () => () => new Promise(f => f());
|
||||
|
||||
export const simpleEmojiReact = (status, emoji) => {
|
||||
return (dispatch, getState) => {
|
||||
const emojiReacts = status.getIn(['pleroma', 'emoji_reactions']);
|
||||
|
||||
if (emoji === '👍' && status.get('favourited')) return dispatch(unfavourite(status));
|
||||
|
||||
const undo = emojiReacts.filter(e => e.get('me') === true && e.get('name') === emoji).count() > 0;
|
||||
if (undo) return dispatch(unEmojiReact(status, emoji));
|
||||
|
||||
return Promise.all(
|
||||
emojiReacts
|
||||
.filter(emojiReact => emojiReact.get('me') === true)
|
||||
.map(emojiReact => dispatch(unEmojiReact(status, emojiReact.get('name')))),
|
||||
status.get('favourited') && dispatch(unfavourite(status))
|
||||
).then(() => {
|
||||
if (emoji === '👍') {
|
||||
dispatch(favourite(status));
|
||||
} else {
|
||||
dispatch(emojiReact(status, emoji));
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchEmojiReacts(id, emoji) {
|
||||
return (dispatch, getState) => {
|
||||
if (!getState().get('me')) return dispatch(noOp());
|
||||
|
||||
dispatch(fetchEmojiReactsRequest(id, emoji));
|
||||
|
||||
const url = emoji
|
||||
? `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
||||
: `/api/v1/pleroma/statuses/${id}/reactions`;
|
||||
|
||||
return api(getState).get(url).then(response => {
|
||||
response.data.forEach(emojiReact => {
|
||||
dispatch(importFetchedAccounts(emojiReact.accounts));
|
||||
});
|
||||
dispatch(fetchEmojiReactsSuccess(id, response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchEmojiReactsFail(id, error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function emojiReact(status, emoji) {
|
||||
return function(dispatch, getState) {
|
||||
if (!getState().get('me')) return dispatch(noOp());
|
||||
|
||||
dispatch(emojiReactRequest(status, emoji));
|
||||
|
||||
return api(getState)
|
||||
.put(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`)
|
||||
.then(function(response) {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(emojiReactSuccess(status, emoji));
|
||||
}).catch(function(error) {
|
||||
dispatch(emojiReactFail(status, emoji, error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function unEmojiReact(status, emoji) {
|
||||
return (dispatch, getState) => {
|
||||
if (!getState().get('me')) return dispatch(noOp());
|
||||
|
||||
dispatch(unEmojiReactRequest(status, emoji));
|
||||
|
||||
return api(getState)
|
||||
.delete(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`)
|
||||
.then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(unEmojiReactSuccess(status, emoji));
|
||||
}).catch(error => {
|
||||
dispatch(unEmojiReactFail(status, emoji, error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchEmojiReactsRequest(id, emoji) {
|
||||
return {
|
||||
type: EMOJI_REACTS_FETCH_REQUEST,
|
||||
id,
|
||||
emoji,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchEmojiReactsSuccess(id, emojiReacts) {
|
||||
return {
|
||||
type: EMOJI_REACTS_FETCH_SUCCESS,
|
||||
id,
|
||||
emojiReacts,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchEmojiReactsFail(id, error) {
|
||||
return {
|
||||
type: EMOJI_REACTS_FETCH_FAIL,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export function emojiReactRequest(status, emoji) {
|
||||
return {
|
||||
type: EMOJI_REACT_REQUEST,
|
||||
status,
|
||||
emoji,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
export function emojiReactSuccess(status, emoji) {
|
||||
return {
|
||||
type: EMOJI_REACT_SUCCESS,
|
||||
status,
|
||||
emoji,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
export function emojiReactFail(status, emoji, error) {
|
||||
return {
|
||||
type: EMOJI_REACT_FAIL,
|
||||
status,
|
||||
emoji,
|
||||
error,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
export function unEmojiReactRequest(status, emoji) {
|
||||
return {
|
||||
type: UNEMOJI_REACT_REQUEST,
|
||||
status,
|
||||
emoji,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
export function unEmojiReactSuccess(status, emoji) {
|
||||
return {
|
||||
type: UNEMOJI_REACT_SUCCESS,
|
||||
status,
|
||||
emoji,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
export function unEmojiReactFail(status, emoji, error) {
|
||||
return {
|
||||
type: UNEMOJI_REACT_FAIL,
|
||||
status,
|
||||
emoji,
|
||||
error,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ALLOWED_EMOJI } from 'gabsocial/utils/emoji_reacts';
|
||||
import emojify from 'gabsocial/features/emoji/emoji';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class EmojiSelector extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
onReact: PropTypes.func.isRequired,
|
||||
visible: PropTypes.bool,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onReact: () => {},
|
||||
visible: false,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onReact, visible } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classNames('emoji-react-selector', { 'emoji-react-selector--visible': visible })}>
|
||||
{ALLOWED_EMOJI.map((emoji, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className='emoji-react-selector__emoji'
|
||||
dangerouslySetInnerHTML={{ __html: emojify(emoji) }}
|
||||
onClick={onReact(emoji)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -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 = () => (
|
||||
<div className='emoji-reacts-container'>
|
||||
<div className='emoji-reacts'>
|
||||
{emojiReacts.map((e, i) => (
|
||||
<span 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>
|
||||
))}
|
||||
</div>
|
||||
<div className='emoji-reacts__count'>
|
||||
{count}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='status-interaction-bar'>
|
||||
{count > 0 && <EmojiReactsContainer />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -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': '😯' },
|
||||
]));
|
||||
});
|
||||
});
|
@ -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,
|
||||
}));
|
||||
}
|
||||
};
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in new issue