diff --git a/app/gabsocial/actions/emoji_reacts.js b/app/gabsocial/actions/emoji_reacts.js new file mode 100644 index 000000000..9f36e8a21 --- /dev/null +++ b/app/gabsocial/actions/emoji_reacts.js @@ -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, + }; +}; diff --git a/app/gabsocial/components/emoji_selector.js b/app/gabsocial/components/emoji_selector.js new file mode 100644 index 000000000..fcaccdbed --- /dev/null +++ b/app/gabsocial/components/emoji_selector.js @@ -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 ( +
+ {ALLOWED_EMOJI.map((emoji, i) => ( +
+ ); + } + +} diff --git a/app/gabsocial/components/icon_button.js b/app/gabsocial/components/icon_button.js index f8816bbf7..2c12626ce 100644 --- a/app/gabsocial/components/icon_button.js +++ b/app/gabsocial/components/icon_button.js @@ -4,6 +4,7 @@ import spring from 'react-motion/lib/spring'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import Icon from 'gabsocial/components/icon'; +import emojify from 'gabsocial/features/emoji/emoji'; export default class IconButton extends React.PureComponent { @@ -12,6 +13,8 @@ export default class IconButton extends React.PureComponent { title: PropTypes.string.isRequired, icon: PropTypes.string.isRequired, onClick: PropTypes.func, + onMouseEnter: PropTypes.func, + onMouseLeave: PropTypes.func, size: PropTypes.number, active: PropTypes.bool, pressed: PropTypes.bool, @@ -23,6 +26,8 @@ export default class IconButton extends React.PureComponent { animate: PropTypes.bool, overlay: PropTypes.bool, tabIndex: PropTypes.string, + text: PropTypes.string, + emoji: PropTypes.string, }; static defaultProps = { @@ -32,6 +37,9 @@ export default class IconButton extends React.PureComponent { animate: false, overlay: false, tabIndex: '0', + onClick: () => {}, + onMouseEnter: () => {}, + onMouseLeave: () => {}, }; handleClick = (e) => { @@ -64,6 +72,8 @@ export default class IconButton extends React.PureComponent { pressed, tabIndex, title, + text, + emoji, } = this.props; const classes = classNames(className, 'icon-button', { @@ -84,11 +94,17 @@ export default class IconButton extends React.PureComponent { title={title} className={classes} onClick={this.handleClick} - style={style} + onMouseEnter={this.props.onMouseEnter} + onMouseLeave={this.props.onMouseLeave} tabIndex={tabIndex} disabled={disabled} > -