Merge branch 'emojireacts' into 'master'
Emoji reactions See merge request soapbox-pub/soapbox-fe!18
This commit is contained in:
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import spring from 'react-motion/lib/spring';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Icon from 'gabsocial/components/icon';
|
import Icon from 'gabsocial/components/icon';
|
||||||
|
import emojify from 'gabsocial/features/emoji/emoji';
|
||||||
|
|
||||||
export default class IconButton extends React.PureComponent {
|
export default class IconButton extends React.PureComponent {
|
||||||
|
|
||||||
|
@ -12,6 +13,8 @@ export default class IconButton extends React.PureComponent {
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
icon: PropTypes.string.isRequired,
|
icon: PropTypes.string.isRequired,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
|
onMouseEnter: PropTypes.func,
|
||||||
|
onMouseLeave: PropTypes.func,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
active: PropTypes.bool,
|
active: PropTypes.bool,
|
||||||
pressed: PropTypes.bool,
|
pressed: PropTypes.bool,
|
||||||
|
@ -23,6 +26,8 @@ export default class IconButton extends React.PureComponent {
|
||||||
animate: PropTypes.bool,
|
animate: PropTypes.bool,
|
||||||
overlay: PropTypes.bool,
|
overlay: PropTypes.bool,
|
||||||
tabIndex: PropTypes.string,
|
tabIndex: PropTypes.string,
|
||||||
|
text: PropTypes.string,
|
||||||
|
emoji: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -32,6 +37,9 @@ export default class IconButton extends React.PureComponent {
|
||||||
animate: false,
|
animate: false,
|
||||||
overlay: false,
|
overlay: false,
|
||||||
tabIndex: '0',
|
tabIndex: '0',
|
||||||
|
onClick: () => {},
|
||||||
|
onMouseEnter: () => {},
|
||||||
|
onMouseLeave: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
handleClick = (e) => {
|
handleClick = (e) => {
|
||||||
|
@ -64,6 +72,8 @@ export default class IconButton extends React.PureComponent {
|
||||||
pressed,
|
pressed,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
title,
|
title,
|
||||||
|
text,
|
||||||
|
emoji,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const classes = classNames(className, 'icon-button', {
|
const classes = classNames(className, 'icon-button', {
|
||||||
|
@ -84,11 +94,17 @@ export default class IconButton extends React.PureComponent {
|
||||||
title={title}
|
title={title}
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
style={style}
|
onMouseEnter={this.props.onMouseEnter}
|
||||||
|
onMouseLeave={this.props.onMouseLeave}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Icon id={icon} fixedWidth aria-hidden='true' />
|
<div style={style}>
|
||||||
|
{emoji
|
||||||
|
? <div className='icon-button__emoji' dangerouslySetInnerHTML={{ __html: emojify(emoji) }} aria-hidden='true' />
|
||||||
|
: <Icon id={icon} fixedWidth aria-hidden='true' />}
|
||||||
|
</div>
|
||||||
|
{text && <span className='icon_button__text'>{text}</span>}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -103,11 +119,17 @@ export default class IconButton extends React.PureComponent {
|
||||||
title={title}
|
title={title}
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
style={style}
|
onMouseEnter={this.props.onMouseEnter}
|
||||||
|
onMouseLeave={this.props.onMouseLeave}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />
|
<div style={style}>
|
||||||
|
{emoji
|
||||||
|
? <div className='icon-button__emoji' style={{ transform: `rotate(${rotate}deg)` }} dangerouslySetInnerHTML={{ __html: emojify(emoji) }} aria-hidden='true' />
|
||||||
|
: <Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />}
|
||||||
|
</div>
|
||||||
|
{text && <span className='icon_button__text'>{text}</span>}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Motion>
|
</Motion>
|
||||||
|
|
|
@ -131,7 +131,7 @@ class SidebarMenu extends ImmutablePureComponent {
|
||||||
<Icon id='dollar' />
|
<Icon id='dollar' />
|
||||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.donate)}</span>
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.donate)}</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
: ''}
|
: ''}
|
||||||
<NavLink className='sidebar-menu-item' to='/lists' onClick={onClose}>
|
<NavLink className='sidebar-menu-item' to='/lists' onClick={onClose}>
|
||||||
<Icon id='list' />
|
<Icon id='list' />
|
||||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.lists)}</span>
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.lists)}</span>
|
||||||
|
|
|
@ -11,6 +11,9 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { isStaff } from 'gabsocial/utils/accounts';
|
import { isStaff } from 'gabsocial/utils/accounts';
|
||||||
import { openModal } from '../actions/modal';
|
import { openModal } from '../actions/modal';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import EmojiSelector from 'gabsocial/components/emoji_selector';
|
||||||
|
import { getReactForStatus, reduceEmoji } from 'gabsocial/utils/emoji_reacts';
|
||||||
|
import { simpleEmojiReact } from 'gabsocial/actions/emoji_reacts';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
|
@ -74,6 +77,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
isStaff: false,
|
isStaff: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
emojiSelectorVisible: false,
|
||||||
|
}
|
||||||
|
|
||||||
// Avoid checking props that are functions (and whose equality will always
|
// Avoid checking props that are functions (and whose equality will always
|
||||||
// evaluate to false. See react-immutable-pure-component for usage.
|
// evaluate to false. See react-immutable-pure-component for usage.
|
||||||
updateOnProps = [
|
updateOnProps = [
|
||||||
|
@ -99,6 +106,41 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isMobile = () => window.matchMedia('only screen and (max-width: 895px)').matches;
|
||||||
|
|
||||||
|
handleLikeButtonHover = e => {
|
||||||
|
if (!this.isMobile()) this.setState({ emojiSelectorVisible: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLikeButtonLeave = e => {
|
||||||
|
if (!this.isMobile()) this.setState({ emojiSelectorVisible: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLikeButtonClick = e => {
|
||||||
|
const meEmojiReact = getReactForStatus(this.props.status) || '👍';
|
||||||
|
if (this.isMobile()) {
|
||||||
|
if (this.state.emojiSelectorVisible) {
|
||||||
|
this.handleReactClick(meEmojiReact)();
|
||||||
|
} else {
|
||||||
|
this.setState({ emojiSelectorVisible: true });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.handleReactClick(meEmojiReact)();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReactClick = emoji => {
|
||||||
|
return e => {
|
||||||
|
const { me, status } = this.props;
|
||||||
|
if (me) {
|
||||||
|
this.props.dispatch(simpleEmojiReact(status, emoji));
|
||||||
|
} else {
|
||||||
|
this.props.onOpenUnauthorizedModal();
|
||||||
|
}
|
||||||
|
this.setState({ emojiSelectorVisible: false });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
handleFavouriteClick = () => {
|
handleFavouriteClick = () => {
|
||||||
const { me } = this.props;
|
const { me } = this.props;
|
||||||
if (me) {
|
if (me) {
|
||||||
|
@ -251,14 +293,32 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.node = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
if (this.node && !this.node.contains(e.target))
|
||||||
|
this.setState({ emojiSelectorVisible: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { status, intl } = this.props;
|
const { status, intl } = this.props;
|
||||||
|
const { emojiSelectorVisible } = this.state;
|
||||||
|
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
|
|
||||||
const replyCount = status.get('replies_count');
|
const replyCount = status.get('replies_count');
|
||||||
const reblogCount = status.get('reblogs_count');
|
const reblogCount = status.get('reblogs_count');
|
||||||
const favoriteCount = status.get('favourites_count');
|
const favouriteCount = status.get('favourites_count');
|
||||||
|
const emojiReactCount = reduceEmoji(
|
||||||
|
status.getIn(['pleroma', 'emoji_reactions'], []),
|
||||||
|
favouriteCount,
|
||||||
|
status.get('favourited'),
|
||||||
|
).reduce((acc, cur) => acc + cur.get('count'), 0);
|
||||||
|
const meEmojiReact = getReactForStatus(status);
|
||||||
|
|
||||||
let menu = this._makeMenu(publicStatus);
|
let menu = this._makeMenu(publicStatus);
|
||||||
let reblogIcon = 'retweet';
|
let reblogIcon = 'retweet';
|
||||||
|
@ -293,9 +353,23 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
<IconButton className='status__action-bar-button' disabled={!publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
|
<IconButton className='status__action-bar-button' disabled={!publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
|
||||||
{reblogCount !== 0 && <Link to={`/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}/reblogs`} className='detailed-status__link'>{reblogCount}</Link>}
|
{reblogCount !== 0 && <Link to={`/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}/reblogs`} className='detailed-status__link'>{reblogCount}</Link>}
|
||||||
</div>
|
</div>
|
||||||
<div className='status__action-bar__counter'>
|
<div
|
||||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
className='status__action-bar__counter status__action-bar__counter--favourite'
|
||||||
{favoriteCount !== 0 && <span className='detailed-status__link'>{favoriteCount}</span>}
|
onMouseEnter={this.handleLikeButtonHover}
|
||||||
|
onMouseLeave={this.handleLikeButtonLeave}
|
||||||
|
ref={this.setRef}
|
||||||
|
>
|
||||||
|
<EmojiSelector onReact={this.handleReactClick} visible={emojiSelectorVisible} />
|
||||||
|
<IconButton
|
||||||
|
className='status__action-bar-button star-icon'
|
||||||
|
animate
|
||||||
|
active={Boolean(meEmojiReact)}
|
||||||
|
title={intl.formatMessage(messages.favourite)}
|
||||||
|
icon='thumbs-up'
|
||||||
|
emoji={meEmojiReact}
|
||||||
|
onClick={this.handleLikeButtonClick}
|
||||||
|
/>
|
||||||
|
{emojiReactCount !== 0 && <span className='detailed-status__link'>{emojiReactCount}</span>}
|
||||||
</div>
|
</div>
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
||||||
|
@ -317,6 +391,7 @@ const mapStateToProps = state => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
dispatch,
|
||||||
onOpenUnauthorizedModal() {
|
onOpenUnauthorizedModal() {
|
||||||
dispatch(openModal('UNAUTHORIZED'));
|
dispatch(openModal('UNAUTHORIZED'));
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,6 +8,7 @@ import Permalink from '../../../components/permalink';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { HotKeys } from 'react-hotkeys';
|
import { HotKeys } from 'react-hotkeys';
|
||||||
import Icon from 'gabsocial/components/icon';
|
import Icon from 'gabsocial/components/icon';
|
||||||
|
import emojify from 'gabsocial/features/emoji/emoji';
|
||||||
|
|
||||||
const notificationForScreenReader = (intl, message, timestamp) => {
|
const notificationForScreenReader = (intl, message, timestamp) => {
|
||||||
const output = [message];
|
const output = [message];
|
||||||
|
@ -141,19 +142,51 @@ class Notification extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderEmojiReact(notification, link) {
|
||||||
|
const { intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
|
<div className='notification notification-emoji-react focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.emoji_react', defaultMessage: '{name} reacted to your post' }, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||||
|
<div className='notification__message'>
|
||||||
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: emojify(emojify(notification.get('emoji'))) }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span title={notification.get('created_at')}>
|
||||||
|
<FormattedMessage id='notification.emoji_react' defaultMessage='{name} reacted to your post' values={{ name: link }} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusContainer
|
||||||
|
id={notification.get('status')}
|
||||||
|
account={notification.get('account')}
|
||||||
|
muted
|
||||||
|
withDismiss
|
||||||
|
hidden={!!this.props.hidden}
|
||||||
|
getScrollPosition={this.props.getScrollPosition}
|
||||||
|
updateScrollBottom={this.props.updateScrollBottom}
|
||||||
|
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||||
|
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderFavourite(notification, link) {
|
renderFavourite(notification, link) {
|
||||||
const { intl } = this.props;
|
const { intl } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={this.getHandlers()}>
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
<div className='notification notification-favourite focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.favourite', defaultMessage: '{name} favorited your post' }, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
<div className='notification notification-favourite focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.favourite', defaultMessage: '{name} liked your post' }, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||||
<div className='notification__message'>
|
<div className='notification__message'>
|
||||||
<div className='notification__favourite-icon-wrapper'>
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
<Icon id='star' className='star-icon' fixedWidth />
|
<Icon id='thumbs-up' className='star-icon' fixedWidth />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span title={notification.get('created_at')}>
|
<span title={notification.get('created_at')}>
|
||||||
<FormattedMessage id='notification.favourite' defaultMessage='{name} favorited your post' values={{ name: link }} />
|
<FormattedMessage id='notification.favourite' defaultMessage='{name} liked your post' values={{ name: link }} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -254,6 +287,8 @@ class Notification extends ImmutablePureComponent {
|
||||||
return this.renderReblog(notification, link);
|
return this.renderReblog(notification, link);
|
||||||
case 'poll':
|
case 'poll':
|
||||||
return this.renderPoll(notification);
|
return this.renderPoll(notification);
|
||||||
|
case 'pleroma:emoji_reaction':
|
||||||
|
return this.renderEmojiReact(notification, link);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -8,6 +8,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import { isStaff } from 'gabsocial/utils/accounts';
|
import { isStaff } from 'gabsocial/utils/accounts';
|
||||||
|
import EmojiSelector from 'gabsocial/components/emoji_selector';
|
||||||
|
import { getReactForStatus } from 'gabsocial/utils/emoji_reacts';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
|
@ -59,6 +61,7 @@ class ActionBar extends React.PureComponent {
|
||||||
onReply: PropTypes.func.isRequired,
|
onReply: PropTypes.func.isRequired,
|
||||||
onReblog: PropTypes.func.isRequired,
|
onReblog: PropTypes.func.isRequired,
|
||||||
onFavourite: PropTypes.func.isRequired,
|
onFavourite: PropTypes.func.isRequired,
|
||||||
|
onEmojiReact: PropTypes.func.isRequired,
|
||||||
onDelete: PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
onDirect: PropTypes.func.isRequired,
|
onDirect: PropTypes.func.isRequired,
|
||||||
onMention: PropTypes.func.isRequired,
|
onMention: PropTypes.func.isRequired,
|
||||||
|
@ -78,6 +81,10 @@ class ActionBar extends React.PureComponent {
|
||||||
isStaff: false,
|
isStaff: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
emojiSelectorVisible: false,
|
||||||
|
}
|
||||||
|
|
||||||
handleReplyClick = () => {
|
handleReplyClick = () => {
|
||||||
const { me } = this.props;
|
const { me } = this.props;
|
||||||
if (me) {
|
if (me) {
|
||||||
|
@ -105,6 +112,41 @@ class ActionBar extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isMobile = () => window.matchMedia('only screen and (max-width: 895px)').matches;
|
||||||
|
|
||||||
|
handleLikeButtonHover = e => {
|
||||||
|
if (!this.isMobile()) this.setState({ emojiSelectorVisible: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLikeButtonLeave = e => {
|
||||||
|
if (!this.isMobile()) this.setState({ emojiSelectorVisible: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLikeButtonClick = e => {
|
||||||
|
const meEmojiReact = getReactForStatus(this.props.status) || '👍';
|
||||||
|
if (this.isMobile()) {
|
||||||
|
if (this.state.emojiSelectorVisible) {
|
||||||
|
this.handleReactClick(meEmojiReact)();
|
||||||
|
} else {
|
||||||
|
this.setState({ emojiSelectorVisible: true });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.handleReactClick(meEmojiReact)();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReactClick = emoji => {
|
||||||
|
return e => {
|
||||||
|
const { me } = this.props;
|
||||||
|
if (me) {
|
||||||
|
this.props.onEmojiReact(this.props.status, emoji);
|
||||||
|
} else {
|
||||||
|
this.props.onOpenUnauthorizedModal();
|
||||||
|
}
|
||||||
|
this.setState({ emojiSelectorVisible: false });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
handleDeleteClick = () => {
|
handleDeleteClick = () => {
|
||||||
this.props.onDelete(this.props.status, this.context.router.history);
|
this.props.onDelete(this.props.status, this.context.router.history);
|
||||||
}
|
}
|
||||||
|
@ -171,11 +213,24 @@ class ActionBar extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.node = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
if (this.node && !this.node.contains(e.target))
|
||||||
|
this.setState({ emojiSelectorVisible: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { status, intl, me, isStaff } = this.props;
|
const { status, intl, me, isStaff } = this.props;
|
||||||
|
const { emojiSelectorVisible } = this.state;
|
||||||
|
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
|
const meEmojiReact = getReactForStatus(status);
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
|
@ -232,9 +287,42 @@ class ActionBar extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='detailed-status__action-bar'>
|
<div className='detailed-status__action-bar'>
|
||||||
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
|
<div className='detailed-status__button'>
|
||||||
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
|
<IconButton
|
||||||
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
|
title={intl.formatMessage(messages.reply)}
|
||||||
|
icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon}
|
||||||
|
onClick={this.handleReplyClick}
|
||||||
|
text={intl.formatMessage(messages.reply)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='detailed-status__button'>
|
||||||
|
<IconButton
|
||||||
|
disabled={reblog_disabled}
|
||||||
|
active={status.get('reblogged')}
|
||||||
|
title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)}
|
||||||
|
icon={reblogIcon}
|
||||||
|
onClick={this.handleReblogClick}
|
||||||
|
text='Boost'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='detailed-status__button detailed-status__button--favourite'
|
||||||
|
onMouseEnter={this.handleLikeButtonHover}
|
||||||
|
onMouseLeave={this.handleLikeButtonLeave}
|
||||||
|
ref={this.setRef}
|
||||||
|
>
|
||||||
|
<EmojiSelector onReact={this.handleReactClick} visible={emojiSelectorVisible} />
|
||||||
|
<IconButton
|
||||||
|
className='star-icon'
|
||||||
|
animate
|
||||||
|
active={Boolean(meEmojiReact)}
|
||||||
|
title={intl.formatMessage(messages.favourite)}
|
||||||
|
icon='thumbs-up'
|
||||||
|
emoji={meEmojiReact}
|
||||||
|
text='Like'
|
||||||
|
onClick={this.handleLikeButtonClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
||||||
<div className='detailed-status__action-bar-dropdown'>
|
<div className='detailed-status__action-bar-dropdown'>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import scheduleIdleTask from '../../ui/util/schedule_idle_task';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Icon from 'gabsocial/components/icon';
|
import Icon from 'gabsocial/components/icon';
|
||||||
import PollContainer from 'gabsocial/containers/poll_container';
|
import PollContainer from 'gabsocial/containers/poll_container';
|
||||||
|
import { StatusInteractionBar } from './status_interaction_bar';
|
||||||
|
|
||||||
export default class DetailedStatus extends ImmutablePureComponent {
|
export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
@ -92,7 +93,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
let applicationLink = '';
|
let applicationLink = '';
|
||||||
let reblogLink = '';
|
let reblogLink = '';
|
||||||
let reblogIcon = 'retweet';
|
let reblogIcon = 'retweet';
|
||||||
let favouriteLink = '';
|
|
||||||
|
|
||||||
if (this.props.measureHeight) {
|
if (this.props.measureHeight) {
|
||||||
outerStyle.height = `${this.state.height}px`;
|
outerStyle.height = `${this.state.height}px`;
|
||||||
|
@ -169,15 +169,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
favouriteLink = (
|
|
||||||
<span className='detailed-status__link'>
|
|
||||||
<Icon id='star' />
|
|
||||||
<span className='detailed-status__favorites'>
|
|
||||||
<FormattedNumber value={status.get('favourites_count')} />
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={outerStyle}>
|
<div style={outerStyle}>
|
||||||
<div ref={this.setRef} className={classNames('detailed-status', { compact })}>
|
<div ref={this.setRef} className={classNames('detailed-status', { compact })}>
|
||||||
|
@ -197,9 +188,12 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
{media}
|
{media}
|
||||||
|
|
||||||
<div className='detailed-status__meta'>
|
<div className='detailed-status__meta'>
|
||||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
|
<StatusInteractionBar status={status} />
|
||||||
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
<div>
|
||||||
</a>{applicationLink} · {reblogLink} · {favouriteLink}
|
{reblogLink} {applicationLink} · <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
|
||||||
|
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ import {
|
||||||
pin,
|
pin,
|
||||||
unpin,
|
unpin,
|
||||||
} from '../../actions/interactions';
|
} from '../../actions/interactions';
|
||||||
|
import { simpleEmojiReact } from '../../actions/emoji_reacts';
|
||||||
import {
|
import {
|
||||||
replyCompose,
|
replyCompose,
|
||||||
mentionCompose,
|
mentionCompose,
|
||||||
|
@ -161,6 +162,10 @@ class Status extends ImmutablePureComponent {
|
||||||
this.setState({ showMedia: !this.state.showMedia });
|
this.setState({ showMedia: !this.state.showMedia });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleEmojiReactClick = (status, emoji) => {
|
||||||
|
this.props.dispatch(simpleEmojiReact(status, emoji));
|
||||||
|
}
|
||||||
|
|
||||||
handleFavouriteClick = (status) => {
|
handleFavouriteClick = (status) => {
|
||||||
if (status.get('favourited')) {
|
if (status.get('favourited')) {
|
||||||
this.props.dispatch(unfavourite(status));
|
this.props.dispatch(unfavourite(status));
|
||||||
|
@ -496,6 +501,7 @@ class Status extends ImmutablePureComponent {
|
||||||
status={status}
|
status={status}
|
||||||
onReply={this.handleReplyClick}
|
onReply={this.handleReplyClick}
|
||||||
onFavourite={this.handleFavouriteClick}
|
onFavourite={this.handleFavouriteClick}
|
||||||
|
onEmojiReact={this.handleEmojiReactClick}
|
||||||
onReblog={this.handleReblogClick}
|
onReblog={this.handleReblogClick}
|
||||||
onDelete={this.handleDeleteClick}
|
onDelete={this.handleDeleteClick}
|
||||||
onDirect={this.handleDirectClick}
|
onDirect={this.handleDirectClick}
|
||||||
|
|
|
@ -41,6 +41,7 @@ const notificationToMap = notification => ImmutableMap({
|
||||||
account: notification.account.id,
|
account: notification.account.id,
|
||||||
created_at: notification.created_at,
|
created_at: notification.created_at,
|
||||||
status: notification.status ? notification.status.id : null,
|
status: notification.status ? notification.status.id : null,
|
||||||
|
emoji: notification.emoji,
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeNotification = (state, notification) => {
|
const normalizeNotification = (state, notification) => {
|
||||||
|
|
|
@ -10,9 +10,13 @@ import {
|
||||||
STATUS_REVEAL,
|
STATUS_REVEAL,
|
||||||
STATUS_HIDE,
|
STATUS_HIDE,
|
||||||
} from '../actions/statuses';
|
} from '../actions/statuses';
|
||||||
|
import {
|
||||||
|
EMOJI_REACT_REQUEST,
|
||||||
|
} from '../actions/emoji_reacts';
|
||||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||||
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
|
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
import { simulateEmojiReact } from 'gabsocial/utils/emoji_reacts';
|
||||||
|
|
||||||
const importStatus = (state, status) => state.set(status.id, fromJS(status));
|
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);
|
return importStatuses(state, action.statuses);
|
||||||
case FAVOURITE_REQUEST:
|
case FAVOURITE_REQUEST:
|
||||||
return state.setIn([action.status.get('id'), 'favourited'], true);
|
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:
|
case FAVOURITE_FAIL:
|
||||||
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false);
|
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false);
|
||||||
case REBLOG_REQUEST:
|
case REBLOG_REQUEST:
|
||||||
|
|
|
@ -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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
|
@ -32,6 +32,8 @@
|
||||||
@import 'gabsocial/components/group-sidebar-panel';
|
@import 'gabsocial/components/group-sidebar-panel';
|
||||||
@import 'gabsocial/components/sidebar-menu';
|
@import 'gabsocial/components/sidebar-menu';
|
||||||
@import 'gabsocial/components/hotkeys-modal';
|
@import 'gabsocial/components/hotkeys-modal';
|
||||||
|
@import 'gabsocial/components/emoji-reacts';
|
||||||
|
@import 'gabsocial/components/detailed-status';
|
||||||
|
|
||||||
@import 'gabsocial/polls';
|
@import 'gabsocial/polls';
|
||||||
@import 'gabsocial/introduction';
|
@import 'gabsocial/introduction';
|
||||||
|
|
|
@ -119,7 +119,7 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: $gab-secondary-text;
|
color: $gab-secondary-text;
|
||||||
border: none;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 100ms ease-in;
|
transition: color 100ms ease-in;
|
||||||
|
@ -718,82 +718,6 @@
|
||||||
width: 23.15px;
|
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 {
|
.reply-indicator__content {
|
||||||
color: $gab-default-text-light;
|
color: $gab-default-text-light;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,7 +74,7 @@
|
||||||
"html-webpack-harddisk-plugin": "^1.0.1",
|
"html-webpack-harddisk-plugin": "^1.0.1",
|
||||||
"html-webpack-plugin": "^4.3.0",
|
"html-webpack-plugin": "^4.3.0",
|
||||||
"http-link-header": "^1.0.2",
|
"http-link-header": "^1.0.2",
|
||||||
"immutable": "^3.8.2",
|
"immutable": "^4.0.0-rc.12",
|
||||||
"imports-loader": "^0.8.0",
|
"imports-loader": "^0.8.0",
|
||||||
"intersection-observer": "^0.7.0",
|
"intersection-observer": "^0.7.0",
|
||||||
"intl": "^1.2.5",
|
"intl": "^1.2.5",
|
||||||
|
|
|
@ -5167,10 +5167,10 @@ ignore@^4.0.6:
|
||||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
||||||
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
|
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
|
||||||
|
|
||||||
immutable@^3.8.2:
|
immutable@^4.0.0-rc.12:
|
||||||
version "3.8.2"
|
version "4.0.0-rc.12"
|
||||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
|
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0-rc.12.tgz#ca59a7e4c19ae8d9bf74a97bdf0f6e2f2a5d0217"
|
||||||
integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
|
integrity sha512-0M2XxkZLx/mi3t8NVwIm1g8nHoEmM9p9UBl/G9k4+hm0kBgOVdMV/B3CY5dQ8qG8qc80NN4gDV4HQv6FTJ5q7A==
|
||||||
|
|
||||||
import-cwd@^2.0.0:
|
import-cwd@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
|
|
Loading…
Reference in New Issue